SchOS/cmd/schdbg/main.c
Nexoniarz bc39b73969 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
2026-09-14 15:33:53 +02:00

139 lines
4 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// schdbg -- ask the kernel a question.
//
// docs/architecture/debugging.md, milestone D2. The kernel half is /dev/dbg: write a
// line, read the answer. This is the half that makes that pleasant, and it is
// deliberately almost nothing -- because the protocol is lines of text, and a
// protocol whose client has to be clever is a protocol you cannot use when the
// client will not start.
//
// schdbg the views this kernel publishes
// schdbg dma one of them
// schdbg all all of them
// schdbg -i a prompt, for when you are asking several
//
// Everything here works equally well as:
//
// echo dma > /dev/dbg && cat /dev/dbg
//
// which is the point, and is what you fall back to on a system where this
// program is one of the things that is broken.
#include "types.h"
#include "user.h"
#include "fcntl.h"
static void
puts_(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void *)s, n);
}
// One question, one answer.
//
// The descriptor is opened fresh for each, because the answer is read from
// offset zero and the kernel replaces it on every write. Reusing one open
// descriptor across two questions would read the second answer from wherever
// the first one left off.
static int
ask(const char *cmd)
{
char buf[512];
int fd, n;
fd = open("/dev/dbg", O_RDWR);
if (fd < 0) {
puts_("schdbg: cannot open /dev/dbg\n");
return -1;
}
if (cmd) {
int len = 0;
while (cmd[len]) len++;
if (write(fd, (void *)cmd, len) != len) {
puts_("schdbg: the kernel would not take that command\n");
close(fd);
return -1;
}
// Back to the start. Writing moved this descriptor's offset forward by
// the length of the command, and the answer begins at zero -- so
// without this the reply arrives with its first few characters missing,
// which is subtle enough to look like a formatting bug in the view.
lseek(fd, 0, 0); /* SEEK_SET */
}
while ((n = read(fd, buf, sizeof(buf))) > 0)
write(1, buf, n);
close(fd);
return 0;
}
// A line from the terminal, without the newline. Returns 0 at end of input.
static int
readline(char *buf, int cap)
{
int n = 0;
char c;
for (;;) {
int r = read(0, &c, 1);
if (r <= 0)
return n > 0;
if (c == '\n') {
buf[n] = 0;
return 1;
}
if (n < cap - 1)
buf[n++] = c;
}
}
int
main(int argc, char *argv[])
{
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'i') {
char line[128];
puts_("schdbg: ask the kernel something. 'help' lists the views, "
"'quit' leaves.\n");
for (;;) {
puts_("dbg> ");
if (!readline(line, sizeof(line)))
break;
if (line[0] == 0)
continue;
if (line[0] == 'q' && line[1] == 'u' && line[2] == 'i' &&
line[3] == 't' && line[4] == 0)
break;
ask(line);
}
return 0;
}
// No argument means "what can I ask", which is the question someone has
// the first time and the one an empty response would not answer.
if (argc < 2)
return ask("help") < 0 ? 1 : 0;
// Everything after the program name, rejoined with single spaces. The
// shell split it and the kernel wants a line; `schdbg vma 4` has to arrive
// as "vma 4" or the argument is silently dropped and the view quietly
// ignores the narrowing it was asked for.
{
char line[128];
int n = 0;
for (int i = 1; i < argc; i++) {
if (i > 1 && n < (int)sizeof(line) - 1)
line[n++] = ' ';
for (int j = 0; argv[i][j] && n < (int)sizeof(line) - 1; j++)
line[n++] = argv[i][j];
}
line[n] = 0;
return ask(line) < 0 ? 1 : 0;
}
}