SchOS/cmd/schdbgd/main.c
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

252 lines
7.9 KiB
C

// 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 <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);
}
}