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
252
cmd/schdbgd/main.c
Normal file
252
cmd/schdbgd/main.c
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// schdbgd -- the debugger, over the network.
|
||||
//
|
||||
// docs/architecture/debugging.md, milestone D5, and the last thing on that plan. The
|
||||
// levels below it are the console key (Ctrl-G), which works before there is a
|
||||
// filesystem, and /dev/dbg, which works once there is a shell. This is the one
|
||||
// for when the machine you are debugging is not the machine you are sitting at:
|
||||
// a target that has locked its display, a board with no keyboard, or a bug that
|
||||
// only happens under load you cannot generate by hand.
|
||||
//
|
||||
// schdbgd & 127.0.0.1:4444
|
||||
// schdbgd -p 9000 a different port, still loopback
|
||||
// schdbgd -a 0.0.0.0 anywhere -- and it will say so, loudly
|
||||
//
|
||||
// Then, from anywhere that can reach it:
|
||||
//
|
||||
// nc 127.0.0.1 4444
|
||||
// dma
|
||||
//
|
||||
// **It is a bridge and nothing else.** A line in becomes a line written to
|
||||
// /dev/dbgro; the answer comes back. There is no protocol here, no framing, no
|
||||
// state, and no parsing of anything the kernel said -- which is what makes `nc`
|
||||
// a complete client and keeps this usable on the day the clever client is one
|
||||
// of the things that is broken.
|
||||
//
|
||||
// --- what it deliberately is not ------------------------------------------
|
||||
//
|
||||
// A kernel debugger plus a TCP listener is a remote root hole. That is true of
|
||||
// every one ever written and it is not a reason not to build one, but it has to
|
||||
// be decided rather than discovered. Three decisions:
|
||||
//
|
||||
// **It does not start by itself.** /etc/schinit/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: a machine
|
||||
// that has not been told to run this does not run it.
|
||||
//
|
||||
// **It binds loopback unless told otherwise**, and binding anywhere else takes
|
||||
// an argument that names the address. Reaching it then means `ssh -L`, which is
|
||||
// somebody's deliberate act rather than a default nobody chose.
|
||||
//
|
||||
// **It opens /dev/dbgro, never /dev/dbg.** The read-only minor refuses any view
|
||||
// that declared itself as changing state -- `watch <addr>` arms a debug
|
||||
// register, and arming one is a fine thing for a debugger to do and not a thing
|
||||
// for a stranger on a socket to do. The refusal is in the kernel, driven by the
|
||||
// view's own registration, so it cannot be got round from here and does not go
|
||||
// stale when a new view is added. See include/dbg.h.
|
||||
//
|
||||
// One connection at a time, because /dev/dbg holds one answer at a time: two
|
||||
// clients would read each other's. A second caller waits rather than
|
||||
// interleaving.
|
||||
|
||||
#include "types.h"
|
||||
#include "user.h"
|
||||
#include "fcntl.h"
|
||||
#include "socket.h"
|
||||
|
||||
#define DEFAULT_PORT 4444
|
||||
|
||||
static void
|
||||
puts_(const char *s)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
write(1, (void *)s, n);
|
||||
}
|
||||
|
||||
static void
|
||||
putn(int v)
|
||||
{
|
||||
char b[12];
|
||||
int i = 0;
|
||||
if (v == 0) { write(1, "0", 1); return; }
|
||||
if (v < 0) { write(1, "-", 1); v = -v; }
|
||||
while (v > 0 && i < 11) { b[i++] = '0' + (v % 10); v /= 10; }
|
||||
while (i > 0) { i--; write(1, &b[i], 1); }
|
||||
}
|
||||
|
||||
static int
|
||||
atoi_(const char *s)
|
||||
{
|
||||
int v = 0;
|
||||
while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0');
|
||||
return v;
|
||||
}
|
||||
|
||||
// "10.0.2.15" -> network byte order, or 0 if it is not four numbers.
|
||||
static unsigned int
|
||||
parse_ip(const char *s, int *ok)
|
||||
{
|
||||
unsigned int b[4] = { 0, 0, 0, 0 };
|
||||
int i;
|
||||
|
||||
*ok = 0;
|
||||
for (i = 0; i < 4; i++) {
|
||||
int any = 0;
|
||||
while (*s >= '0' && *s <= '9') { b[i] = b[i] * 10 + (*s++ - '0'); any = 1; }
|
||||
if (!any || b[i] > 255)
|
||||
return 0;
|
||||
if (i < 3) {
|
||||
if (*s != '.') return 0;
|
||||
s++;
|
||||
}
|
||||
}
|
||||
if (*s != 0)
|
||||
return 0;
|
||||
*ok = 1;
|
||||
return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24); // network order
|
||||
}
|
||||
|
||||
// One question, one answer.
|
||||
//
|
||||
// The descriptor is opened fresh each time and the answer is read from offset
|
||||
// zero, for the reason written at length in kernel/dev/dbg.c: writing advances
|
||||
// the offset, so a program that writes and then reads on one descriptor starts
|
||||
// as many bytes into the answer as its command was long.
|
||||
static int
|
||||
ask(int sock, const char *cmd, int len)
|
||||
{
|
||||
char buf[1024];
|
||||
int fd, n;
|
||||
|
||||
fd = open("/dev/dbgro", O_RDWR);
|
||||
if (fd < 0) {
|
||||
const char *m = "schdbgd: cannot open /dev/dbgro\n";
|
||||
write(sock, (void *)m, 32);
|
||||
return -1;
|
||||
}
|
||||
write(fd, (void *)cmd, len);
|
||||
lseek(fd, 0, 0);
|
||||
for (;;) {
|
||||
n = read(fd, buf, sizeof(buf));
|
||||
if (n <= 0)
|
||||
break;
|
||||
if (write(sock, buf, n) != n) { // the client went away mid-answer
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Serve one client until it disconnects.
|
||||
static void
|
||||
serve(int sock)
|
||||
{
|
||||
char line[256];
|
||||
int n = 0;
|
||||
|
||||
{
|
||||
const char *hello =
|
||||
"SchOS debugger. 'help' for the views, 'quit' to go.\n"
|
||||
"read-only: views that change state are refused here.\n\n";
|
||||
int l = 0; while (hello[l]) l++;
|
||||
write(sock, (void *)hello, l);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
char c;
|
||||
int r = read(sock, &c, 1);
|
||||
|
||||
if (r <= 0)
|
||||
return; // closed, or gone
|
||||
if (c == '\r')
|
||||
continue;
|
||||
if (c != '\n') {
|
||||
// A line longer than the buffer is truncated rather than split.
|
||||
// Splitting would turn one bad command into two, and the second
|
||||
// half of a command is a command.
|
||||
if (n < (int)sizeof(line) - 1)
|
||||
line[n++] = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
line[n] = 0;
|
||||
if (n == 0) { n = 0; continue; }
|
||||
|
||||
if ((line[0] == 'q' && line[1] == 'u') ||
|
||||
(line[0] == 'e' && line[1] == 'x')) {
|
||||
const char *bye = "bye\n";
|
||||
write(sock, (void *)bye, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ask(sock, line, n) < 0)
|
||||
return;
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct sockaddr_in sa;
|
||||
unsigned int addr = 0x0100007f; // 127.0.0.1, network order
|
||||
int port = DEFAULT_PORT, lfd, i, open_to_all = 0;
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (argv[i][0] == '-' && argv[i][1] == 'p' && i + 1 < argc) {
|
||||
port = atoi_(argv[++i]);
|
||||
} else if (argv[i][0] == '-' && argv[i][1] == 'a' && i + 1 < argc) {
|
||||
int ok;
|
||||
unsigned int a = parse_ip(argv[++i], &ok);
|
||||
if (!ok) { puts_("schdbgd: -a wants an address like 0.0.0.0\n"); return 1; }
|
||||
addr = a;
|
||||
open_to_all = (addr != 0x0100007f);
|
||||
} else {
|
||||
puts_("usage: schdbgd [-p port] [-a address]\n"
|
||||
" loopback and port 4444 unless told otherwise.\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (port <= 0 || port > 65535) {
|
||||
puts_("schdbgd: that is not a port\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
lfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (lfd < 0) { puts_("schdbgd: no socket\n"); return 1; }
|
||||
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_port = (unsigned short)(((port & 0xff) << 8) | ((port >> 8) & 0xff));
|
||||
sa.sin_addr = addr;
|
||||
for (i = 0; i < 8; i++) sa.sin_zero[i] = 0;
|
||||
|
||||
if (bind(lfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||
puts_("schdbgd: cannot bind port "); putn(port); puts_("\n");
|
||||
return 1;
|
||||
}
|
||||
if (listen(lfd, 1) < 0) { puts_("schdbgd: cannot listen\n"); return 1; }
|
||||
|
||||
puts_("schdbgd: debugger on port "); putn(port);
|
||||
if (open_to_all) {
|
||||
// Worth a line of its own. Anyone who can reach this port can read
|
||||
// this kernel's memory layout, its process table and its open sockets.
|
||||
puts_("\nschdbgd: *** bound beyond loopback -- anyone who can reach "
|
||||
"this port can read\n"
|
||||
"schdbgd: *** this kernel's internal state. That was asked for "
|
||||
"with -a.\n");
|
||||
} else {
|
||||
puts_(", loopback only\n");
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
int fd = accept(lfd, 0, 0);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
serve(fd);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue