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
108 lines
4.9 KiB
C
108 lines
4.9 KiB
C
#pragma once
|
|
|
|
#include "types.h"
|
|
|
|
struct sbuf;
|
|
|
|
// Asking the kernel a question.
|
|
//
|
|
// 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.
|
|
//
|
|
// The interesting part is not the views. It is that a subsystem publishes its
|
|
// own state with **one line at the bottom of the file that owns the data**, and
|
|
// nothing central has to be edited to add one:
|
|
//
|
|
// dbg_register("dma", dma_dump, "the DMA arena's free extents");
|
|
//
|
|
// That is the difference between something that stays current and something
|
|
// that does not. /proc/sysrq is the version that does not -- a switch in one
|
|
// file with a letter per action, which is why after two years it holds three
|
|
// self-tests and no state at all.
|
|
//
|
|
// Views run in ordinary process context, from a write to /dev/dbg or from
|
|
// sysrq, so they may take locks. **They must not be called from panic().**
|
|
|
|
// The argument is whatever followed the view's name on the command line, or
|
|
// "" when there was none. Most views ignore it; the ones that can produce a
|
|
// hundred lines use it to narrow -- `vma 4` rather than `vma`. It is also the
|
|
// shape milestone D3 needs, since `whose <addr>` is nothing but an argument.
|
|
typedef void (*dbg_view_fn)(struct sbuf *out, const char *arg);
|
|
|
|
// Publish a view. `what` is one line, shown by `help`.
|
|
//
|
|
// Safe to call from any subsystem's init, in any order: the table is static
|
|
// 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?*
|
|
//
|
|
// Each view speaks for one subsystem, which is what keeps them cheap and
|
|
// current -- and it is exactly why none of them can see the bug that matters.
|
|
// A page on the DMA arena's free list looks fine from dma.c. The same page on
|
|
// kalloc's free list looks fine from kalloc.c. That it is on both is a fact no
|
|
// single subsystem holds, and it is the bug that cost this project a day.
|
|
//
|
|
// So `whose` asks every subsystem about one page and counts the answers. The
|
|
// contributors are registered the same way views are, for the same reason:
|
|
// adding one is a line in the file that owns the data.
|
|
|
|
struct whose {
|
|
uint64 phys; // the page being asked about, page-aligned
|
|
uint64 off; // how far into it the original address was
|
|
int owners; // allocators and reserved regions claiming it
|
|
int free_owners; // of those, how many believe it is unused
|
|
int mappings; // user page tables mapping it
|
|
};
|
|
|
|
typedef void (*dbg_whose_fn)(struct sbuf *out, struct whose *w);
|
|
|
|
// Publish a claimant. `name` is the column it prints under.
|
|
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/architecture/debugging.md for why that and
|
|
// not a typed binary format. Commands:
|
|
//
|
|
// help what this kernel can be asked
|
|
// all every view, in registration order
|
|
// <name> [arg] that view, optionally narrowed
|
|
// whose <addr> every subsystem's claim on one page
|
|
int dbg_command(const char *line, struct sbuf *out);
|
|
|
|
// Every view, straight to the console. This is what sysrq 'g' runs, and it is
|
|
// the level-0 path from the plan: no process, no filesystem, no client.
|
|
void dbg_dump_all(void);
|
|
|
|
#define DBG_MAJOR 10
|