debugger: D5, the network transport -- and the plan closes

schdbgd bridges TCP to the debugger, which is the one way in that does not
need you to be sitting at the machine. It is a bridge and nothing else: a line
in becomes a line written to /dev/dbgro and the answer comes back, so `nc` is a
complete client and there is no clever client to be broken on the day you need
this.

    # schdbgd &
    schdbgd: debugger on port 4444, loopback only
    $ nc 127.0.0.1 4444
    dma
    arena  0xffff80003da41000-0xffff80003ea41000  16384 KB total, ...

A kernel debugger plus a TCP socket is a remote root hole, so three things were
decided rather than discovered:

  - It does not start by itself. schdbgd.job ships Disabled. SchOS has no
    kernel command line, so the boot flag the plan asked for is the job
    manifest, which is the same thing in this system's own idiom.
  - It binds loopback; -a names an address and says so in a paragraph.
  - It cannot reach a view that changes state, enforced in the kernel.

That last one is /dev/dbgro: /dev/dbg on minor 1. The minor is the one piece of
per-open state a character device already sees, so which node you opened is the
permission -- nothing to latch, nothing to remember between the write and the
read, no way across. Which views are refused comes from dbg_register_rw() on
the registration itself, because a central list of dangerous views goes stale
in the direction where a new view is quietly treated as safe. Today that is
`watch <addr>`; `watch` with no argument is still the listing and still
answered.

Also: socket() now accepts IPPROTO_TCP with SOCK_STREAM and IPPROTO_UDP with
SOCK_DGRAM. They name the default rather than departing from it, and toybox's
nc writes them out -- so TCP was "Protocol not supported" on a system whose
whole network is TCP.

docs/plan/debugger.md retires into docs/architecture/debugging.md; 16 files
repointed. Every plan in docs/plan/ that was open is now closed, so the next
thing is Gate 0 itself: the survey, with the instrument built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-14 15:33:53 +02:00
commit bc39b73969
23 changed files with 491 additions and 293 deletions

View file

@ -6,7 +6,7 @@ struct sbuf;
// Asking the kernel a question.
//
// docs/plan/debugger.md milestone D2. The thing this exists to end is the
// docs/architecture/debugging.md milestone D2. The thing this exists to end is the
// ninety-second loop: add a cprintf, rebuild, boot, read the serial log, and
// learn the answer to exactly the one question you thought of before the
// rebuild.
@ -37,6 +37,31 @@ typedef void (*dbg_view_fn)(struct sbuf *out, const char *arg);
// and needs no initialisation of its own.
void dbg_register(const char *name, dbg_view_fn fn, const char *what);
// The same, for a view that **changes something**.
//
// Nearly every view only reads, and the few that do not are worth separating
// because of where this can now be reached from. `watch 0xffff...` arms a debug
// register; that is a legitimate thing for a debugger to do and an illegitimate
// thing for a stranger on a TCP socket to do, and milestone D5 put a TCP socket
// in front of it.
//
// The flag lives on the registration and not in a list somewhere, for the same
// reason the registration exists at all: a central list of which views are
// dangerous is a list that goes stale, and it goes stale in the direction where
// a new view is quietly treated as safe.
void dbg_register_rw(const char *name, dbg_view_fn fn, const char *what);
// Answer only the views that read.
//
// `/dev/dbgro` is the same device on minor 1. The minor is the one piece of
// per-open state a character device already sees, so *which node you opened* is
// the permission -- there is nothing to latch, nothing to remember between the
// write and the read, and no way to escalate from one descriptor to the other.
//
// `schdbgd` opens that one and never the other, so nothing reachable over the
// network can arm a watchpoint. See cmd/schdbgd.
int dbg_command_ro(const char *line, struct sbuf *out, int ro);
// --- whose <addr> -----------------------------------------------------------
//
// The query the views cannot answer between them: *who owns this page?*
@ -67,7 +92,7 @@ void dbg_register_whose(const char *name, dbg_whose_fn fn);
// Run one command line and render the answer. Returns the number of bytes
// written into `out`.
//
// The protocol is lines of text -- see docs/plan/debugger.md for why that and
// The protocol is lines of text -- see docs/architecture/debugging.md for why that and
// not a typed binary format. Commands:
//
// help what this kernel can be asked

View file

@ -365,7 +365,7 @@ void ioapicinit(void);
// dma.c — see include/dma.h for the full contract
void dma_init(void*, void*);
// watch.c -- hardware watchpoints on DR0-DR3. See docs/plan/debugger.md, D4.
// watch.c -- hardware watchpoints on DR0-DR3. See docs/architecture/debugging.md, D4.
struct trapframe;
void watch_init(void);
void watch_sync(void);
@ -393,7 +393,7 @@ uint64 klog_len(void);
uint64 klog_prev_bytes(void);
int klog_recovered(void);
// ksym.c -- naming a kernel address. See docs/plan/debugger.md, D0.
// ksym.c -- naming a kernel address. See docs/architecture/debugging.md, D0.
int ksym_ready(void);
const char* ksym_lookup(uint64 addr, uint64 *off);
int ksym_fmt(char *buf, int cap, uint64 addr);

View file

@ -4,7 +4,7 @@
// The kernel log that outlives the kernel.
//
// docs/plan/debugger.md milestone D1. Until this existed, `cprintf` wrote to a
// docs/architecture/debugging.md milestone D1. Until this existed, `cprintf` wrote to a
// serial line and a screen and was then gone: a panic's report could be read
// only by whoever was watching at the time, and a triple fault left nothing at
// all. panic.c said so in its own header comment -- *"there is no core dump and

View file

@ -8,7 +8,7 @@
// static to it. The debugger's introspection registry needs the same thing --
// a subsystem rendering its own state into a caller's buffer, with no
// allocation and no way to run off the end -- and the choice was to write a
// second set or to move this one. See docs/plan/debugger.md, D2.
// second set or to move this one. See docs/architecture/debugging.md, D2.
//
// Nothing here can overflow: every writer checks cap, and a full buffer
// silently drops the rest. That is the right trade for something whose callers

View file

@ -43,6 +43,7 @@
#define SOL_SOCKET 1
#define IPPROTO_ICMP 1
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
#define SO_REUSEADDR 2
#define SO_TYPE 3