The site and several documents still described the system as it was before virtual terminals, the new schinit, the hybrid work and descriptor passing. docs/architecture/init.md described the wrong program. It documented a single schinit that parses manifests and supervises jobs, which has not been true since PID 1 was split: PID 1 forks a supervisor, spawns jobs on its behalf and reaps; the job table lives in /proc/schinit; a heartbeat runs both ways; and proc_reincarnate() re-execs PID 1 in place rather than panicking. Rewritten around what exists, keeping the reasoning that is still load-bearing -- the five failures of the sibling-init scheme, the escalation ladder, and the three bugs in the reincarnation path that each had to happen before they were obvious. docs/plan/init.md is deleted, which is the tree's own rule for finished work and had not been applied. Fourteen source comments referenced it by part number, so they are repointed at the architecture doc by section name rather than left dangling -- a deleted document nobody notices is worse than a stale one. docs/architecture/devices.md gained the section it has been missing since /dev/pci existed: what a driver needs from a kernel, why mmap is checked against the caller's claims, why DMA is a call and not a mapping, and why the write back after an interrupt is not optional. Also that a released device does not come back by itself, which is true and was nowhere. docs/architecture/networking.md gained a descriptor-passing section, and docs/architecture/README.md's stack diagram gained the two layers and the third seam it was missing. "Two seams hold the whole thing together" has not been true since /dev/pci. docs/plan/README.md said the hybrid work was deliberately last, on the argument that an interface with no consumer is wrong in ways nobody can see. The argument was right; the conclusion was not, and the document now says why it changed -- the alternative to a driver compatibility layer is writing a driver for every device that exists, and the original objection is answered rather than ignored because SchOS's own drivers are the consumers. The site: a Hybrid section with the eviction transcript, cards for virtual terminals, schinit and drivers outside the kernel, and the numbers remeasured rather than estimated -- 46,961 lines, 207 syscalls. The boot log in app.js is from a boot taken today, and the lines are trimmed to what the console can show without clipping, each still a verbatim prefix of real output. Two small rendering fixes found by looking at the pages rather than assuming: .console pre's min-height is now scoped to the hero, so a static transcript sizes to its content instead of reserving eight lines it does not have; and the docs reader's contents panel strips inline marks from headings, so a heading written `proc_reincarnate()` no longer carries its backticks into the sidebar. Every intra-doc link checked; none broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
343 lines
17 KiB
Markdown
343 lines
17 KiB
Markdown
# SchOS 1.0
|
||
|
||
A finish line, so that the list of open work stops being infinite.
|
||
|
||
---
|
||
|
||
## Why this document exists
|
||
|
||
[schapi.md](schapi.md) §3 says the stability freeze starts at 1.0 and covers
|
||
whatever is marked `stable` at that moment. **1.0 was never defined**, which
|
||
means there has been no finish line — and with no finish line every gap is
|
||
equally mandatory, the list only grows, and the project feels larger every week
|
||
regardless of how much gets done.
|
||
|
||
That feeling is structural, not a judgement about progress. This document fixes
|
||
the structure by drawing a line and putting most of the open work on the far
|
||
side of it.
|
||
|
||
> **Anything not named in this document is not part of 1.0.** It is not
|
||
> cancelled, not forgotten, and not up for reconsideration until 1.0 ships.
|
||
|
||
---
|
||
|
||
## What 1.0 is
|
||
|
||
> **A machine one person can sit down at and use for an afternoon without
|
||
> falling off an edge.**
|
||
|
||
Concretely — boots on real hardware, roots on SchFS, runs a shell and the real
|
||
toybox userland, is on the network, and puts a windowed terminal on the screen.
|
||
|
||
It is deliberately a **small complete system** rather than a rich incomplete
|
||
one. SchOS 1.0 is not Linux, does not run third-party applications, and has no
|
||
package manager. It is a system that works, end to end, with nothing in it that
|
||
lies about what it did.
|
||
|
||
### The acceptance test
|
||
|
||
One unattended run, asserted from the host, the way everything else here is
|
||
tested. When this passes, it is 1.0:
|
||
|
||
```
|
||
boot on real hardware from a USB stick
|
||
→ SchFS root mounts, generation counter advances
|
||
→ schinit starts the compositor
|
||
→ a terminal window appears and is asserted by pixel
|
||
→ in it: ls, cat, a file written, a network fetch
|
||
→ reboot
|
||
→ the file is still there and the checksum matches
|
||
```
|
||
|
||
Nothing in that sentence is a feature list. Every clause is something that is
|
||
currently either broken, missing, or unproven.
|
||
|
||
---
|
||
|
||
## The four gates
|
||
|
||
In order. **Do not start a gate before the one before it closes**, and closing
|
||
means a description exists in [../architecture/](../architecture/) — see
|
||
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||
|
||
### Gate 0 — Fix what is already wrong
|
||
|
||
**This comes first, before any new capability.** The desktop work is the
|
||
temptation and it is the wrong thing to do next: a compositor built on a kernel
|
||
with an unexplained hang and a socket layer that reports success for things it
|
||
did not do will fail in ways indistinguishable from its own bugs. That is the
|
||
same argument that put `netdev_selftest()` before the TCP/IP stack, and it was
|
||
right then.
|
||
|
||
**Things that are actively wrong:**
|
||
|
||
- [x] ~~**The `ls -lR` hang.**~~ **Not reproducible.** Instrumented and hunted;
|
||
see below. Left here rather than deleted because the absence of a result
|
||
is the result.
|
||
- [x] ~~**`setsockopt` returns 0 without setting anything.**~~ Six options now
|
||
genuinely reach lwIP's pcb and everything else is `ENOPROTOOPT`. Round-trip
|
||
checks in `cmd/socktest`, which a permissive stub cannot pass: it has to
|
||
read back a value it was set *and* refuse an unknown option.
|
||
- [x] ~~**`accept4` silently drops its flags.**~~ It is its own table entry now
|
||
rather than an alias for `accept` — an alias would have had to read a
|
||
fourth argument that `accept` never passes, inventing a `SOCK_NONBLOCK`
|
||
out of whatever the caller last left in that register.
|
||
- [x] ~~**Timestamps on everything created after boot read 1970.**~~ Both
|
||
`know()` (SchFS) and `now_sec()` (v1) returned `uptime()` under comments
|
||
saying the kernel had no wall clock. It has had one since `time_init()`
|
||
started reading the RTC; the comments outlived the condition. Two errors,
|
||
not one — `uptime()` counts 10 ms ticks and they were used as seconds, so
|
||
the clocks also ran 100× fast.
|
||
- [ ] **`getrusage` does not distinguish `RUSAGE_CHILDREN`** from `RUSAGE_SELF`.
|
||
- [ ] **`/proc/<pid>/exe` is a regular file, not a symlink**, so `readlink()`
|
||
fails and `ps -o comm` prints `[init]`. Needs symlink support in procfs.
|
||
- [ ] **OVMF memory reporting**, if the region-list fix did not fully close it.
|
||
|
||
**Work already done and not connected:**
|
||
|
||
These are the galling ones — the replacement exists, is tested, and is unused.
|
||
|
||
- [ ] **Port the console onto the text layer and delete the cell grid.**
|
||
`text.c` and `ttf.c` both exist and work. The console still indexes
|
||
`font_data[256][16]`.
|
||
- [ ] **Load a font from disk** and point the console at it.
|
||
- [x] ~~**Interrupt-driven receive.**~~ The e1000's handler calls
|
||
`netdev_rx_signal()`, which ends the network thread's wait early; the frame
|
||
is still *pulled* by the thread, because an interrupt handler can neither
|
||
sleep on the buffer cache nor take the lwIP lock. The wait stays on the
|
||
tick so lwIP's timers keep their floor. Proven rather than assumed:
|
||
`/proc/netif` gained `wake_rx`/`wake_tick`, and across three pings
|
||
`rx_frames` and `wake_rx` both went 2 -> 6.
|
||
|
||
**Structural, because everything after this compounds on it:**
|
||
|
||
- [ ] **A slab or pool allocator.** `kalloc` hands out one page at a time, and
|
||
every subsystem written since has invented its own fixed-size table to
|
||
work around it — `NPROC`, `NSOCK`, `NDEVICE`, `NSHM`, `MAXJOBS`. This is
|
||
the single change that most improves everything else in the kernel, and
|
||
it gets more expensive with every table added.
|
||
- [x] ~~**SchFS on-disk format version policy.**~~ Done. `SCHFS_VERSION` for
|
||
frame changes, plus `compat`/`ro_compat`/`incompat` feature flags in what
|
||
was `reserved0` — so existing images keep every field offset and read
|
||
unchanged. `uber_check()` now distinguishes *damaged* (step over it; that
|
||
is what the ring is for) from *too new* (stop), because falling back to an
|
||
older uberblock mounts a stale filesystem and reports success. Proven on a
|
||
forged image: the old code mounted the stale root and exited 0.
|
||
Still owed: an actual upgrade step, which is only writable once there are
|
||
two formats. See [../architecture/schfs.md](../architecture/schfs.md).
|
||
|
||
**Found while doing the above, and fixed:**
|
||
|
||
- [x] ~~Every `AF_INET` socket failed with `ENFILE` until DHCP bound.~~
|
||
`inet_new()` required an *address* before it would hand out a socket, and
|
||
reported the refusal as "file table overflow". Creating a socket is not
|
||
the same as having somewhere to send; a bind or connect with no route
|
||
fails on its own with an error that says so. `net_is_ready()` (lwIP up) is
|
||
now distinct from `net_is_up()` (DHCP bound).
|
||
- [x] ~~`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` above made the refusal look
|
||
real. A test asserting a failure has to say *which* failure.
|
||
|
||
**The suite itself:**
|
||
|
||
Both of these were found by running `make schfs-check` rather than by reading
|
||
it, and both predate the current work — confirmed by stashing and re-running.
|
||
They are here rather than filed as nice-to-haves because a suite that is red for
|
||
known reasons stops being read, and then it is red for unknown reasons too.
|
||
|
||
- [x] ~~**`make schfs-check` fails at baseline.**~~ Fixed, and it was not only a
|
||
test bug. The allocation map was taken through a `palloc` callback and
|
||
there was **no way to give it back** — no `schfs_space_release`, and a
|
||
comment in `schfs_space_init` reading *"caller frees what was taken"* for
|
||
an API the caller did not have.
|
||
|
||
So the kernel leaked the whole map on **every unmount**, up to 2 MB a
|
||
time, and `schfs_space_init`'s own error path stranded a page it had
|
||
already taken because `npages` was only raised after *both* allocations
|
||
succeeded. The test's 518 leaked pages were the symptom that pointed at
|
||
it.
|
||
|
||
The map now carries its own `pfree` so it can only be freed by the
|
||
allocator that filled it, `schfs_vol_open`/`schfs_vol_format` release any
|
||
map the volume already holds, and unmount releases. `schfs_objtest`
|
||
asserts the balance — 520 pages taken, 520 given back — so the next leak
|
||
fails the suite instead of being absorbed by it.
|
||
|
||
- [x] ~~**31 UndefinedBehaviorSanitizer errors.**~~ Fixed, plus two the test
|
||
never reached. One root cause: a leaf's value area holds variable-length
|
||
records packed end to end, so a record begins wherever the last one ended
|
||
— a multiple of 8 only by luck — and the code read them by casting a
|
||
pointer into the block, which tells the compiler they are aligned when
|
||
they are not.
|
||
|
||
The seven record types that live at arbitrary offsets now carry
|
||
`SCHFS_PACKED`. **No on-disk layout changed**, and that was measured
|
||
before it was applied rather than assumed: every one already had explicit
|
||
`pad` fields, so sizes and member offsets are identical with and without
|
||
the annotation. Seven `_Static_assert`s now pin those sizes, because a
|
||
record that silently grows misreads every existing volume from that point
|
||
on and looks like corruption rather than like a struct that changed.
|
||
|
||
`schfs_node_hdr`, `schfs_item`, `schfs_kptr` and `schfs_uberblock` are
|
||
deliberately left alone — the node header is exactly 32 bytes, so
|
||
everything after it is already 8-aligned, and annotating them would cost
|
||
byte-wise access on the tree's hottest paths to fix a problem they do not
|
||
have.
|
||
|
||
Verified by reading an image written *before* the change: full
|
||
`schfs_fsck --check` passes on structure, checksums, key ordering, data
|
||
blocks and derived free space.
|
||
|
||
Also caught on the way: `mkschfs` passed a null pointer to `qsort` for an
|
||
empty directory. Every implementation ignores it when the count is zero;
|
||
the standard does not say they have to.
|
||
|
||
**The identity item:**
|
||
|
||
- [ ] **Boot from a USB disk.** The xHCI stack, bulk-only transport, SCSI and
|
||
the block device all work; a superblock reads back through `bread()`. What
|
||
is missing is that the root device is compiled in rather than selectable.
|
||
Without this the acceptance test above cannot run at all.
|
||
|
||
#### The `ls -lR` hang, and why it is closed
|
||
|
||
Hunted with the instrumentation it asked for, under a workload heavier than the
|
||
one that originally triggered it — ten iterations of `ls -lR /bin`, interleaved
|
||
with writes and `cd`, 33 execs and 13378 tree descents. Measured across it:
|
||
|
||
```
|
||
inode_peak 9 / 200 unchanged
|
||
file_peak 9 / 512 unchanged
|
||
buf_pinned_peak 1 / 256 unchanged
|
||
schfs0 locked 0 tx_owner_pid 0 tx_depth 0
|
||
```
|
||
|
||
Nothing climbed, nothing wedged, and the repaired `holdingsleep` assertion never
|
||
fired. **That eliminates this file's own stated hypothesis** — "a lock or
|
||
reference-count leak somewhere in the `*at()` paths" — and it is very likely the
|
||
bug was 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 is closed rather than chased further, and that is a deliberate application of
|
||
the two-things-in-flight rule. The instrumentation is permanent: if it ever
|
||
happens again, `/proc/fsstat` names the table, or shows `locked 1` with
|
||
`tx_owner_pid 0` and the volume lock is orphaned. That is a much better position
|
||
than the one it was reported from.
|
||
|
||
Two real defects were found on the way and 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.
|
||
|
||
### Gate 1 — The foundations
|
||
|
||
[foundations.md](foundations.md), items 1–4. Descriptor passing, a shareable
|
||
memory object, `AF_UNIX` in the filesystem namespace.
|
||
|
||
Nothing above needs these; everything below stops without them.
|
||
|
||
- [x] ~~**Item 1 — `sendmsg`/`recvmsg` with `SCM_RIGHTS`.**~~ Built and tested;
|
||
fourteen checks in `cmd/socktest`, each moving data through the passed
|
||
descriptor rather than inspecting it. Described in
|
||
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
|
||
This was also mechanism 1 of [hybrid.md](hybrid.md) — one primitive for
|
||
both, which is the whole reason those are one plan.
|
||
- [ ] **Item 2 — a shareable memory object** (`memfd_create`, and
|
||
`mmap(MAP_SHARED)` on it genuinely working between two processes). This is
|
||
what a Wayland buffer still cannot cross on.
|
||
- [ ] **Item 3 — `AF_UNIX` sockets in the filesystem namespace.**
|
||
|
||
### Gate 2 — Init as a role — **done**
|
||
|
||
Milestones I0–I3, all built. PID 1 forks a supervisor and spawns jobs on its
|
||
behalf; the job table lives in `/proc/schinit`; a heartbeat runs both ways; and
|
||
`proc_reincarnate()` re-execs PID 1 in place rather than panicking. Both failure
|
||
paths are exercised by `sysrq` keys rather than reasoned about.
|
||
|
||
Before the desktop rather than after, because a compositor being brought up for
|
||
the first time crashes constantly, and something that restarts it and says why is
|
||
worth far more during that work than at the end of it.
|
||
|
||
The description is now [../architecture/init.md](../architecture/init.md); the
|
||
plan document it replaced has been deleted, per
|
||
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||
|
||
### Gate 3 — The desktop
|
||
|
||
[wayland.md](wayland.md), W1–W3, and the terminal.
|
||
|
||
**W4 and W5 are not in 1.0.** Multi-window focus policy and a panel are
|
||
post-1.0. One terminal window that works is the bar.
|
||
|
||
---
|
||
|
||
## Not in 1.0
|
||
|
||
The point of this list is that it is *written down*, so it can stop occupying
|
||
attention. Nothing here is cancelled.
|
||
|
||
| | Why it waits |
|
||
|---|---|
|
||
| **The rest of the hybrid kernel** ([hybrid.md](hybrid.md)) | Still not a completeness requirement — but the mechanisms and the first eviction happened early, because the alternative to a driver compatibility layer is writing a driver for every device that exists, and that is not a plan. What is deferred is the foreign driver host, not the interface it needs. |
|
||
| **Dynamic linking, Mesa, SDL3** | Everything at 1.0 is statically linked. This is the single biggest post-1.0 unlock and it is a whole project. |
|
||
| **A page cache** | `memfd` covers the desktop's needs. General writable `MAP_SHARED` of a file is a real project with no 1.0 consumer. |
|
||
| **`.sip` and the installer** | 1.0 installs by writing a USB stick. That is how most systems start and it is honest. |
|
||
| **SchFS snapshots, FUSE, Linux module, sparse files, online fsck** | The format version (Gate 0) is the prerequisite for all of them, and it is the only part on a clock. |
|
||
| **The interactive crash screen** | PSOGM renders, prints a full report and emits a QR code. That is enough. |
|
||
| **CFF/OTF fonts, kerning, text layout** | TrueType and fixed advances are enough for a terminal. |
|
||
| **Routing, `getsockname`/`getpeername`, IPv6** | One interface, one default route, works. |
|
||
| **Multiple outputs, damage tracking, subsurfaces, xkbcommon** | One screen, full repaints, one keymap. |
|
||
| **Audio** | Does not exist in any form and is not required to use a machine. |
|
||
| **Panel, notifications, settings, file manager** | A terminal is a desktop. |
|
||
| **SchAPI itself** | Deliberately. It should be designed *from* the compositor's experience, not before it. |
|
||
|
||
---
|
||
|
||
## The rule for this list
|
||
|
||
**Adding to Gate 0–3 requires deleting something else from it.**
|
||
|
||
Not a productivity trick — a forcing function. The reason this project feels
|
||
overwhelming is that ~20 fronts have been open simultaneously, and no amount of
|
||
discipline makes that workable. The fix is structural.
|
||
|
||
And one working limit, worth holding to literally:
|
||
|
||
> **Two things in flight. Never three.**
|
||
|
||
One in Gate 0 and one being written up. Everything else is closed — in the
|
||
document, and out of your head.
|
||
|
||
---
|
||
|
||
## How you will know it is going well
|
||
|
||
Not by velocity. By this:
|
||
|
||
**Every closed item leaves a description behind.** When the `ls -lR` hang is
|
||
fixed, `history/kernel.md` gains an entry saying what it actually was. When the
|
||
console moves onto the text layer, `architecture/graphics.md` loses a line from
|
||
its *What it does not do* section.
|
||
|
||
Those "does not do" sections shrinking is the real progress bar, and it is the
|
||
only one that does not lie.
|
||
|
||
---
|
||
|
||
## A note worth keeping
|
||
|
||
This document was written because the work started to feel overwhelming, and
|
||
that is worth recording rather than tidying away — because the feeling was
|
||
about the *shape* of the work and not its quality, and the two are easy to
|
||
confuse when you are inside it.
|
||
|
||
The evidence at the time: SMP working on real hardware after five bugs stacked
|
||
behind each other; a copy-on-write filesystem whose crash test cuts the power one
|
||
write later, 119 times, and demands a clean mount after each; a TCP/IP stack
|
||
asserted from both ends; an xHCI driver written from the specification; and a
|
||
documentation tree that records why things are strange so they do not get
|
||
un-fixed by accident.
|
||
|
||
None of that is the output of a project in trouble. It was a project with too
|
||
many tabs open.
|