SchOS/kernel/fs/devfs.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

306 lines
10 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// devtmpfs: /dev, generated rather than stored.
//
// It used to be ~20 mknod() calls in cmd/schinit, which was wrong in two ways
// that are worth separating.
//
// *The cost.* Every node was a real inode on the root filesystem, and SchFS
// commits each mutation as its own transaction group -- a copy-on-write rewrite
// of the path to the root, then an uberblock behind a cache flush. Measured:
// 2600 ms of a first boot spent writing device nodes to a disk, before schinit
// printed its first line. On later boots the nodes already existed and the same
// code took 0 ms, so the cost was invisible unless you built a fresh image --
// which is exactly what anyone working on the system does every time.
//
// *The duplication, which matters more.* Major and minor numbers are the
// kernel's: they are what devsw[] in kernel/dev/cdev.c dispatches on. schinit
// held a second copy of that mapping and nothing checked the two agreed. A new
// character device meant editing a userspace program, and getting a minor wrong
// there produced a node that opened successfully and did nothing.
//
// So the table below sits next to the thing it describes, and /dev is a
// mount rather than a directory somebody populates. `mount("dev", "/dev",
// "devtmpfs", 0, 0)` is the whole of what init does now.
//
// The namespace is all this provides. Reads and writes never reach here --
// fileread/filewrite see T_DEV and dispatch through devsw themselves -- which
// is why there is no read op below that does anything.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "proc.h"
#include "fs.h"
#include "file.h"
#include "errno.h"
// Inode numbering. Two directories and a flat table of nodes, so the number is
// either a directory id or an index into devnodes[] offset past them.
#define DEVFS_ROOT 1ULL
#define DEVFS_INPUT 2ULL /* the /dev/input directory */
#define DEVFS_FILE0 16ULL /* devnodes[0] is inode 16 */
// Which directory a node lives in. Only two exist and a third would want a real
// tree rather than a flag, which is the point at which this file should grow
// one instead of adding DEV_DIR_THIRD.
#define D_ROOT 0
#define D_INPUT 1
struct devnode {
const char *name;
short major;
short minor;
short dir;
};
// The devices this kernel implements, in the order they appear in a listing.
//
// Minor numbers under major 1 follow Linux's mem-major layout where it costs
// nothing (null=2, zero=5, full=7, random=8, urandom=9), which is worth keeping
// because it is one less arbitrary mapping for anyone reading both.
//
// The majors are devsw[] indices and nothing else; see kernel/dev/cdev.c.
static const struct devnode devnodes[] = {
{ "console", 1, 1, D_ROOT },
{ "null", 1, 2, D_ROOT },
{ "keyboard",1, 3, D_ROOT },
{ "zero", 1, 5, D_ROOT },
{ "full", 1, 7, D_ROOT },
{ "random", 1, 8, D_ROOT },
{ "urandom", 1, 9, D_ROOT },
/* /dev/tty is the caller's *controlling* terminal, which is a different
thing from the console and used to be the same thing only because there
was one terminal. Minor 10 resolves through proc->ctty; see
vt_for_minor() in kernel/core/console.c. */
{ "tty", 1, 10, D_ROOT },
{ "tty1", 1, 11, D_ROOT },
{ "tty2", 1, 12, D_ROOT },
{ "tty3", 1, 13, D_ROOT },
{ "tty4", 1, 14, D_ROOT },
{ "tty5", 1, 15, D_ROOT },
{ "tty6", 1, 16, D_ROOT },
{ "stdin", 1, 1, D_ROOT },
{ "stdout", 1, 1, D_ROOT },
{ "stderr", 1, 1, D_ROOT },
{ "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 },
// Raw disks. Created whether or not a drive is present: opening one with no
// disk behind it fails with ENXIO, which is a better answer than a missing
// path -- and with no node at all, `echo x > /dev/hdb` silently creates an
// ordinary file of that name and appears to work.
{ "hda", 8, 0, D_ROOT },
{ "hdb", 8, 1, D_ROOT },
// Raw input events, Linux evdev layout. The minor is the device index, so
// event0 is the keyboard and event1 the mouse.
{ "event0", 7, 0, D_INPUT },
{ "event1", 7, 1, D_INPUT },
};
#define NDEVNODES ((int)(sizeof(devnodes) / sizeof(devnodes[0])))
static int
node_of(uint64 inum)
{
if (inum < DEVFS_FILE0)
return -1;
uint64 i = inum - DEVFS_FILE0;
return i < (uint64)NDEVNODES ? (int)i : -1;
}
static void
devfs_iload(struct inode *ip)
{
int i = node_of(ip->inum);
ip->major = ip->minor = 0;
ip->size = 0;
if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT) {
ip->type = T_DIR;
ip->nlink = 2;
} else if (i >= 0) {
ip->type = T_DEV;
ip->nlink = 1;
ip->major = devnodes[i].major;
ip->minor = devnodes[i].minor;
} else {
ip->type = 0; /* no such node */
ip->nlink = 0;
}
ip->valid = 1;
}
static void devfs_istore(struct inode *ip) { (void)ip; }
static void devfs_ifree(struct inode *ip) { (void)ip; }
static void devfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; }
// Never reached for a device node: fileread and filewrite see T_DEV and go
// through devsw. Present because fs_ops has the slots, and answering -EIO is
// better than a null pointer if something ever does arrive here.
static int devfs_read(struct inode *ip, int u, uint64 d, uint64 o, uint n)
{ (void)ip; (void)u; (void)d; (void)o; (void)n; return -1; }
static int devfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n)
{ (void)ip; (void)u; (void)s; (void)o; (void)n; return -1; }
static int
streq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
static struct inode *
devfs_dirlookup(struct inode *dp, char *name, uint *poff)
{
if (poff)
*poff = 0;
if (streq(name, ".") || (streq(name, "..") && dp->inum == DEVFS_ROOT))
return iget(DEVFS_DEV, dp->inum);
if (streq(name, ".."))
return iget(DEVFS_DEV, DEVFS_ROOT); /* out of /dev/input */
if (dp->inum == DEVFS_ROOT && streq(name, "input"))
return iget(DEVFS_DEV, DEVFS_INPUT);
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
for (int i = 0; i < NDEVNODES; i++)
if (devnodes[i].dir == want && streq(name, devnodes[i].name))
return iget(DEVFS_DEV, DEVFS_FILE0 + (uint64)i);
return 0;
}
// A read-only filesystem, and these say so rather than pretending. mknod on
// /dev returning EROFS is the correct answer: the node either exists because
// the kernel has the device, or it does not exist because it has not.
static int devfs_dirlink(struct inode *dp, char *name, uint64 inum)
{ (void)dp; (void)name; (void)inum; return -EROFS; }
static int devfs_dirunlink(struct inode *dp, char *name, uint off)
{ (void)dp; (void)name; (void)off; return -EROFS; }
static int devfs_dirempty(struct inode *dp) { (void)dp; return 0; }
static struct inode *devfs_ialloc(uint dev, short type)
{ (void)dev; (void)type; return 0; }
struct linux_dirent64 {
uint64 d_ino;
int64 d_off;
uint16 d_reclen;
uchar d_type;
char d_name[];
};
static int
emit(char *out, int cap, int *used, uint64 ino, uchar type, const char *name)
{
int nl = (int)strlen(name);
int rec = (int)((sizeof(struct linux_dirent64) + nl + 1 + 7) & ~7);
if (*used + rec > cap)
return -1; /* caller stops and resumes next call */
struct linux_dirent64 *d = (struct linux_dirent64 *)(out + *used);
d->d_ino = ino;
d->d_off = 0;
d->d_reclen = (uint16)rec;
d->d_type = type;
memmove(d->d_name, name, nl);
memset(d->d_name + nl, 0, rec - (int)sizeof(*d) - nl);
*used += rec;
return 0;
}
// `off` is an ordinal over the entries of this directory: 0 is ".", 1 is "..",
// and the rest follow. An opaque cookie the way SchFS uses one buys nothing
// here, because the table is fixed and small enough to index directly.
static int
devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
{
int used = 0;
uint64 n = *poff;
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT)
return -1;
if (n == 0) {
if (emit(out, cap, &used, dp->inum, 4 /* DT_DIR */, ".") < 0) return -1;
n++;
}
if (n == 1) {
uint64 up = dp->inum == DEVFS_ROOT ? DEVFS_ROOT : DEVFS_ROOT;
if (emit(out, cap, &used, up, 4, "..") < 0) goto out;
n++;
}
// /dev/input is a directory entry of the root, and it is emitted between the
// dots and the nodes so that its ordinal is stable.
if (dp->inum == DEVFS_ROOT && n == 2) {
if (emit(out, cap, &used, DEVFS_INPUT, 4, "input") < 0) goto out;
n++;
}
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
uint64 base = dp->inum == DEVFS_ROOT ? 3 : 2;
for (int i = 0; i < NDEVNODES; i++) {
if (devnodes[i].dir != want)
continue;
if (n > base) { base++; continue; } /* already emitted */
if (emit(out, cap, &used, DEVFS_FILE0 + (uint64)i, 2 /* DT_CHR */,
devnodes[i].name) < 0)
goto out;
n++;
base++;
}
out:
*poff = n;
return used;
}
const struct fs_ops devfs_ops = {
.name = "devtmpfs",
.iload = devfs_iload,
.istore = devfs_istore,
.ifree = devfs_ifree,
.read = devfs_read,
.write = devfs_write,
.truncate = devfs_truncate,
.dirlookup = devfs_dirlookup,
.dirlink = devfs_dirlink,
.dirunlink = devfs_dirunlink,
.dirempty = devfs_dirempty,
.ialloc = devfs_ialloc,
.getdents = devfs_getdents,
};
// Graft it over /dev. Called by init, which has made the directory.
void
devfs_mount(void)
{
struct inode *dp = namei("/dev");
if (dp == 0) {
cprintf("devfs: /dev does not exist; not mounted\n");
return;
}
if (mount_add(dp->dev, dp->inum, DEVFS_DEV, DEVFS_ROOT, "/dev", "dev",
"devtmpfs") < 0)
cprintf("devfs: /dev already has something mounted on it\n");
iput(dp);
}