docs: README, plan, guides and the site describe what is built now

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
This commit is contained in:
Nexoniarz 2026-10-26 08:08:05 +01:00
commit cb3d28e3d6
57 changed files with 685 additions and 2121 deletions

View file

@ -298,17 +298,8 @@ exported entry points, `nameifrom()`/`nameiparentfrom()`. `sys_fs.c` gained
Every `*at()` syscall now uses these instead of its own inline
absolute-path/`AT_FDCWD`-only check.
**Known follow-on issue, not yet root-caused**: heavy `ls`/`ls -lR` use
across many commands in one long-running shell session can eventually make
a *later*, unrelated directory hang (reproduced with `ls -lR /var` after
~10 prior commands in the same session; the identical command succeeds
immediately on a fresh boot). Not an inode-table or file-table exhaustion
— both `iget()` and `filealloc()` fail cleanly (panic / return 0
respectively) when full, and neither fired. Smells like a lock or
reference-count leak somewhere in the *at() paths above, or in toybox's
own fd handling, that only manifests after enough accumulated opens/closes.
Next step: instrument `itable`/`ftable` occupancy counts and watch them
across a long `ls`-heavy session to see what's actually climbing.
**Known follow-on issue** (heavy `ls -lR` in one long shell session eventually hanging a
*later* directory): hunted, and not reproducible — see section 24.
## 6. Boot speed
@ -1255,3 +1246,86 @@ is at least as long as the width, and that the aperture holds that many rows.
If not, it goes back to the previous mode and says so in the menu. This proves
nothing about a mode that passes, but it stops a lying one from reaching the
kernel.
---
## 24. The `ls -lR` hang that was hunted and not found
**Symptom.** After about ten `ls`-heavy commands in one long shell session, a later and
unrelated `ls -lR /var` would hang; the same command succeeded at once on a fresh boot. It
was not inode-table or file-table exhaustion (both fail cleanly), and the first hypothesis
was a lock or reference-count leak in the `*at()` paths.
**What was done.** `/proc/fsstat` was given the occupancy counters the hypothesis asked
for (inode, file and buffer peaks, the SchFS volume lock and transaction owner), and the
workload was made heavier than the report: ten rounds of `ls -lR /bin` interleaved with
writes and `cd` — 33 execs and 13,378 tree descents. Nothing climbed:
inode_peak 9 / 200 file_peak 9 / 512
buf_pinned_peak 1 / 256 schfs0 locked 0 tx_owner_pid 0 tx_depth 0
**Conclusion.** The hypothesis is eliminated, and the bug is very likely gone — fixed in passing
by the `*at()` dirfd rewrite, the `getdents` cookie rewrite, or SchFS becoming the root, all of
which landed after it was reported. It was closed rather than chased. The instrumentation
stayed: if it happens again, `/proc/fsstat` names the table, or shows `locked 1` with
`tx_owner_pid 0`, which is an orphaned volume lock.
Two real defects turned up on the way and were fixed, neither proven to be the cause:
`holdingsleep()` never compared the owning pid, which disarmed all seven `panic()` assertions
built on it; and SchFS's `tx_mine()` compared a bare `struct proc *`, which a recycled ptable
slot makes ambiguous.
**The transferable part.** A hang you cannot reproduce still earns its instrumentation. The
absence of a result *is* the result, and the right place to leave it is where the next person
with the same symptom will look.
## 25. Epiphany's heap was corrupted by a page nobody had unmapped yet
**Symptom.** Epiphany (WebKit: forty threads on four processors) died at a different address
on every run; musl's allocator was the first to notice, a group's metadata no longer pointing
back at its group.
**Diagnosis.** Nothing was freed twice. One thread unmapped a range, `mmap` handed the same
addresses straight back, and a thread on *another* processor went on reading and writing the
**old** pages through its TLB: stores vanished, loads were stale. Deferring the free of the
unmapped pages had already kept a stale translation from reaching a page someone else owned;
it did not keep one address space *coherent*.
**Fix.** Unmapping from a shared address space interrupts the processors running it and waits
until each has reloaded `CR3` (`tlb_shootdown()`, `kernel/arch/x86_64/vm.c`).
**The transferable part.** Threads turned an invisible assumption — one CPU's TLB is the only
one that matters — into the loudest bug in the tree, in the one program that used the most
threads. Memory corruption that moves every run is a coherence bug until proven otherwise.
## 26. Every MDIO read returned `0x02460246`
**Symptom.** Linux's `r8169` under LDLS, on the laptop, read its PHY's ID as `0x02460246`.
**Diagnosis.** `0x246` is a value of `RFLAGS`, not a register. `mdio-bus` keeps the word it
has just read in `r11` across `preempt_enable()`, which on x86 is `decl %gs:__preempt_count; je`
to a call that preserves every register. LDLS had given `__preempt_count` zeroes, so the count
reached zero after the decrement and the call was made — into a logging stub whose `syscall`
instruction leaves `RFLAGS` in `r11`.
**Fix.** `__preempt_count` starts at `PREEMPT_ENABLED` (x86 keeps `NEED_RESCHED` inverted), and
`preempt_schedule` and its notrace twin are a bare `ret`.
**The transferable part.** A stub that returns zero is not safe when zero changes which path
runs — the same family as the five in [../architecture/ldls.md](../architecture/ldls.md). The
tell here was a value that was a plausible *flag word* rather than a plausible *register*.
## 27. GLib's network monitor spun at 100%
**Symptom.** Epiphany's main thread consumed a full core in `poll()` returning at once.
**Diagnosis.** GLib sizes each netlink datagram with `recvmsg(MSG_PEEK|MSG_TRUNC)` and no buffer.
The kernel answered 0, GLib read 0 bytes, the datagram stayed queued, and `poll()` said readable
again, for ever. A drained netlink socket also reported hangup, because `sock_hup` took "no peer"
for "closed".
**Fix.** `recvmsg` on netlink peeks without consuming and with `MSG_TRUNC` returns what is waiting.
**The transferable part.** A kernel that accepts a flag and does nothing with it turns a
well-formed program into a busy loop. Do it, or refuse it.