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
The second of the two things the stack owed. recvfrom accepted the
address arguments and ignored them, so every caller was told nothing
about where a datagram came from -- which for UDP is most of the point:
a server that cannot see the sender cannot reply to it.
Fixing it properly meant fixing something worse that was sitting
underneath. UDP payloads went into the same byte ring TCP uses, with no
record of where one datagram ended and the next began, so two datagrams
that arrived together came back from one read as one. That is a bug
whose appearance depends on timing and whose blame lands on the sender.
Datagrams now carry a header alongside the bytes: source address, source
port, length. Sixteen of them per socket, in a ring beside the existing
one. Whole buffers would have been 16 sockets x 16 datagrams x an MTU of
static memory for something almost always empty; only the boundaries need
keeping, and those are eight bytes each. A datagram too big for the
buffer it is read into is truncated and the remainder discarded, rather
than left in the ring to be delivered as the head of the next one. One
that arrives with the queue full is dropped whole -- that is UDP's own
contract, and half a datagram in the ring would corrupt every one after.
An empty queue and a zero-length datagram are kept apart, because a
sender really can send zero bytes and the right answer to that is 0
rather than "nothing has arrived yet".
sendto grew the mirror image: a destination address on a datagram
socket, which is what a server replying to whoever just wrote to it
needs. On TCP a destination is ignored rather than refused -- the
connection decides where the bytes go, and programs pass one out of
habit. For a stream, recvfrom's source is read from the pcb: it is the
peer, and it is the same answer for every byte.
Verified: bootcheck -Net now covers UDP too, 45 [ok] and no failures.
The address check is real rather than cosmetic -- nettest deliberately
leaves its sockaddr uninitialised, so a kernel that filled in nothing
would print stack rubbish instead of 10.0.2.2 and the port the responder
is actually on. The boundaries are checked by byte count: the responder
sends 20 bytes and then 16, and the guest reports reading 20 and then
16. Treated as a stream the first read would have taken all 36.
Left for later, and deliberately: accept() still does not report the
peer's address, and getsockname/getpeername do not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lwIP 2.2.0, fetched by ports/lwip and compiled straight into the kernel.
Not written from scratch, and the reason is not the packet formats: it is
congestion control, retransmission, window management and twenty years of
corner cases that only show up against a real peer. Not FreeBSD's
netinet either, though the licence would have allowed it -- that stack is
not a library, it reaches into mbufs, epoch(9), netisr, VNET and uma(9),
so adopting it means porting FreeBSD's kernel infrastructure first. lwIP
was written for exactly this situation: a small kernel with a raw
Ethernet driver and a port layer of a few hundred lines.
It is kernel code, so nothing is installed into the sysroot and there is
no second build system: the top-level Makefile compiles the files it
wants against our port headers. The file list is explicit rather than a
wildcard, because lwIP ships IPv6, 6LoWPAN, PPP, SLIP, DNS-SD and a dozen
apps in the same tree, and building what is not configured yields either
dead code or a link error naming a port function nobody meant to write.
Its sources are unmodified and its warnings are silenced for its files
only -- patching them would mean maintaining a fork.
The port layer is ours: a lock, a clock, the netif that hands frames to
e1000, lwipopts.h, and a shim/ directory of tiny headers so lwIP's
<string.h> and friends resolve to the kernel's functions instead of the
host's, whose prototypes disagree and would otherwise be found on the
default include path.
NO_SYS=1, so lwIP has no threads of its own and one kernel thread feeds
it. A thread rather than the interrupt handler, because a call into lwIP
can reach the driver, the driver can sleep, and an interrupt handler
cannot -- so the interrupt still only clears the cause register and the
thread polls. kthread_create is new and generally useful: a process with
a kernel-only page directory, no files, no cwd, no parent, dispatched
from forkret, which can therefore sleep and be preempted like anything
else. DHCP obtains an address at boot and the console says which one.
AF_INET went in exactly where phase 2 said it would. socknew dispatches
on the domain, kernel/net/inet.c implements the internet side on lwIP's
raw API, and not one syscall changed shape: bind, connect, listen,
accept, read and write reach the same descriptors they always did. TCP
and UDP both work. cmd/nettest opens a connection, writes, and reads the
answer back.
Two things are owed and named here so they are not discovered later:
recvfrom does not yet report the source address, and the socket paths
poll on the timer tick instead of sleeping on the socket. Both are
follow-ups, not redesigns.
NOTICE carries lwIP's BSD-3 text. Clause 3 is no-endorsement, not the
old advertising clause, so it sits with Apache-2.0 without difficulty.
Verified: the tree builds clean, and the headless boot check passes with
27 [ok] and no failures with the stack compiled in and the thread
running -- which is the part that would break first if netinit deadlocked
or the thread starved anything. An end-to-end connection test belongs in
that suite and is the next commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>