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>
252 lines
7.9 KiB
C
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);
|
|
}
|
|
}
|