From bc39b73969b8f792265f90691db64ff7edaef013 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Mon, 14 Sep 2026 15:33:53 +0200 Subject: [PATCH] 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 `; `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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- Makefile | 4 +- cmd/schdbg/main.c | 2 +- cmd/schdbgd/main.c | 252 +++++++++++++++++++++++++++++++++ docs/architecture/debugging.md | 91 +++++++++++- docs/plan/1.0.md | 19 +-- docs/plan/README.md | 9 +- docs/plan/debugger.md | 243 ------------------------------- etc/schinit/schdbgd.job | 30 ++++ include/dbg.h | 29 +++- include/defs.h | 4 +- include/klog.h | 2 +- include/sbuf.h | 2 +- include/socket.h | 1 + kernel/arch/x86_64/trap.c | 2 +- kernel/arch/x86_64/watch.c | 4 +- kernel/core/ksym.c | 2 +- kernel/dev/cdev.c | 2 +- kernel/dev/dbg.c | 56 ++++++-- kernel/dev/procfs.c | 4 +- kernel/fs/devfs.c | 6 + kernel/proc/proc.c | 2 +- kernel/sys/sys_net.c | 14 +- tools/ksyms.pl | 4 +- 23 files changed, 491 insertions(+), 293 deletions(-) create mode 100644 cmd/schdbgd/main.c delete mode 100644 docs/plan/debugger.md create mode 100644 etc/schinit/schdbgd.job diff --git a/Makefile b/Makefile index 42efbe3..641e549 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schmode netecho ldlsd +CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schdbgd schmode netecho ldlsd CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- @@ -623,7 +623,7 @@ qemu-iso: schos.iso # # Pass one exists to be read: tools/ksyms.pl turns its symbol table into a C # file, which pass two compiles in, so the kernel can name its own addresses -# (docs/plan/debugger.md, D0). kernel.ld places .ksym after .text and .rodata +# (docs/architecture/debugging.md, D0). kernel.ld places .ksym after .text and .rodata # precisely so that carrying the table cannot move anything the table records # -- which is what makes two passes sufficient instead of the three Linux needs. # diff --git a/cmd/schdbg/main.c b/cmd/schdbg/main.c index 7c1d116..4cd205f 100644 --- a/cmd/schdbg/main.c +++ b/cmd/schdbg/main.c @@ -2,7 +2,7 @@ // // schdbg -- ask the kernel a question. // -// docs/plan/debugger.md, milestone D2. The kernel half is /dev/dbg: write a +// 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 diff --git a/cmd/schdbgd/main.c b/cmd/schdbgd/main.c new file mode 100644 index 0000000..47aeca3 --- /dev/null +++ b/cmd/schdbgd/main.c @@ -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 ` 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); + } +} diff --git a/docs/architecture/debugging.md b/docs/architecture/debugging.md index dd6dfd0..407a148 100644 --- a/docs/architecture/debugging.md +++ b/docs/architecture/debugging.md @@ -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 ` 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. diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index 2394560..4cd07c7 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -95,9 +95,9 @@ The galling ones. The work exists, is tested, and nothing uses it. |---|---| | **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** | | ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". | -| ~~**`ldls.md`**~~ | **Closed.** L0 through L4 built: the loader handles 2,440 stock modules, `8139too` runs in a SchOS process driving QEMU's rtl8139 through `/dev/pci`, `ping` works over it, and killing the host releases the device and DMA so a replacement takes over. Described in [../architecture/ldls.md](../architecture/ldls.md). One driver, one kernel version, tried once. | +| ~~**`ldls.md`**~~ | **Closed.** L0 through L4 built: the loader handles 2,440 stock modules, and both `8139too` and Linux's `e1000` run in a SchOS process through `/dev/pci` with `ping` working over each. The e1000 run is a handover — the kernel was driving that NIC and gave it up. Described in [../architecture/ldls.md](../architecture/ldls.md). Two drivers, one kernel version, one machine. | | **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. | -| **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. | +| ~~**`debugger.md`**~~ | **Closed.** D0 through D5 built, including the network transport: `schdbgd` bridges TCP to `/dev/dbgro`, ships disabled, binds loopback, and cannot reach a view that changes state. Described in [../architecture/debugging.md](../architecture/debugging.md). | **The row LDLS spent, and gave back.** `hybrid.md` closed — H3 built, H4 withdrawn — and `ldls.md` took the slot it freed. It has since closed too, so @@ -107,13 +107,16 @@ part of 1.0**: nothing in the acceptance test needs a foreign driver. It was taken on because the mechanisms it uses were finished and idle, and because the alternative to it is writing a driver for every device that exists. -**The row the debugger spent.** Adding one requires closing one, and *A released -PCI device* closed when H2 landed. It is worth saying plainly that the debugger -is not a capability of the OS the way every other row here is: nothing in it -ships to a user or appears in the acceptance test. The case for it is that -**Gate 0 is a list of bugs to find in a kernel that could not be asked +**The row the debugger spent, and gave back.** Adding one requires closing one, +and *A released PCI device* closed when H2 landed. It is worth saying plainly +that the debugger is not a capability of the OS the way every other row here is: +nothing in it ships to a user or appears in the acceptance test. The case for it +is that **Gate 0 is a list of bugs to find in a kernel that could not be asked anything** — so it is the instrument the rest of this register is surveyed with, -and it earns its slot by making every other row cheaper to close. +and it earned its slot by making every other row cheaper to close. + +It has closed. **Every plan in `docs/plan/` that was open is now closed**, which +means the next thing is Gate 0 itself: the survey, with the instrument built. **Closed since this register was written.** *A released PCI device* — release is now symmetric with claim (bus mastering cleared before the DMA is freed, a diff --git a/docs/plan/README.md b/docs/plan/README.md index e7e8c65..f82105d 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -43,13 +43,8 @@ And the one the hybrid mechanisms were built for: | | | |---|---| -| ~~ldls.md~~ | **Built.** The Linux Driver Layer for SchOS: someone else's drivers, in a process, reached through `/dev/pci`. `ping` works over `8139too`. The description moved to [../architecture/ldls.md](../architecture/ldls.md). | - -And one that is a tool rather than a capability: - -| | | -|---|---| -| **[debugger.md](debugger.md)** | A debugger for this kernel — one that observes rather than halts, and answers in this kernel's own vocabulary. **Built**, except the network transport: symbols, a log that outlives the kernel, `/dev/dbg`, the `whose` ownership query and hardware watchpoints are all described in [../architecture/debugging.md](../architecture/debugging.md). | +| ~~ldls.md~~ | **Built.** The Linux Driver Layer for SchOS: someone else's drivers, in a process, reached through `/dev/pci`. `ping` works over `8139too` and over `e1000`. The description moved to [../architecture/ldls.md](../architecture/ldls.md). | +| ~~debugger.md~~ | **Built**, all of it, including the network transport. A debugger that observes rather than halts and answers in this kernel's own vocabulary: symbols, a log that outlives the kernel, `/dev/dbg`, `whose`, hardware watchpoints, and `schdbgd`. The description moved to [../architecture/debugging.md](../architecture/debugging.md). | Two longer-horizon documents sit behind these: [schapi.md](schapi.md), the API layering and the compatibility promise, and [roadmap.md](roadmap.md), which holds diff --git a/docs/plan/debugger.md b/docs/plan/debugger.md deleted file mode 100644 index 1f9b3b9..0000000 --- a/docs/plan/debugger.md +++ /dev/null @@ -1,243 +0,0 @@ -# A debugger for this kernel - -> **Every milestone on this plan is built.** Symbols, a log that outlives the -> kernel, the introspection registry behind `/dev/dbg`, the `whose` ownership -> query and hardware watchpoints are all described in -> [../architecture/debugging.md](../architecture/debugging.md). -> -> What is left here is **level 2, the network transport** — a userspace daemon -> bridging TCP to `/dev/dbg`, deliberately outside the kernel. Nothing else. - -Every bug in this project so far has been found the same way: add a `cprintf`, -rebuild, boot, read the serial log, guess again. That loop is about ninety -seconds long and it answers exactly one question per turn — the question you -thought to ask before you rebuilt. - -The DMA ownership bug in `do_dma()` is what made the case. Finding it took -several rounds of that loop, and the thing that eventually found it was not a -print at all; it was reading `freevm()` and noticing what `PHYSTOP` meant for -the arena. The kernel could not be *asked* whether two allocators owned the same -page, because nothing in it can be asked anything. - -> **The kernel has no way to answer a question it was not already compiled to -> answer.** That is the gap. - ---- - -## What there was, and what was missing - -The state this plan was written against. D0 to D2 have since closed the first -three rows; they are kept because they are the argument. - -| | | -|---|---| -| `cprintf` | goes to UART and console and is then gone. **There is no log.** `panic.c` says so itself: *"there is no core dump and no log to go back to."* | -| PSOGM | collects a great deal at the moment of a panic — CPU, process, call chain, load address — and paints it. It is the only artefact of a crash, and it does not survive being looked away from. | -| `getcallerpcs()` | walks frames and prints **raw addresses**. Turning one into a function name is a manual `objdump` session. | -| `/proc/sysrq` | a hardcoded `switch` with one letter per action. `xhci_dump`, `usb_storage_dump`, `display_selftest` — the right instinct, in the wrong shape: they are self-tests, not state, and adding one means editing a central file. | -| DR0–DR3 | **unused.** `T_DEBUG` maps to `SIGTRAP` and nothing ever arms a hardware watchpoint. | - ---- - -## The two rules - -### It observes; it does not control - -No stepping. No software breakpoints. No stopping the world. - -This is the decision everything else follows from, and it is not a scope -compromise. Half the bugs this project actually hits are *timing*: an interrupt -storm at 77,000 a second, a level-triggered line nobody acknowledges, a driver -that faults only on the second start, a supervisor that restarts a job fifteen -times in forty-five seconds. **Every one of those disappears when you stop the -world to look at it.** A debugger that can only work by halting the machine -cannot see the class of bug this kernel produces. - -So it answers questions, and it watches. Both of those work on a machine that is -still running. - -### It must not depend on what it is debugging - -The same circularity rule the display already has, pointed at a different -subsystem: - -> A debugger that needs lwIP cannot debug lwIP. A debugger that needs a process -> cannot debug the scheduler. A debugger that needs to allocate cannot debug the -> allocator. - -That rule decides the layering below rather than taste deciding it, which is the -only reason to write a rule down. - ---- - -## Why not GDB - -The licence is not the reason — a stub written here from the published protocol -would be our own Apache-2.0 code, the same clean-room argument as -[../architecture/devices.md](../architecture/devices.md) makes for Linux -drivers. Two technical reasons, and they are both about what a *generic* -debugger structurally cannot do. - -**It only knows registers and memory.** GDB can tell you that `0x3eceb000` -contains zeroes. It cannot tell you that the page is in the DMA arena's free -list *and* on `kalloc`'s free list *and* mapped into pid 7 — because those are -this kernel's abstractions, and a debugger that knows about them has to be -written against them. That query is the one that would have ended the session -described above in one line. - -**It stops the world**, per the rule above. - -There is a third, smaller reason: a `gdbstub` is a protocol you implement and -then debug, and its failure mode is an unresponsive stub with no way to ask it -why. The thing below can always be driven by hand over a serial line. - ---- - -## The layers - -Three transports, and the rule decides which gets what. **The kernel's half is -the minimum that works when nothing else does; everything convenient lives in a -process** — which is the same sentence as [hybrid.md](hybrid.md)'s, about a -different thing. - -| | Works when | | -|---|---|---| -| **0. Console and UART** | always, including panic context, with interrupts off, no allocation, no scheduler, no lock taken | `sysrq`-driven. If the machine is dying this is the one you have. | -| **1. `/dev/dbg`** | there is a scheduler and a process | A character device. The local client is an ordinary program — exactly the `/dev/pci` discipline, for exactly the same reason. | -| **2. The network** | there is a network | A **userspace daemon** bridging TCP to `/dev/dbg`. Not in the kernel. lwIP must stay out of the debugger's path, or the debugger cannot be pointed at lwIP. | - -Level 2 is deliberately last, and it is not in the first cut. - ---- - -## The protocol is lines of text - -A request is a line. A response is lines, ending in a blank one. - -``` -> whose 0x3eceb000 -phys 0x3eceb000 - dma arena extent 2, FREE (4096000 bytes @ 0x3eceb000) - kalloc free list, position 41 - pid 7 vgpud va 0x4000d000 rw-u vma[3] no_free - *** three owners -- this page is claimed twice - -> view dma -arena 0x3e000000-0x3f000000 16384 KB, 12284 KB free - free[0] 0x3e0a4000 8192 KB - free[1] 0x3ecec000 4092 KB - in use 4096 KB in 3 blocks -``` - -Text, and not a typed binary protocol, for three reasons that all turn out to be -the same reason: - -- **It works at level 0.** Panic context cannot allocate, cannot frame records, - cannot wait for a client to acknowledge anything. It can put characters on a - serial line. A binary protocol needs a text fallback anyway, and then there are - two protocols. -- **It is usable before a client exists.** `cat`, `echo`, a serial terminal, the - guest's own shell. A protocol that needs its client built first is a protocol - nobody can debug. -- **It survives the thing it is debugging.** When the answer arrives garbled, - you can still read most of it. - -A richer client can be layered on later by parsing this, and if that ever -happens the kernel does not change. - ---- - -## Milestones - -| | | | -|---|---|---| -| **D0** | **Symbols.** A compact sorted table emitted from `nm` at link time and linked into the image; `ksym_lookup(addr, &off)`. | **done** | -| **D1** | **A log that survives.** A ring behind `cprintf`, timestamped, in a region the allocator never sees — so a panic's last words are readable on the next boot. `/proc/kmsg`. | **done** | -| **D2** | **The introspection registry.** `dbg_register("dma", dma_dump)`. Seven views, `/dev/dbg`, `schdbg`, and `sysrq g`. | **done** | -| **D3** | **`whose `** — the cross-subsystem ownership query above. | **done** | -| **D4** | **Watchpoints**, DR0–DR3, break-on-write, reporting rip and call chain and **continuing** rather than stopping. | **done** | -| **D5** | **The network transport**, level 2 above: a userspace daemon bridging TCP to `/dev/dbg`. Off unless a boot flag turns it on, loopback by default. | | - -### Why D0 and D1 come before the interesting parts - -Neither is clever. Both multiply everything after them: a view that prints an -address is worth less than one that prints a name, and a query you can only run -*after* you have noticed something is worth less than a log that was already -running when it happened. - -D1 in particular changes the job rather than improving it. Today a triple fault -leaves nothing at all — the screen is gone, the serial buffer is gone, and the -only evidence is whatever you happened to be watching. A log that outlives the -boot turns that into a file. - -### What D1 turned out to cost - -One measurement changed the design. The log was first placed at the top of RAM -beside the DMA arena, which is the obvious spot for a region nothing should -allocate — and on the first reboot its magic came back as ASCII, because **the -top of usable memory is exactly where the firmware makes its own allocations** -before handing the machine over. It sits above the kernel image instead, which -is both a stable address across boots and memory the loader has already -committed to. Recorded because the reasoning that picked the wrong place was -sound, and only the test was not. - -### What D2 is really for - -Not the views themselves — the *registry*. A subsystem publishes its state with -one line at the bottom of the file that owns the data, and nothing central has to -be edited. That is the only version of this that stays current; the `sysrq` -switch is the version that does not, and it is already three self-tests and no -state. - ---- - -## The security note, for the parts not yet built - -A debugger that reads arbitrary physical memory and a TCP listener are, together, -a remote root hole. That is true of every kernel debugger ever written and it is -not a reason not to build one, but it has to be decided rather than discovered: - -- Level 2 is **off unless a boot flag turns it on**, and binds loopback by - default. -- No memory *writes* in the first cut. Reading is what finds bugs; writing is - what a later milestone can argue for on its own. - ---- - -## What it will still not do - -- **It will not step, break, or halt**, per the rule. If a bug genuinely needs a - stepper, this is the wrong tool and that is a deliberate trade. -- **No stack unwinding through interrupt frames.** `getcallerpcs()` walks `rbp` - chains and stops where the frame pointer stops; a trap frame in the middle of - a chain ends the walk. -- **No userspace symbolization.** The kernel will name kernel symbols. A fault - in a process reports the binary and the offset, and resolving that stays a - host-side job. -- **No profiling.** No sampling, no counters, no flame anything. A timer-driven - sampler is a different tool that happens to share a transport. -- **Watchpoints are limited to four**, by the hardware, and to 1/2/4/8 bytes. - There is no software watchpoint fallback and there should not be one. -- **It is not a tracer.** Static tracepoints and an event ring are the obvious - next thing after D4 and are not on this plan. - ---- - -## On the freeze - -[1.0.md](1.0.md) says no new capability starts until the register is empty, and -the register is not empty. Two things about that. - -The bookkeeping answer is that the rule is *"adding a row requires closing one"*, -and **A released PCI device** closed when H2 landed. There is a slot. - -The better answer is that the register is a list of **capabilities of the OS**, -and this is not one — nothing in it ships to a user or appears in the acceptance -test. It is a tool, and the case for spending time on a tool is that it makes -everything still on the register cheaper to finish. Gate 0 of [1.0.md](1.0.md) -is *"fix what is already wrong"*, assembled from every *What it does not do* -section in [../architecture/](../architecture/). That gate is a list of bugs to -find in a kernel that cannot currently be asked anything. - -**Build the instrument before the survey**, and then the freeze holds for the -rest of it. diff --git a/etc/schinit/schdbgd.job b/etc/schinit/schdbgd.job new file mode 100644 index 0000000..1e4c6dd --- /dev/null +++ b/etc/schinit/schdbgd.job @@ -0,0 +1,30 @@ +# The debugger, over the network. Off. +# +# docs/architecture/debugging.md milestone D5, and the `Disabled` line below is the +# milestone's own security note being kept. SchOS has no kernel command line, so +# the boot flag the plan asked for is this manifest: a machine that has not been +# told to run this does not run it, and telling it is one line in one file that +# somebody edited on purpose. +# +# schinitd enable os.schos.schdbgd (or: set Disabled = no here) +# +# Off rather than loopback-only-and-on, even though loopback-only is already the +# default bind, because "a listener nobody asked for" is a different kind of +# thing from "a listener bound somewhere narrow". A debugger that can read the +# process table should not be running on a machine whose owner never wanted one. +# +# What it can reach is bounded in the kernel rather than here: schdbgd opens +# /dev/dbgro, which refuses any view that declared itself as changing state. +# See include/dbg.h. + +Label = os.schos.schdbgd +Program = /bin/schdbgd +Args = schdbgd +Disabled = yes +RunAtLoad = yes +KeepAlive = yes + +ThrottleInterval = 3 + +StandardOut = /dev/tty1 +StandardError = /dev/tty1 diff --git a/include/dbg.h b/include/dbg.h index bfcf767..f293df0 100644 --- a/include/dbg.h +++ b/include/dbg.h @@ -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 ----------------------------------------------------------- // // 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 diff --git a/include/defs.h b/include/defs.h index 3b99db9..c162e55 100644 --- a/include/defs.h +++ b/include/defs.h @@ -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); diff --git a/include/klog.h b/include/klog.h index 52f1c1b..a4c8eb4 100644 --- a/include/klog.h +++ b/include/klog.h @@ -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 diff --git a/include/sbuf.h b/include/sbuf.h index fe6d2c9..19229eb 100644 --- a/include/sbuf.h +++ b/include/sbuf.h @@ -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 diff --git a/include/socket.h b/include/socket.h index 3de95f7..3b2e6e2 100644 --- a/include/socket.h +++ b/include/socket.h @@ -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 diff --git a/kernel/arch/x86_64/trap.c b/kernel/arch/x86_64/trap.c index 0b2f91f..cfb26cb 100644 --- a/kernel/arch/x86_64/trap.c +++ b/kernel/arch/x86_64/trap.c @@ -130,7 +130,7 @@ trap(struct trapframe *tf) if(myproc() == 0 || (tf->cs&3) == 0){ // The rip is named where it can be. This is a kernel address, so // ksym_lookup() has something to say about it -- and the whole reason - // docs/plan/debugger.md puts D0 first is that "rip ffff8000001294c2" + // docs/architecture/debugging.md puts D0 first is that "rip ffff8000001294c2" // costs an objdump session and "rip in dma_alloc+0x8c" does not. char where[72]; ksym_fmt(where, sizeof(where), tf->rip); diff --git a/kernel/arch/x86_64/watch.c b/kernel/arch/x86_64/watch.c index 54fabd4..99b204a 100644 --- a/kernel/arch/x86_64/watch.c +++ b/kernel/arch/x86_64/watch.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // -// Hardware watchpoints. docs/plan/debugger.md milestone D4. +// Hardware watchpoints. docs/architecture/debugging.md milestone D4. // // The question this answers is the one no amount of printing gets at: // *something wrote to this address -- who?* Adding a print to every writer @@ -411,6 +411,6 @@ watch_view(struct sbuf *out, const char *arg) void watch_init(void) { - dbg_register("watch", watch_view, + dbg_register_rw("watch", watch_view, "hardware watchpoints -- `watch ` to arm one"); } diff --git a/kernel/core/ksym.c b/kernel/core/ksym.c index 0b97522..b6dcc90 100644 --- a/kernel/core/ksym.c +++ b/kernel/core/ksym.c @@ -2,7 +2,7 @@ // // Naming an address. // -// docs/plan/debugger.md milestone D0. It is the least clever thing on that +// docs/architecture/debugging.md milestone D0. It is the least clever thing on that // plan and the one everything else is worth more with: a call chain of six // hex numbers has to be resolved by hand against a disassembly that may not // be the build that was running, and the resolving is done at exactly the diff --git a/kernel/dev/cdev.c b/kernel/dev/cdev.c index d6fbbd4..9108ba8 100644 --- a/kernel/dev/cdev.c +++ b/kernel/dev/cdev.c @@ -119,7 +119,7 @@ struct devsw devsw[] = { // everything else through ioctl; see include/pciuser.h. [9] = { pciuser_read, pciuser_write }, // /dev/dbg (major 10) -- ask the kernel a question. Write a line, read the - // answer; see include/dbg.h and docs/plan/debugger.md. + // answer; see include/dbg.h and docs/architecture/debugging.md. [10] = { dbg_read, dbg_write }, // /dev/netdev (major 11) -- a process, being a network interface. The // reverse of major 9: that hands a device to a process, this lets a process diff --git a/kernel/dev/dbg.c b/kernel/dev/dbg.c index 95bda6c..8274b51 100644 --- a/kernel/dev/dbg.c +++ b/kernel/dev/dbg.c @@ -8,7 +8,7 @@ // // That shape is chosen so the debugger is usable with the tools already on the // machine, before `schdbg` is built and on a system where it will not start. -// docs/plan/debugger.md argues the case at length; the short version is that a +// docs/architecture/debugging.md argues the case at length; the short version is that a // protocol you cannot drive by hand is a protocol you cannot debug. // // One conversation at a time, deliberately. The answer buffer is global and @@ -37,6 +37,7 @@ struct view { const char *name; dbg_view_fn fn; const char *what; + int rw; // changes something; see dbg_register_rw }; static struct view views[NVIEW]; @@ -60,8 +61,8 @@ static int dbglen; static struct spinlock dbglock; static int dbginit; -void -dbg_register(const char *name, dbg_view_fn fn, const char *what) +static void +dbg_register_at(const char *name, dbg_view_fn fn, const char *what, int rw) { if (nview >= NVIEW || !name || !fn) return; @@ -74,9 +75,22 @@ dbg_register(const char *name, dbg_view_fn fn, const char *what) views[nview].name = name; views[nview].fn = fn; views[nview].what = what ? what : ""; + views[nview].rw = rw; nview++; } +void +dbg_register(const char *name, dbg_view_fn fn, const char *what) +{ + dbg_register_at(name, fn, what, 0); +} + +void +dbg_register_rw(const char *name, dbg_view_fn fn, const char *what) +{ + dbg_register_at(name, fn, what, 1); +} + void dbg_register_whose(const char *name, dbg_whose_fn fn) { @@ -105,6 +119,8 @@ do_help(struct sbuf *out) sb_puts(out, " "); sb_pad(out, views[i].name, 10); sb_puts(out, views[i].what); + if (views[i].rw) + sb_puts(out, " [changes state]"); sb_putc(out, '\n'); } sb_puts(out, "\n whose every subsystem's claim on one page\n"); @@ -257,6 +273,18 @@ do_all(struct sbuf *out) int dbg_command(const char *line, struct sbuf *out) +{ + return dbg_command_ro(line, out, 0); +} + +// `ro` refuses any view that declared itself as changing state. +// +// The refusal is driven by the registration rather than by a list here, which +// is the whole reason dbg_register_rw() exists: a list of dangerous views in +// this file would go stale in the direction where a new one is treated as +// safe. +int +dbg_command_ro(const char *line, struct sbuf *out, int ro) { char word[32], arg[64]; int n = 0; @@ -291,6 +319,16 @@ dbg_command(const char *line, struct sbuf *out) for (int i = 0; i < nview; i++) { if (streq(word, views[i].name)) { + // A view that changes state, asked for through /dev/dbgro. Reading it + // with no argument is still allowed, because that is the listing -- + // `watch` on its own prints what is armed and arms nothing. + if (ro && views[i].rw && arg[0] != 0) { + sb_puts(out, "'"); + sb_puts(out, word); + sb_puts(out, "' changes state, and this is /dev/dbgro.\n" + "use /dev/dbg from the console for that.\n"); + return out->len; + } views[i].fn(out, arg); return out->len; } @@ -309,13 +347,13 @@ dbg_command(const char *line, struct sbuf *out) // with no explanation reads as a kernel that is broken rather than as a buffer // that is full. The last 64 bytes are held back to have somewhere to say it. static int -fill(const char *cmd) +fill(const char *cmd, int ro) { struct sbuf sb; sb.p = dbgbuf; sb.len = 0; sb.cap = DBGBUF - 64; if (cmd) - dbg_command(cmd, &sb); + dbg_command_ro(cmd, &sb, ro); else do_all(&sb); @@ -332,7 +370,7 @@ fill(const char *cmd) // // Ctrl-G runs the debugger with no filesystem, no shell and no client program, // which is what makes it available *during boot* rather than once userspace is -// up. docs/plan/debugger.md calls this level 0, and it is the level that +// up. docs/architecture/debugging.md calls this level 0, and it is the level that // matters when the thing you are debugging is the boot. // // It cannot do the work in the interrupt handler, though, and the reason is @@ -384,7 +422,7 @@ dbg_dump_all(void) if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; } acquire(&dbglock); - dbglen = fill(0); + dbglen = fill(0, 0); // Printed inside the lock so a second sysrq cannot interleave with this one. // cprintf takes cons.lock, which is never held by anything that wants // dbglock, so the order is fixed and there is no cycle. @@ -408,7 +446,7 @@ dbg_write(struct inode *ip, uint64 off, char *src, int n) char line[128]; int i; - (void)ip; (void)off; + (void)off; if (n <= 0) return 0; @@ -421,7 +459,7 @@ dbg_write(struct inode *ip, uint64 off, char *src, int n) if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; } acquire(&dbglock); - dbglen = fill(line); + dbglen = fill(line, ip && ip->minor == 1); release(&dbglock); return n; // the whole line was consumed either way diff --git a/kernel/dev/procfs.c b/kernel/dev/procfs.c index dc7e318..1b8ba1f 100644 --- a/kernel/dev/procfs.c +++ b/kernel/dev/procfs.c @@ -42,7 +42,7 @@ // The formatting helpers these files are built out of now live in // include/sbuf.h, because the debugger's introspection registry needs exactly -// the same thing and two copies would have drifted. See docs/plan/debugger.md. +// the same thing and two copies would have drifted. See docs/architecture/debugging.md. // --- the files ------------------------------------------------------------- @@ -451,7 +451,7 @@ proc_sysrq_cmd(char cmd) case 'g': // Every registered view, straight to the console. // - // This is docs/plan/debugger.md's level 0: no process, no filesystem, no + // This is docs/architecture/debugging.md's level 0: no process, no filesystem, no // client program. If /dev/dbg cannot be opened -- because the filesystem // is what is broken -- this still works, and on a machine with only a // serial line it is the whole debugger. diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index dfc2267..dca42e6 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -89,6 +89,12 @@ static const struct devnode devnodes[] = { { "fb0", 2, 0, D_ROOT }, { "pci", 9, 0, D_ROOT }, { "dbg", 10, 0, D_ROOT }, + // The same device, answering only the views that read. Minor 1 rather than + // a flag, because the minor is the one piece of per-open state a character + // device already sees -- so which one you opened *is* the permission, and + // nothing has to be remembered between the write and the read. See + // include/dbg.h and cmd/schdbgd. + { "dbgro", 10, 1, D_ROOT }, { "netdev", 11, 0, D_ROOT }, { "mouse", 3, 1, D_ROOT }, diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index e24d4a0..8b8e509 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -47,7 +47,7 @@ kstack_check_canary(const char *where) // --- what the debugger can be told about processes -------------------------- // -// docs/plan/debugger.md, D2. These are here rather than in kernel/dev/dbg.c +// docs/architecture/debugging.md, D2. These are here rather than in kernel/dev/dbg.c // because this file owns ptable, and a view written anywhere else would need // the lock and the struct exported to get at it. One line at the bottom of the // file that owns the data is the whole discipline. diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index 0138655..5b0772b 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -156,9 +156,19 @@ SYSCALL_DEFINE(socket) // letting an unprivileged process do the one thing raw sockets were used // for. Anything else is still refused rather than quietly given a socket // that speaks a different protocol than the caller asked for. + // + // IPPROTO_TCP with SOCK_STREAM and IPPROTO_UDP with SOCK_DGRAM name the + // default rather than departing from it, and programs do write them out: + // toybox's `nc` passes IPPROTO_TCP, and refusing it meant "Protocol not + // supported" for TCP on a system whose whole network is TCP. if (protocol != 0) { - if (!(domain == AF_INET && type == SOCK_DGRAM && - protocol == IPPROTO_ICMP)) + int redundant = + (domain == AF_INET && type == SOCK_STREAM && protocol == IPPROTO_TCP) || + (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP); + if (redundant) + protocol = 0; + else if (!(domain == AF_INET && type == SOCK_DGRAM && + protocol == IPPROTO_ICMP)) return -EPROTONOSUPPORT; } diff --git a/tools/ksyms.pl b/tools/ksyms.pl index c68978f..bb56b55 100644 --- a/tools/ksyms.pl +++ b/tools/ksyms.pl @@ -3,7 +3,7 @@ # # Turn `nm -n kernel` into a symbol table the kernel can carry with it. # -# docs/plan/debugger.md milestone D0. Everything the debugger prints is an +# docs/architecture/debugging.md milestone D0. Everything the debugger prints is an # address until this exists, and an address has to be resolved by hand against # a disassembly that may not be the one that was running. # @@ -104,7 +104,7 @@ my $n = scalar @sym; print <<"HDR"; // Generated by tools/ksyms.pl from the first link pass. Do not edit. // -// See docs/plan/debugger.md, milestone D0, and kernel/core/ksym.c for the +// See docs/architecture/debugging.md, milestone D0, and kernel/core/ksym.c for the // lookup that reads this. Everything lands in .ksym, which kernel/kernel.ld // places after .text and .rodata so that carrying the table cannot move the // addresses recorded in it.