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:
parent
071499a378
commit
bc39b73969
23 changed files with 491 additions and 293 deletions
|
|
@ -204,6 +204,7 @@ fine.
|
|||
| `sysrq g` | `/proc` is mounted |
|
||||
| `/dev/dbg` | `/dev` is mounted |
|
||||
| `schdbg` | there is a shell |
|
||||
| `schdbgd` over TCP | there is a network, and somebody enabled it |
|
||||
|
||||
**Ctrl-G is the one that matters for boot problems**, because it is the only
|
||||
one that does not need a filesystem. It sits beside the older Ctrl-P, which
|
||||
|
|
@ -226,6 +227,78 @@ called from `panic()`.**
|
|||
|
||||
---
|
||||
|
||||
## Over the network
|
||||
|
||||
The fourth way in, and the only one that does not need you to be sitting at the
|
||||
machine: a target that has locked its display, a board with no keyboard, or a
|
||||
bug that only appears under load you cannot produce by hand.
|
||||
|
||||
```
|
||||
# schdbgd &
|
||||
schdbgd: debugger on port 4444, loopback only
|
||||
|
||||
$ nc 127.0.0.1 4444
|
||||
SchOS debugger. 'help' for the views, 'quit' to go.
|
||||
read-only: views that change state are refused here.
|
||||
|
||||
dma
|
||||
arena 0xffff80003da41000-0xffff80003ea41000
|
||||
16384 KB total, 16319 KB free in 1 extent(s), 64 KB out
|
||||
```
|
||||
|
||||
`schdbgd` is **a bridge and nothing else**: a line in becomes a line written to
|
||||
`/dev/dbgro`, and the answer comes back. No framing, no state, no parsing of
|
||||
anything the kernel said — which is what makes `nc` a complete client, and what
|
||||
keeps this usable on the day the clever client is one of the broken things.
|
||||
|
||||
It is in a process rather than in the kernel because there is no reason for it
|
||||
to be in the kernel. A TCP listener inside a kernel debugger is a listener that
|
||||
has to work when the kernel does not, and this one does not pretend to: if the
|
||||
scheduler is wedged, `schdbgd` is not running either, and the answer is Ctrl-G.
|
||||
|
||||
### What a listener had to be decided about
|
||||
|
||||
A kernel debugger plus a TCP socket is a remote root hole. That is true of every
|
||||
one ever written; it is not a reason not to build one, and it is a reason to
|
||||
decide rather than discover. Three decisions, in increasing order of how much
|
||||
they actually protect:
|
||||
|
||||
**It does not start by itself.** `/etc/schinit/schdbgd.job` ships `Disabled`.
|
||||
SchOS has no kernel command line, so the boot flag is the job manifest — the
|
||||
same thing in this system's own idiom. A machine whose owner never asked for a
|
||||
debugger is not running one.
|
||||
|
||||
**It binds loopback**, and binding anywhere else takes `-a` naming the address,
|
||||
and says so in a paragraph rather than a word. Reaching it then means `ssh -L`,
|
||||
which is somebody's deliberate act.
|
||||
|
||||
**It cannot reach a view that changes state, and that is enforced in the
|
||||
kernel.** `/dev/dbgro` is `/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*: there is nothing to latch, nothing to remember between the write
|
||||
and the read, and no way to get from one descriptor to the other.
|
||||
|
||||
Which views are refused comes from the registration — `dbg_register_rw` instead
|
||||
of `dbg_register` — for exactly the reason the registry exists at all. A list of
|
||||
dangerous views in one central file is a list that goes stale, and it goes stale
|
||||
in the direction where a newly added view is quietly treated as safe.
|
||||
|
||||
Today that is one view. `watch <addr>` arms a debug register, which is a
|
||||
reasonable thing for a debugger to do and not a reasonable thing for a stranger
|
||||
on a socket to do:
|
||||
|
||||
```
|
||||
$ nc 127.0.0.1 4444
|
||||
watch 0xffff80003da41000
|
||||
'watch' changes state, and this is /dev/dbgro.
|
||||
use /dev/dbg from the console for that.
|
||||
```
|
||||
|
||||
`watch` with no argument is still answered, because that is the listing and it
|
||||
arms nothing.
|
||||
|
||||
---
|
||||
|
||||
## Who owns this page
|
||||
|
||||
The views each speak for one subsystem, which is what keeps them cheap and
|
||||
|
|
@ -360,10 +433,16 @@ something that reports nonsense.
|
|||
`dbg_thread_start()` — which is the last thing `main()` does before entering
|
||||
the scheduler — nothing answers but the log, and the log is enough because it
|
||||
is read on the *next* boot.
|
||||
- **No network transport.** The plan puts a TCP bridge in a userspace daemon,
|
||||
deliberately outside the kernel; it does not exist, so the debugger is local
|
||||
or serial only.
|
||||
- **No memory writes**, and no way to poke anything. Reading is what finds bugs.
|
||||
- **`schdbgd` has no authentication.** Reaching the port *is* the permission,
|
||||
which is why the port is on loopback and why the daemon is off. Anyone who can
|
||||
open a TCP connection to it can read this kernel's memory layout, its process
|
||||
table and its open sockets. Putting a password on it would be worse than
|
||||
useless — it would make the current arrangement feel safer than it is.
|
||||
- **One client at a time.** `/dev/dbg` holds one answer at a time, so two
|
||||
sessions would read each other's; a second caller waits.
|
||||
- **A disconnected client is noticed only when written to.** A session that
|
||||
goes away between commands holds the daemon until TCP notices.
|
||||
- **`all` truncates at 16 KB** and says so. On a busy machine, ask for one view
|
||||
at a time.
|
||||
- **One conversation at a time.** The answer buffer is global, so two programs
|
||||
|
|
@ -379,5 +458,7 @@ something that reports nonsense.
|
|||
happened in the two hundred events before this" is not a question this can
|
||||
answer.
|
||||
|
||||
What remains — a network transport, kept deliberately outside the kernel — is
|
||||
in [../plan/debugger.md](../plan/debugger.md).
|
||||
Nothing remains on the plan this was built from. What is *not* on it, and is the
|
||||
obvious next thing if this is ever picked up again, is tracing: static
|
||||
tracepoints and an event ring. That is a different tool that happens to share a
|
||||
transport.
|
||||
|
|
|
|||
Loading…
Reference in a new issue