README: rewritten around what runs (Linux software, Xorg, apk, Linux's drivers), the honest gaps, measured numbers.
plan/: 1.0.md is the finish line, where each clause of the acceptance test stands, and the register (one failing
clause: a root that survives a reboot); built plans (foundations, hybrid, alpine, xorg) and the decided-against one
(wayland) are gone, their content lives in architecture/; roadmap.md is only what is still open.
guides: building/layout/testing/adding-a-driver (Linux code may be used). history: four entries (ls -lR, TLB
shootdowns, MDIO 0x02460246, netlink MSG_TRUNC). Website: cards, stack, download page, docs index.
Code comments that pointed at deleted plan docs point at the architecture docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".
Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
Gate 1, item 3, and the last of foundations.md.
bind() creates a T_SOCK node at the path; connect() resolves the path and looks
for the socket bound to **that inode**. Matching on the inode rather than on
the path *string* is the part that matters: the name used to live only in the
socket table, so two spellings of one path missed each other, a relative path
never matched an absolute one, and there was nothing to stat() or unlink().
An inode is the thing a path resolves *to*, so connect() can walk the path
however it likes and arrive at the same socket.
# ls -l /tmp
srwxr-xr-x 1 root root 0 2026-09-22 08:04 socktest
The node is a marker, not the binding. It holds no data and the live
association stays in the socket table, so a socket file left by a crash is
what Linux leaves behind -- a dead file that connect() refuses until something
binds again -- and unlink()ing a live socket's path does not disturb anyone
already connected.
EADDRINUSE comes from the *directory* rather than from a scan of the table,
deliberately: a stale socket file has to refuse a rebind for the same reason a
running server's does, or two servers both believe they own it. The creation
lives in sys_bind() and not in unix.c, because that is the layer with both --
kernel/net/unix.c knows about sockets and has no business calling create().
T_SOCK is a compat addition to SchFS by the rule in schfs.h: a driver that does
not know SCHFS_T_SOCK maps it to a zero-length regular file, so nothing goes
stale and nothing becomes unreadable.
cmd/socktest section 15 asserts the plan's own wording -- bind, stat, see an
`s`, unlink -- plus the two a name table could never fail: a second bind to a
live path is refused, and connecting to a path that is a *regular file* is
refused rather than matched by name.
**And the semantics being real broke socktest on its second run**, correctly:
the socket files persisted and every bind() hit EADDRINUSE. Every real server
unlinks before binding; socktest now does too, which is the change the new
behaviour demands rather than a workaround for it.
Chasing that turned up something worse. `socktest` reported rc=1 with every
check passing, and so did every other first-party program:
- **lib/start.S called main and threw %eax away.** sys_exit() reads the
status from %rdi and nothing put it there, so every program exited with
whatever that register happened to hold. Every test suite ends in
`return failures ? 1 : 0` and none of it reached the shell.
- **exit(void) passed no status either**, for the same reason: the
declaration takes no argument, so %rdi was the caller's litter.
`if socktest; then` works now, and a failing command reports 1 -- checked both
ways, because a fix that makes everything return 0 would look identical from
the success side.
Also factored: unlink_path() out of sys_unlink, so a failed bind can take its
own node back out without going through the syscall entry, and create_sock()
as a named door rather than exporting create() with an arbitrary type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sendmsg/recvmsg with SCM_RIGHTS. Item 1 of docs/plan/foundations.md and
mechanism 1 of docs/plan/hybrid.md, which are the same primitive seen from two
directions: Wayland's wl_shm_create_pool passes a file descriptor with no
integer-shaped alternative, and handing a driver process the device it is
allowed to touch is the same act. Both plans stopped here.
Linux's structures byte for byte -- msghdr, cmsghdr, the CMSG_ macros -- because
the programs that need this are not ours. libwayland and toybox both call
recvmsg with Linux's headers and would not notice a friendlier door.
The delivery rule is the whole design. A batch of descriptors is pinned to the
stream position of the first byte of the message that carried it, and that has
two halves: a reader arriving at that position is handed them, and a reader that
has not reached it is stopped SHORT of it. Leave out the second half and a
program asking for 4096 bytes swallows three messages and is handed one
message's descriptors, with nothing in the result saying which. socktest checks
exactly that: two one-byte messages, an eight-byte read, one byte back.
The kernel holds a reference to every descriptor in flight, so a sender may
close its own copy the moment sendmsg returns. Giving that reference back is the
part that leaks if it is not written down, and there are three ways a message
ends:
- received with room installed in the receiver's table
- received without room, or by
plain read(2) closed; MSG_CTRUNC for the first case
- never received, socket closed closed by sockclose
The middle row looks like a choice and is not: holding kernel references for a
receiver that has proved it is not looking is a leak with extra steps.
No garbage collector. A socket descriptor sent over that same socket and never
received holds a reference to the socket that holds the reference to it, and
neither close reaches zero. Linux carries several hundred lines for this.
Nothing on either plan passes a socket over a socket -- Wayland passes memory,
the driver work passes devices -- so it is a bounded leak that is documented
rather than a silent one.
Fourteen checks in cmd/socktest, and every one moves data through the descriptor
rather than inspecting it. The descriptor passed is always a pipe end, because a
pipe announces its own lifetime: a read of a pipe whose writers are all gone
returns 0, which turns "was the kernel's reference released" -- otherwise
invisible from userspace -- into a value a test can compare.
Two bugs fell out, neither in the new code:
- sockpair() never set the socket's domain, so every socketpair() socket
claimed domain 0. Invisible while the data paths asked only whether a socket
was AF_INET or AF_NETLINK, since zero answers "neither" correctly.
SCM_RIGHTS asks "is this AF_UNIX", and zero is wrong for that one.
- pipe_poll() reports POLLHUP but not POLLIN at end of file, so a non-blocking
read of a pipe whose writers had all gone returned EAGAIN forever instead of
0 -- a program that polls and reads spins waiting for an end that already
happened. sockreadable() always had the equivalent condition; the pipe path
did not. Found because the test helper for "is this descriptor closed" is a
non-blocking read of a pipe.
projects/schcom/README.md said SCM_RIGHTS was done for a long time while nothing
in the kernel implemented it, and an entire GUI plan was drawn on that line.
It is true now, which is not the same as it having been right, and the README
says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
setsockopt was `return 0;` and getsockopt wrote a zero for whatever was asked.
The comment said it was so a server setting SO_REUSEADDR and treating failure as
fatal would not fail. That trade is the wrong way round: a caller told "no"
adapts, a caller told "yes" changes its behaviour on the strength of it -- sets
SO_RCVTIMEO and then blocks forever in a read it believes will time out, sets
TCP_NODELAY and reports latency numbers that measure Nagle. The failure is
silent, remote from the call, and indistinguishable from a bug in the stack.
Six options are honoured now and everything else is ENOPROTOOPT. SO_REUSEADDR,
SO_KEEPALIVE, SO_BROADCAST and TCP_NODELAY reach lwIP's pcb; SO_SNDBUF/SO_RCVBUF
are accepted and report the real size on read. SO_REUSEADDR is implemented rather
than refused precisely because of the original comment's concern -- it is the one
option a server really does set and really does check.
accept4 becomes its own table entry rather than an alias for accept. It has to
be: accept takes three arguments, so a shared handler reading a fourth register
reads whatever the caller last left there and invents a SOCK_NONBLOCK out of
stack litter. SOCK_CLOEXEC and SOCK_NONBLOCK are now honoured by socket(),
socketpair() and accept4() -- a dropped CLOEXEC leaks a listening socket across
every exec, and a dropped NONBLOCK has the caller block forever in what it
believes is a poll loop.
ICMP ping sockets: socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP), on lwIP's raw API,
with the callback stripping the variable-length IP header. Only echo replies are
eaten, and that restriction is load-bearing -- raw_input runs before lwIP's own
icmp_input, so consuming every ICMP message would swallow the echo *requests* and
this machine would silently stop answering pings from outside.
Loopback, so 127.0.0.1 is a real destination. LWIP_NETIF_LOOPBACK is the half
that is easy to miss: without it a packet addressed to an interface's own address
goes out of the wire and never comes back. With NO_SYS=1 nothing drains the
loopback queue, so the network thread calls netif_poll_all().
cmd/ping is first-party rather than toybox's, and the reason is a limitation not
a preference: toybox reads replies with recvmsg(2) and wants the TTL through an
IP_RECVTTL control message, and SchOS has neither. recvmsg is item 1 of
docs/plan/foundations.md, where it is wanted for SCM_RIGHTS anyway, and should
not be rushed to make one applet run.
Two bugs found on the way. Every AF_INET socket failed with ENFILE until DHCP
bound, because inet_new() demanded an *address* before handing out a socket and
reported it as "file table overflow"; net_is_ready() (lwIP up) is now distinct
from net_is_up() (DHCP bound). And that bug was hiding a bad test: socktest
asserted "AF_INET is refused, not faked", true when written, false since AF_INET
landed, and green the whole time because the suite runs before DHCP binds and the
ENFILE made the refusal look real. Fixing the kernel is what turned it red.
Verified: ping to 127.0.0.1, localhost, the gateway, 1.1.1.1 and 9.9.9.9, all
0% loss; socktest ALL CHECKS PASSED with ten new checks, including a setsockopt
round trip and an unknown option that must be refused -- a permissive stub cannot
pass both at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.
No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.