docs: architecture describes what is built now

kernel (threads, demand paging, TLB shootdowns, dynamic linking, RAM direct map), syscalls, filesystem (/proc, /sys,
/dev), terminals (12, VT handshake, ptys), networking (r8169 through LDLS), LDLS (module sets; Linux code may ship),
devices (what stays in the kernel; hybrid.md's lasting content), boot (the loader's placement of the kernel),
graphics (fbdev, GMMOS, QR 1-40), audio (ALSA over OSS), debugging, usb. New: packages.md (the apk root) and
desktop.md (Xorg on the framebuffer).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 07:57:25 +01:00
commit 2556a955b3
15 changed files with 921 additions and 341 deletions

View file

@ -1,7 +1,9 @@
# Networking
ARP, IPv4, ICMP, UDP, TCP and DHCP, over a driver-agnostic interface class,
reached through the same socket syscalls as Unix domain sockets.
reached through the same socket syscalls as Unix domain sockets — plus DNS
(resolved in the process, by musl), HTTPS (`wget`, Alpine's TLS libraries) and a
small netlink responder for `getifaddrs`.
Verified end to end rather than by inspection: `bootcheck -Net` starts a
listener and a UDP responder on the host, tells the guest to talk to them, and
@ -25,7 +27,9 @@ that completes a handshake and drops every byte.
└────────────────┬───────────────────┘
│ struct netdev_ops
┌────────────────┴───────────────────┐
│ e1000, and any future NIC driver │
│ e1000 rtl8139 r8169 — or a │ in the kernel
│ Linux driver in a process, which │ (LDLS, via /dev/netdev)
│ is a netdev like any other │
└────────────────────────────────────┘
```
@ -86,50 +90,55 @@ covers two decades of silicon — and why the driver prints the chip's XID on
probe, since the PCI device id tells you almost nothing about which part
arrived.
### Why it is native, when the plan said LDLS
### Native first, then Linux's own
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md). Measuring
it is what changed the answer: the module has **183 undefined symbols, 130 of
them not implemented**, and 35 of those 130 are **phylib** —
`phy_connect_direct`, `phy_start`, `mdiobus_read`, `genphy_soft_reset`.
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md), and
measuring it changed that: the module has **183 undefined symbols, 130 of them not
implemented**, and 35 of those 130 are **phylib** — `phy_connect_direct`,
`phy_start`, `mdiobus_read`, `genphy_soft_reset`. phylib is a subsystem with a link
state machine, not a set of leaf calls, and LDLS's usual answer for an
unimplemented symbol — a stub that reports itself and returns zero — is precisely
wrong for it: `phy_start` returning zero means the link never comes up and nothing
says why. So a **native driver** came first: it reaches the PHY through the
chip's own MII window (`PHYAR`, one register that is a whole MII bus) and skips
the subsystem, in the shape of the e1000 beside it.
phylib is a subsystem with a link state machine, not a set of leaf calls, and
LDLS's usual answer for an unimplemented symbol — a stub that reports itself
and returns zero — is precisely wrong for it. `phy_start` returning zero means
the link never comes up and there is nothing on screen to say why. The 8139too
that ldls.md uses as its model has no phylib dependency, which is why that one
was tractable.
It has since been done the other way as well. LDLS grew a driver core, devres,
firmware loading and phylib's tables ([ldls.md](ldls.md), *A set of modules*),
and the ASUS TUF A15's RTL8168H is now driven by **Linux's r8169, unmodified, in
a process**: `make ldls-r8169` stages it with libphy, mdio-bus and the PHY driver,
and `etc/schinit/ldls-r8169.job` starts it at boot. The native driver is **out of
the registry** — it would claim the device first — and stays in the tree, with
Linux's 8168H setup, as a fallback.
A native driver reaches the PHY through the chip's own MII window (`PHYAR`,
one register that is a whole MII bus) and skips the subsystem entirely. It is
less work than the port, it carries no GPL entanglement, it needs no firmware
blob, and it is not coupled to one Linux version.
The register layout is a *fact about the hardware*, taken the way
[ldls.md](ldls.md) §3 says facts may be taken. The expression is this tree's:
a descriptor-ring driver in the same shape as the e1000 beside it.
On that laptop the interface is `ldls0`, and it appears *after* boot, which is why
`net_attach_late()` starts the stack when the first interface arrives instead of
turning it away.
### The Chromebox has one of these
The Asus Chromebox CN62 "Guado" turns out to carry an RTL8111 rather than the
Intel part its spec sheet suggests, so this driver covers it as well as the
laptop that prompted it — which is the argument for the family match in one
line rather than a device id per board.
The Asus Chromebox CN62 "Guado" carries an RTL8111 rather than the Intel part its
spec sheet suggests, so the family match (`10EC:8168`) covers it as well as the
laptop — which is the argument for matching a family in one line rather than a
device id per board.
### It has not run on hardware
### It has not been shown to work on hardware
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139`
and nothing else from Realtek, and the 8139 is a different programming model —
a circular receive buffer rather than descriptors — so it is deliberately *not*
in the match table. Claiming it would break a NIC that works.
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139` and
nothing else from Realtek, and the 8139 is a different programming model — a
circular receive buffer rather than descriptors — so it is deliberately *not* in
the match table. Claiming it would break a NIC that works.
What has been verified: the driver builds, its presence changes nothing on a
machine without the device, and with an `rtl8139` on the bus it correctly
leaves it unclaimed (`10ec:8139 network controller [-]`).
What has been verified: both drivers build, their presence changes nothing on a
machine without the device, and Linux's r8169 under LDLS loads, links and
initialises in QEMU, registering and finding no device. On the laptop it reached
probe and read the PHY (after a bug in LDLS that made every MDIO read return
`0x02460246` was fixed); the native 8168H bring-up — the PLL, the receive gate, the
PHY through the OCP window — has not run on a part.
What has not: every register write, on a real part.
What has not: traffic on real hardware, from either.
So the probe says everything a first bug report would need, with `cprintf`
The native probe says everything a first bug report would need, with `cprintf`
rather than `kinfo` so it reaches the screen on a machine with no serial port:
```
@ -141,6 +150,9 @@ register 1, where bit 2 is link and bit 5 is autonegotiation complete. Those
four facts separate "the chip is not there", "the chip is there and the PHY
sees nothing", and "everything is up and the rings are wrong".
The kernel turns **ASPM off** on Realtek NIC links at boot, as Linux's driver
does through `pci_disable_link_state` — which a process under LDLS cannot do.
### Two constants that were wrong first
Recorded because both would have worked, quietly:
@ -262,6 +274,12 @@ to. Measured across three pings: `rx_frames` and `wake_rx` both went 2 → 6.
table, and it returns immediately: everything happens in that thread, which
cannot run until the scheduler is entered.
**The thread follows each device's link into lwIP.** The link used to be read once
at attach, 0.02 s into boot; real autonegotiation takes seconds, so on hardware
the interface stayed down for good. It also starts the stack when an interface
appears after boot (a driver in a process, a USB NIC), and attaches every device
there is as its first act; `net_eth_attach()` refuses one it already has.
DHCP obtains an address at boot. `/proc/netif` reports each interface's address,
netmask, gateway, MAC and frame counts, taken at the lwIP seam.
@ -326,19 +344,20 @@ The creation lives in `sys_bind()` rather than here, because that is the layer
with both: `kernel/net/unix.c` knows about sockets and has no business calling
`create()`.
**`flock()` is still missing.** libwayland locks `wayland-0.lock` beside the
socket so that two compositors cannot claim one display. The lock file can be
created and opened now that paths are real; the advisory lock itself is a
separate mechanism with no other caller yet.
`flock()` and `fcntl` record locks exist (`kernel/fs/lock.c`), so the lock files
next to a socket work the way they do on Linux. Names in
the **abstract namespace** (a `sun_path` starting with a NUL) name no file; they
work, and `getsockname` reports them unnamed. `SO_PASSCRED` is honoured, and
`SO_PEERCRED` answers (tmux asks).
### Descriptor passing
`sendmsg`/`recvmsg` with `SCM_RIGHTS`, which is the one thing a socket can carry
that a pipe cannot: a reference to an object the kernel already knows, rather
than a copy of its contents. Wayland's `wl_shm_create_pool` passes a descriptor
and has no integer-shaped alternative, and
[hybrid.md](../plan/hybrid.md)'s mechanism 1 is how a driver process is handed
the device it is allowed to touch. Both wanted the same primitive.
and has no integer-shaped alternative, and the same mechanism is how a driver
process is handed the device it is allowed to touch ([devices.md](devices.md)).
Both wanted the same primitive.
**A batch of descriptors is pinned to the stream position of the first byte of
the message that carried it.** That single rule is the whole delivery model, and
@ -374,9 +393,9 @@ descriptors to a position that never arrives.
**There is no garbage collector.** An AF_UNIX 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 can reach zero. Linux carries several hundred
lines to break these cycles. Nothing on either plan passes a socket over a
socket — Wayland passes memory, the driver work passes devices — so this is a
known and bounded leak (thirty-two sockets) rather than a silent one.
lines to break these cycles. Nothing passes a socket over a
socket — display servers pass memory, the driver work passes devices — so this is
a known and bounded leak (thirty-two sockets) rather than a silent one.
### AF_INET
@ -593,59 +612,47 @@ information was there and the code did not ask.
## What it does not do
- **`ping` is a first-party program**, `cmd/ping`, not toybox's. `recvmsg` now
exists, so half of what stopped toybox's is gone; what remains is
`IP_RECVTTL`, an `IPPROTO_IP` control message that would have to be generated
on the receive path rather than merely carried. Ours has no `-I`, no flood
mode and no TTL display.
- **`SCM_RIGHTS` is the only control message.** `SCM_CREDENTIALS` is refused
with `EINVAL` rather than ignored: a caller told its credentials were passed
would have the receiver read a uid out of uninitialised memory and act on it.
- **`ping` is a first-party program**, `cmd/ping`, not toybox's or busybox's. Ours
has no `-I`, no flood mode and no TTL display (`IP_RECVTTL` is an `IPPROTO_IP`
control message that would have to be generated on the receive path), and takes a
dotted quad or `localhost` — it does not link musl's resolver. Busybox's `ping`,
from the Alpine root, works over a `SOCK_RAW` ICMP socket.
- **Only `SCM_RIGHTS` and `SCM_CREDENTIALS` travel in a control message.** Anything
else is refused rather than ignored.
- **A message may carry eight descriptors, and a socket eight such messages.**
Past that `sendmsg` answers `ENOBUFS`. The cap exists because the kernel holds
a reference to each one until it is received; a sender eight unreceived
messages ahead of its receiver is not rescued by a ninth slot.
- **`ping` still takes a dotted quad**, even though names now work. It is a
first-party program that does not link musl's resolver, and `localhost` is
special-cased in it rather than resolved. Every *ported* program resolves
names properly; ours is the one that does not.
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot. A
lease that changes the DNS server, or a `/dev/pci` handover that brings up a
new interface, leaves a stale `/etc/resolv.conf` until somebody runs it again.
- **lwIP's own DNS client is still unused.** It is compiled in and only
`dns_getserver()` is ever called; resolution happens entirely in the process.
Nothing in the kernel can resolve a name, which matters the day something in
the kernel wants to.
- **`httpd` does not serve HTTP/1.1.** Every response is HTTP/1.0 with
`Connection: close`, because the directory listing is generated straight into
the socket and so has no `Content-Length` to give. One request per connection
is the cost.
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot
(`etc/schinit/resolvconf.job`). A lease that changes the DNS server, or a
`/dev/pci` handover that brings up a new interface, leaves a stale
`/etc/resolv.conf` until somebody runs it again.
- **lwIP's own DNS client is unused.** Resolution happens entirely in the process
(musl); nothing in the kernel can resolve a name.
- **`httpd` serves HTTP/1.0** with `Connection: close`, one request per connection,
and has no timeouts: a client that connects and says nothing ties up a child
process until it goes away, and `SO_RCVTIMEO` does not exist here.
- **A queued-but-never-accepted connection is closed without clearing its
callbacks.** `inet_close()` clears the argument and closes, which is enough
because the pcb is freed — but it is a narrower guarantee than the one the
main path makes, and it relies on `tcp_close()` succeeding.
- **`httpd` has no timeouts.** A client that connects and says nothing ties up
a child process until it goes away; `SO_RCVTIMEO` does not exist here (see
below), so there is nothing to set.
- **No routing table**, and no `/proc/net/route` to report one. With two
interfaces up, the first is the default route and the second is reachable only
by an address on its own subnet. That is a rule rather than an accident, and it
is not routing. The missing *file* is the more immediately visible half: it is
how a program asks which interface carries the default route, and without it
anything that filters on "the default one" sees nothing.
- **Netlink serves two message types.** No `RTM_GETROUTE`, no `RTM_NEWROUTE`, no
event stream, no multicast groups. A program that expects to *subscribe* to
link changes gets a socket that answers dumps and never speaks again.
- **No `getsockname`/`getpeername`**, and `accept()` does not report the peer's
address for an AF_INET socket. A server that wants to log who connected cannot.
- **`setsockopt` covers six options**, and refuses the rest with `ENOPROTOOPT`
rather than pretending: `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. Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
because the pcb is freed, but it is a narrower guarantee than the main path
makes and relies on `tcp_close()` succeeding.
- **No routing table**, and no `/proc/net/route`. With two interfaces up, the first
is the default route and the second is reachable only by an address on its own
subnet. That is a rule rather than an accident, and it is not routing.
- **Netlink is a responder**: `RTM_GETLINK` and `RTM_GETADDR` dumps (what
`getifaddrs` needs), `MSG_PEEK|MSG_TRUNC` sizing (GLib's network monitor measures
each datagram that way and spun at 100% when it answered 0), and nothing else. No
`RTM_GETROUTE`, no event stream, no multicast groups; a program that expects to
*subscribe* to link changes gets a socket that answers dumps and never speaks
again.
- **`setsockopt` applies what it understands** (`SO_REUSEADDR`, `SO_KEEPALIVE`,
`SO_BROADCAST`, `TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` report the
real size; `SO_PASSCRED` on Unix sockets) and refuses the rest with `ENOPROTOOPT`.
Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
- **`SOCK_DGRAM` over AF_UNIX does not exist** — stream only.
- **AF_UNIX names are not filesystem paths**, so nothing can `stat()` or
`unlink()` a socket.
- **One NIC driver exists.** The class is proven with two e1000 cards, not with
two different drivers.
- **No IPv6**, no DNS resolver in the kernel (lwIP's is compiled but unused by
anything), and no firewall or packet filter of any kind.
- **No IPv6**, no firewall or packet filter of any kind, and a TCP stack that has
been used for downloads at tens of megabits and no more than that.
- **Three drivers in the kernel, one Linux driver outside it**, and the class is
proven with two e1000 cards and an LDLS handover, not on a wide range of
hardware. The Realtek parts have never carried a packet on real silicon from this
tree.