// SPDX-License-Identifier: GPL-2.0-or-later // // 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/core/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 "core/types.h" #include "uapi/user.h" #include "uapi/fcntl.h" #include "net/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); } }