diff --git a/CLAUDE.md b/CLAUDE.md index 071d53649ef..56479266957 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ ToyOS is a general-purpose operating system written from scratch in Rust. Its ch - **Rust is first-class.** Prefer compile-time safety to runtime checks, and runtime checks to tests. Validate external representations once before constructing typed values; validate each later message at its own boundary. Nothing a type makes impossible needs a runtime test. A kernel object type and an application's protocol type are separate contracts. - **No silent debt.** Remove a discovered compromise or record its evidence, owner and exit in `issues/`. Filing it does not make it acceptable or complete. Never weaken audible or visual quality without the owner's explicit agreement. -Never use a fixed delay where an event can be awaited. A wait has a failure timeout. A hardware-mandated delay with no notification is the exception, with the requirement cited at the operation. +Never use a fixed delay where an event can be awaited. A wait has a failure timeout; on expiry it returns an error where its interface can carry one and otherwise reports the expiry, except a wait for swapped memory, which behaves like RAM and is reported. A hardware-mandated delay with no notification is the exception, with the requirement cited at the operation. ## Architecture and sources of truth diff --git a/docs/src/architecture/boot.md b/docs/src/architecture/boot.md index 3560982324c..0998152ca29 100644 --- a/docs/src/architecture/boot.md +++ b/docs/src/architecture/boot.md @@ -34,6 +34,9 @@ - The disk carries two slots, each a boot volume and a ROOT, named by a slot table. An update is written to the slot the machine is not running. +- Updates fix forward. The loader boots the slot marked to boot next and no + other; a slot that is refused or fails is reported, never replaced by the + previous one automatically. The remedy is a newer image. - Updating is an ordinary program: a signed image on standard input, written to the idle slot and marked to boot next. The signature is its whole authority, and how the bytes arrived does not matter. diff --git a/docs/src/architecture/files.md b/docs/src/architecture/files.md index 65f1bac16e6..8700e7ebd7c 100644 --- a/docs/src/architecture/files.md +++ b/docs/src/architecture/files.md @@ -21,7 +21,9 @@ prefix. An empty view names nothing. - **Sharing is granted, never reached.** A file outside a program's view reaches it only through the file picker, which hands over that one file. - **No special cases.** The boot partition is in the updater's view and nobody - else's; login data is in the login authority's view alone. + else's; login data is in the login authority's view alone; `/apps` is + writable only in the installer service's view, and every other program sees + it read-only. The working directory is the client's: a spawn states the child's starting directory, which the child resolves in its own view. diff --git a/docs/src/architecture/memory.md b/docs/src/architecture/memory.md index a3434c5e88e..e8bf2fc7777 100644 --- a/docs/src/architecture/memory.md +++ b/docs/src/architecture/memory.md @@ -61,8 +61,8 @@ preallocated on internal storage, with keys that live for one boot. - Commitment counts usable RAM plus reserved swap, never hoped-for compression or growth. Swap adds capacity, not responsiveness. -- Swapped memory may make a load, a store or a copying system call wait on - storage. The wait advances with storage progress and is reported when +- Swapped memory behaves like RAM: it may make a load, a store or a copying + system call wait on storage. The wait advances with storage progress and is reported when overdue; a timeout never turns it into an error, and a non-blocking descriptor does not cover the caller's own buffer. - A page is lost only when a running memory service establishes the loss: a diff --git a/docs/src/architecture/scheduling.md b/docs/src/architecture/scheduling.md index ad5f0f17054..88f0b9d0fb2 100644 --- a/docs/src/architecture/scheduling.md +++ b/docs/src/architecture/scheduling.md @@ -10,6 +10,8 @@ - CPU time is shared by jobs, not threads. A process's and its helpers' threads share one job, so helpers buy no extra share; authorised launchers create independent jobs. +- Nothing reserves CPU time: no program is promised a budget, and nothing is + admitted against one. - Every job has equal weight, across users too. Window focus changes responsiveness, not sustained entitlement. - Each CPU runs EEVDF over the job groups it holds, and again among each @@ -26,8 +28,7 @@ - Each CPU has an aggregate real-time allowance, replenished each interval, that protects ordinary progress. Once it is exhausted, that CPU's real-time workers run only when no ordinary work wants the CPU, until it refills. -- There is no per-stream reservation and no deadline admission. New streams - are accepted best-effort; genuine overload may cause dropouts, and a late +- New streams are accepted best-effort; genuine overload may cause dropouts, and a late client contributes silence without holding back the others. ## Power diff --git a/docs/src/architecture/sessions.md b/docs/src/architecture/sessions.md index 8a57ad30079..3a122b2d0ff 100644 --- a/docs/src/architecture/sessions.md +++ b/docs/src/architecture/sessions.md @@ -37,8 +37,10 @@ to ask for a session. The session's view is the user's home, the system read-only and the session's own temporary directory. - A program in one user's session cannot name another user's files. -- The local desktop is a login session. SSH authentication starts the user's - session, whose shell inherits no host key and no SSH service authority. +- The local desktop is a login session like any other: the local greeter asks + for it, and it has the same per-session view and isolation. SSH + authentication starts the user's session, whose shell inherits no host key + and no SSH service authority. ## Ending a session diff --git a/docs/src/architecture/trust.md b/docs/src/architecture/trust.md index 26dc15d5871..6522bb375f0 100644 --- a/docs/src/architecture/trust.md +++ b/docs/src/architecture/trust.md @@ -29,9 +29,9 @@ DMA stays behind the IOMMU. - The same driver runs with and without an IOMMU; there is never a second driver. -- Where the hardware genuinely has no isolation, a driver the signed image - ships may claim a device and no other may, and the machine states plainly - that it runs without device isolation. +- Where the hardware genuinely has no isolation, any driver the signed image + ships may claim its device with its ordinary claim, and no other driver may. + The machine states plainly that it runs without device isolation. - External DMA-capable devices always need isolation. - Unknown isolation, unsupported isolation hardware and a failed setup are errors, not evidence that isolation is absent. There is no machine-specific diff --git a/docs/src/product/privacy.md b/docs/src/product/privacy.md index eeccfa6125e..ec98a4426e8 100644 --- a/docs/src/product/privacy.md +++ b/docs/src/product/privacy.md @@ -6,8 +6,9 @@ These are the user-visible rules. The mechanisms behind them are ## Permissions -- Programs request resources at run time, and the answer, approval or denial, - can be remembered. +- Programs ask for a right at run time, when they need it, and the answer, + approval or denial, can be remembered. There is no up-front permission + list. - Choosing a file grants that file, not its folder. Remembered access does not follow renames. Writable access to a document includes replacing it safely, scoped to that file. diff --git a/issues/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md b/issues/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md index 255271b4b1a..8b3d2aa840e 100644 --- a/issues/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md +++ b/issues/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md @@ -40,9 +40,7 @@ kernel arms the chipset's watchdog only on the `watchdog` parameter which `issues/every-job-test-runner-starts-holds-its-whole-system-capability.md` fences. -- `watchdogd` feeds from a thread under `rt`, until - `issues/cpu-time-is-a-band-and-not-a-reservation.md` gives it a - reservation. +- `watchdogd` feeds from a thread under `rt`. - **Every guest runs with `-action watchdog=none`.** q35's TCO counts `QEMU_CLOCK_VIRTUAL`, which a loaded host advances while it starves a guest, and its second expiry does what `-action watchdog=` says (QEMU 11.1.1, diff --git a/issues/a-machine-without-an-iommu-refuses-every-claim.md b/issues/a-machine-without-an-iommu-refuses-every-claim.md index b2ea7e87b5d..2b197b91cfd 100644 --- a/issues/a-machine-without-an-iommu-refuses-every-claim.md +++ b/issues/a-machine-without-an-iommu-refuses-every-claim.md @@ -7,7 +7,11 @@ opened: 2026-09-29 # A machine without an IOMMU refuses every claim; signed in-image drivers should claim there Owner ruling, 2026-09-29: on a machine with no IOMMU unit a driver signed and -shipped in the ToyOS image may claim a device, and no other may. +shipped in the ToyOS image may claim a device, and no other may. On +2026-11-05, answering #1191's open decisions, the owner confirmed it is +image-wide: any driver the signed image ships claims its device there with its +ordinary claim, and no `system.toml` row grants DMA without an IOMMU per +program. It is the same driver code as on a machine with a unit, never a second driver: the kernel's DMA layer hands it a physical address where there is no unit and a domain address where there is. A claim's space is an diff --git a/issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md b/issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md index afefd6d9888..a04b61768e5 100644 --- a/issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md +++ b/issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md @@ -37,9 +37,10 @@ the binary in a ToyOS guest is this track's harness's job, not gbae's. stay for crates, `rust/` and LLVM. Recipes and patches live in this repository, where `NOTICE` and the licence gate carry them, not in a ports repository. -- **The installer is an ordinary program**, `/system/bin/pkg`, with no - authority a shell does not have: it fetches, verifies, unpacks and writes - under `/apps` because `/apps` is writable to it, and it asks the user +- **The installer** is `/system/bin/pkg`. Who may write `/apps` (owner + ruling, 2026-11-05) is staged in stage 2 of + `issues/every-program-sees-only-the-files-it-was-given.md`. `pkg` + fetches, verifies, unpacks and commits, and it asks the user before it installs — at install, the moment the user typed the command, never at first run, so a refusal leaves nothing on disk and no answer has to be stored. `pkg install `, `pkg install `, `pkg remove @@ -97,19 +98,13 @@ The storage track's users and mount-protocol stages do not block this one. the session user's home, `/home/toy/Apps/`, which is its `HOME` and the one part of the home it sees; its own `/apps/` is read-only to it (`toyos_manifest::Program::view`). -6. **An app's rights are its request ∩ the user's grant ∩ the image's - ceiling** (owner ruling, 2026-09-24; the ceiling's shape, 2026-09-26). The - package's manifest *requests* rights; the user *grants* them per user - (from a user action — the file picker hands over one file — or asked on - first use for the risky ones, remembered and revocable; harmless ones such - as a window, sound and the app's own data directory are free); the image - holds the *ceiling*. **The ceiling names rights, never apps**: it is the - set any installed app may be granted at all, so installing an app never - edits the image and the image never lists an app by name. The `[apps]` - row of stage 1 becomes that ceiling. **Exit**: an app installed by `pkg` - from a release runs with exactly the intersection, a request beyond the - ceiling is refused by name, and no file under `/apps` can widen what it - gets. +6. **App rights** (owner ruling, 2026-11-05, superseding the 2026-09-24 + up-front request; the ceiling's shape, 2026-09-26). Built: the run-time + request path from an app to the user's grant, and the `[apps]` row of + stage 1 becoming the image's ceiling. **Exit**: an app installed by `pkg` + from a release starts holding only the free rights, a run-time request + beyond the ceiling is refused by name without asking the user, and no file + under `/apps` can widen what it gets. 7. **The apps leave this repository.** Each app (snake first, as the pilot: it builds unchanged for every OS) moves to its own repository, built with only the published SDK crates and the released toolchain, and published as diff --git a/issues/all-cores-are-assumed-equal-and-arm64-breaks-that.md b/issues/all-cores-are-assumed-equal-and-arm64-breaks-that.md index 734fff0246c..10c3f1f6683 100644 --- a/issues/all-cores-are-assumed-equal-and-arm64-breaks-that.md +++ b/issues/all-cores-are-assumed-equal-and-arm64-breaks-that.md @@ -19,8 +19,4 @@ task character (bursty/latency-sensitive work on big cores, throughput work on efficient ones); and eventually an energy model deciding when saving power beats finishing sooner (the EAS school on ARM). -What is already done so this does not have to be retrofitted: reservation -admission sums against **each CPU's own capacity** — a reservation is a fraction -of a specific CPU, and "all cores equal" is never baked into the arithmetic. -That one decision is the cheap half; this issue is the expensive half, and it -waits for ARM64. +It waits for ARM64. diff --git a/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md b/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md index cd75748385a..c2e003de575 100644 --- a/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md +++ b/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md @@ -49,7 +49,7 @@ The floor's need ends at stage 3 of `issues/the-loader-does-only-what-must-precede-the-handover.md`, which deletes `floor::Scope` and names every floor for its key alone, and the attempts record's at its stage 5, which replaces that record with the slot -table's tries. The black box's does not: stage 9 moves its check into the +table's good flag. The black box's does not: stage 9 moves its check into the kernel, still keyed on the log GUID. **Owner**: `issues/toyos-builds-itself.md`, whose M4 exits on a guest build diff --git a/issues/an-installed-gbae-browses-to-no-rom.md b/issues/an-installed-gbae-browses-to-no-rom.md index 8c489c94ed9..a9890c666bf 100644 --- a/issues/an-installed-gbae-browses-to-no-rom.md +++ b/issues/an-installed-gbae-browses-to-no-rom.md @@ -32,7 +32,7 @@ Evidence, in a QEMU guest on `tests/images/proctreecase.toml`, a package launche **Exit**: an installed gbae, started from the desktop, loads a ROM the user picked from outside its own folder. The designed answer is the file picker, which hands an app the one file the user chose: the package track's stage 6 -(an app's rights are its request, the user's grant and the image's ceiling), +(an app asks for a right at run time, within the image's ceiling), and the isolation track's "Sharing is granted, never reached" (`issues/every-program-sees-only-the-files-it-was-given.md`). Until then gbae plays only a ROM placed in its own folder and given on its command line. diff --git a/issues/cpu-time-is-a-band-and-not-a-reservation.md b/issues/cpu-time-is-a-band-and-not-a-reservation.md index 4289f0c0e11..2f7b8696295 100644 --- a/issues/cpu-time-is-a-band-and-not-a-reservation.md +++ b/issues/cpu-time-is-a-band-and-not-a-reservation.md @@ -1,102 +1,22 @@ --- -status: open -kind: track +status: none +kind: rejected opened: 2026-08-14 --- -# CPU time is a band, not a reservation, so nothing anyone is promised is priced +# CPU time is a band, not a reservation: reservations are rejected -Real-time work outranks normal work by right, fairness is a share of whatever is -left, and no entity is promised anything a number can check. Measured -consequence: **93.3 ms of audio starvation behind a fair storm**, in a 24-period -gap of 70 ms. Nothing below is built. +**Declined by the owner on 5 November 2026**, answering the open decisions of +#1191: ToyOS's scheduler is fair scheduling, EEVDF by job, with a per-CPU +real-time allowance. CPU time is not reserved. -**The commitment: CPU time is a reservation.** Every entity holds a budget and a -period, admission refuses a set that does not fit, and no runnable entity is -served below what admission priced. One invariant replaces four pairwise -derivations that each held for one workload shape. **There is no band whose -precedence is unbounded in either direction** — both absolutes were tried and -both starve, and neither is expressible under reservations. +The declined proposal made every entity hold a budget and a period: a per-CPU +ledger with admission arithmetic, real-time clients as earliest-deadline +reservation clients, a dying server per CPU, a reserved urgency mark, +reservation rows in the system manifest and a service invariant gated against +them. Its full text is in this file's history. -Exactly three entity kinds and no fourth: a real-time client, one fair class per -CPU, one dying server per CPU. A killed thread unwinding is the dying server's -work — not a right-holder and not a band. Interrupt and scheduling-pass time is -charged to a system reserve, to no entity. - -## The chunks, and the invariant each must preserve - -- **The reservation type, the per-CPU ledger, the admission arithmetic.** - Admission is integer, checked, rounds against the holder, and runs per CPU - against that CPU's own ledger, so overcommit cannot be admitted by a wrapped - multiply. -- **The dying server as an entity**, with its queue, demotion, replenishment, - timer discipline and preemption predicate. However many corpses a CPU holds, - they are one entity with one reservation, so no count of them can close the - rotation on anything. *Smaller than it was staged as: the corpse-aging cluster - it was to dismantle has already left the tree.* -- **Real-time clients become reservation clients.** Dispatch is earliest - deadline among entities holding budget, under a total tie-break, so a replay - of the same choices dispatches the same entity and no band outranks another by - right. -- **The urgency mark.** Precedence is bounded, totally ordered, and charged to - the marked thread's own class, so no party's service depends on another - party's behaviour. *Hard prerequisite: the simulator has no resource-holding - op, so the waiter/holder trigger cannot be staged at all until one exists.* -- **The manifest row, the parser refusals, init's endowment check, the build - gate.** A reservation is refused where it is created, by name, with the - arithmetic in the message, and never observed later as a latency. -- **The service invariant and its scenario matrix.** The law and the instrument - are one predicate, compared cumulatively rather than per period, and the - fraction of periods actually compared is gated alongside the verdict. -- **The kill path's counters, its report, and its one fixed-hop tripwire.** The - kill path asserts only over what its own CPU can observe and no workload can - scale; everything composed is reported. -- **Measurement: the audio gate, soundd's budget, the system reserve.** A budget - is twice the worst measured spend, and a measurement that will not fit is - escalated to the owner, never absorbed by moving the ceiling, the floor or the - reserve. - -## Decisions already made, and the numbers behind them - -- Capacity 1000 permille, system reserve 100 (**provisional and unmeasured** — - everything resting on it moves when it is measured), admissible 900. Fair - class 500 (5 ms per 10 ms), dying server 100 (1 ms per 10 ms), real-time - ceiling 300. soundd is 580 µs per 2,902,494 ns = **200 permille**; 800 - admitted, 100 slack. -- **Exhaustion demotes into a work-conserving background tier** — never a - throttle, never a silent extension. A reservation is a floor and never a cap. -- Replenishment happens only at a period boundary, on a grid anchored at - admission that nothing moves. A waking entity may spend at most its - utilisation times the time to its deadline. An overrun is charged back in full - at the next boundary, which is why both coordinates are floored at the - delivery granularity — 200,000 ns, which is 34.5 % of soundd's budget. -- **One urgency mechanism**: a mark, ordered ahead of unmarked threads inside - its own class only, spending its own class's budget, totally ordered by set - time, capped at half the class's budget, ending at 1 ms of running time. **No - budget moves anywhere**, so a mark inside one process buys that process - nothing. **Rejected, each falsified by an ordinary workload:** the lent band, - the wake grant, the budget donation, and every derived bound on a blocked - wait. -- The kill path asserts nothing of its own beyond one fixed-hop tripwire over - the kick, its delivery and the drain. **Rejected:** an arm-time queue-depth - snapshot (blind to in-flight siblings), a queue-occupancy assertion, and a - progress marker — the last measured at **14.4 ms between two legal bumps** on - a host port of the kernel's own allocator and ticket lock. -- Every sleep lock in the fixed set declares a hold bound, and a lock that - declares none may not be taken where a real-time client can block on it. The - filesystem lock can honestly declare nothing shorter than the 2 s device - timeout, **so no real-time path may take it** — true of soundd today. **The - process lock is the owner's open question**: soundd takes it every period, and - a holder inside a sync parks under the whole device chain. -- Governance, and it is the point: four concepts and no fifth, three layers and - two seams. **Every future feature enters as data or as policy, never as a new - mechanism.** Heterogeneous capacity, topology, EEVDF, hierarchical shares and - DVFS each cost one sentence and nothing until implemented. -- The manifest's period truncation is 0.11 ppm — one full period of slip per - 7.07 hours, against 21,638× headroom. 11 corpses is where the old rotation - closed on itself. - -**Two rules the current scheduler states as law that this design abolishes**, so -whoever lands it changes both deliberately: a ready real-time task always -preempting the normal band, and a real-time writer lending its band to a blocked -reader. Both are true of the tree today. +The scheduler's stages are `issues/the-scheduler-and-the-clocks-never-talk.md` +(owner decisions J1 and J2). The reservation model was to replace the dying +corpses' per-corpse deferral; that weakness is +`issues/k-dying-corpses-take-k-deferral-grants-on-one-cpu.md`. diff --git a/issues/every-program-sees-only-the-files-it-was-given.md b/issues/every-program-sees-only-the-files-it-was-given.md index 39e4176cf90..cfb1edc80c5 100644 --- a/issues/every-program-sees-only-the-files-it-was-given.md +++ b/issues/every-program-sees-only-the-files-it-was-given.md @@ -18,8 +18,9 @@ Rulings that bound the stages: who starts programs, "Restrict", grants as badges on a connection, "Adopt", and `/tmp` per session (owner, 2026-10-03); the local desktop counts as a login session, "Desktop counts" (owner, 2026-10-04), a premise that holds while only the compositor, terminal, shell, -toybox and sshserver rows list `starts` and no `/apps` row does. Whether the -desktop stays one once stage 3's local greeter exists is not ruled. +toybox and sshserver rows list `starts` and no `/apps` row does. Once stage +3's local greeter exists, the local desktop is a login session like any +other, with the same per-session isolation (owner, 2026-11-05). ## Stages @@ -66,14 +67,20 @@ desktop stays one once stage 3's local greeter exists is not ruled. of which each row holds only its own directory where it keeps state (`service = true` or `state = true`: the daemons' own state is built). `/apps` is included, so a shell and everything it starts can - rewrite an installed package; whether the installer alone writes `/apps` - is `issues/whether-pkg-alone-writes-apps.md`. + rewrite an installed package. Only the installer service writes `/apps` + (owner ruling, 2026-11-05): this stage gives that one row `/apps` + read-write and every other row, the shell included, + `/apps` read-only. Whether `pkg` becomes that service or its client is + this stage's brief. **Its exit**: a shell's write under `/apps` is refused + by the file server, and an install through the installer service still + commits; granting the shell's row `/apps` read-write reds the first. `/dev/null` is a name only libc's `open` knows, answered with a kernel null handle (`SYS_NULL`): no view holds it, so `stat` of it and a program that does not open through libc find nothing. Whether views name it is this stage's. 3. **Sessions and users.** `issues/a-user-is-a-home-tree-and-a-login-row.md` - on top of views: a login authority (sshd, and later a local greeter) holds + on top of views: a login authority (sshd, and later a local greeter, which + asks for the desktop's session the same way) holds a `login` right and asks init's `launcher` for a session, and init builds the session's view from the user's row: the user's home, `/system` read-only, and the session's own `/tmp`. The machine's SSH identity is sshd's own diff --git a/issues/k-dying-corpses-take-k-deferral-grants-on-one-cpu.md b/issues/k-dying-corpses-take-k-deferral-grants-on-one-cpu.md new file mode 100644 index 00000000000..e803e6fd87e --- /dev/null +++ b/issues/k-dying-corpses-take-k-deferral-grants-on-one-cpu.md @@ -0,0 +1,19 @@ +--- +status: open +kind: finding +opened: 2026-11-05 +--- + +# k dying corpses take k deferral grants on one CPU + +A dying task on a CPU defers real-time work for at most one +`DYING_CHUNK_NS` grant once it has aged `DYING_AGE_NS` +(`kernel/pure/sched/cpu.rs`, `CpuSched::dying`). The bound is per corpse and +not per CPU: k corpses on one CPU take k consecutive grants, and a real-time +band that briefly empties throws the accumulated age away on every dispatch. + +The planned exit was a per-CPU dying server holding a reservation, which the +owner's 2026-11-05 rejection of reservations +(`issues/cpu-time-is-a-band-and-not-a-reservation.md`) removed. No measurement +shows the effect on audio; the per-CPU real-time allowance of owner decision J2 +is the bound a replacement would work within. diff --git a/issues/placement-has-no-migration-cost-or-wake-affinity.md b/issues/placement-has-no-migration-cost-or-wake-affinity.md index ff8ec9b9705..6777f3ea7b3 100644 --- a/issues/placement-has-no-migration-cost-or-wake-affinity.md +++ b/issues/placement-has-no-migration-cost-or-wake-affinity.md @@ -44,7 +44,6 @@ lacks: with the CPU count. Derived from the code, not measured; both benches have at most 16 CPUs. -Composes with the reservation model (placement decides *where*, reservations -decide *how much*) and with the scheduler simulator (`kernel/sim`), which runs +Composes with the scheduler simulator (`kernel/sim`), which runs every scenario at the benches' scouted shapes and two permuted synthetic trees (`kernel/sim/src/topology.rs`). diff --git a/issues/the-fair-band-orders-by-share-alone-and-a-woken-editor-waits.md b/issues/the-fair-band-orders-by-share-alone-and-a-woken-editor-waits.md deleted file mode 100644 index da18121db7e..00000000000 --- a/issues/the-fair-band-orders-by-share-alone-and-a-woken-editor-waits.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: track -opened: 2026-08-16 ---- - -# The fair band orders by share alone, and a just-woken editor waits for a compiler - -Queued track from the 2026-08-16 owner conversation on scheduler direction; the -owner wants responsiveness over long-running compute, universally, not as an option. - -Today the fair band orders purely by share. The state of the art (EEVDF, Linux's -default since 6.6, and the BORE line of work) keys on one insight: interactive -tasks betray themselves behaviorally — they sleep long and run in short bursts — -and the fair band can order by *lag and virtual deadline* so a just-woken task -gets the next slice without ever receiving more total CPU. A compiler loses the -race for the next 10 ms, never its share of the hour. - -This is the intended occupant of the intra-fair policy seam: the reservation -layer is mechanism and is untouched; an intra-fair policy may reorder fair tasks -freely but cannot affect any reservation guarantee, and the simulator's -invariants gate the swap. One policy in the tree at any time — the seam exists so -the swap is a bounded, sim-gated change, never an A/B in the OS. - -Blocked on: the reservation layer being built. diff --git a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index 22378516bc3..17d35c79388 100644 --- a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -24,7 +24,7 @@ name is a `#[test]` in the named crate or a `METAL` row, and reverting the behav - host `root_chunk_refused`: an unreadable ROOT chunk is refused by name, never retried forever. - host `root_candidate_overlaps`: an overlapping ROOT partition is refused. - host `root_named_twice_on_the_boot_disk`, `root_named_twice`: a twin ROOT is never read. -- host `update_refusals_boot_the_other_slot`: every refused slot is refused by name and the other boots. +- host `update_refusals_are_named`: every refused slot is refused by name and no other slot boots. - host `update_grant_refuses_a_stray_partition`: init grants only the idle slot's partition. - host `update_floor_is_the_images_own`: the anti-rollback floor is the key's and the image's. - host `update_refused_pass_credits_no_image`: a refused pass credits nothing. @@ -45,8 +45,6 @@ name is a `#[test]` in the named crate or a `METAL` row, and reverting the behav - metal `shipped_config_boots`: `images/system.toml` boots to ready. - metal `diskless_boot`, `internal_disk_boot`: a boot with no NVMe, and one off the internal disk. - metal `update_boots_the_new_kernel`: an update over ssh is the kernel the next boot runs. -- metal `update_falls_back_from_a_dying_kernel`, `update_hang_kills_an_unproven_image`: a slot - that dies or hangs falls back, its death in the next boot's `/log`. - metal `foreign_disk_untouched`: a disk the system was not given comes back byte for byte; brings back `src/fingerprint.rs`. @@ -61,7 +59,7 @@ console's renderer. Exit: as stage A. `blackbox_early_panic_sealed_muted`, the T14's own shape), `blackbox_fault_sealed`: the report is sealed on the page and the next pass reads it. - metal `panic_outlives_the_deadline`: a panic crosses the deadline's reset as a panic report. -- metal `hang_bounded_by_the_stick`: a hanging image costs one boot and never traps the machine. +- metal `hang_bounded_by_the_stick`: a hanging `--once` image costs one boot, and the marked slot boots after it. - metal `double_fault_stack`, `syscall_panic_halts`, `syscall_fault_halts`, `heap_over_ceiling_halts`: each fatal path halts with its own report. - metal `reentry_names_the_first_panic`, `double_panic_names_the_fault`, diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index 838279f6739..28723f65726 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -21,7 +21,7 @@ PR #539 does not land. Its pieces: `update_boot_first_puts_the_loader_first`, which also takes `update_boot_next_boots_the_entry_once`'s read-only half; the harness's `stick_readonly`; and `update_trial_writes_nothing_of_the_kept_slot`, - rewritten for priorities; + rewritten for the good flag; - stage 6 takes the bench: `src/metalbench.rs`, `tests/common/bench.rs`, `tests/bench*case`, `--bench-image` with `build::bench_image`, `build/image.rs`' `update_of`, `tests/common/metal.rs`' `Reach`, `stage` and @@ -160,9 +160,9 @@ Each stage lands on its own, in this order. build. That prefix stays flat. After the word, `KernelArgs` is typed: `repr(C)` sub-structs, and `#[repr(C, u32)]` enums for the optional parts in place of `u32` presence flags and zero sentinels; a field the typing - changes moves after the word. The slot booted and why (the one the table - chose, or the other one after a refusal and its reason) and the black-box - page are fields, not `boot-slot=`, `slot-refused=` and `blackbox=` text. + changes moves after the word. The slot booted and the black-box page are + fields, not `boot-slot=` and `blackbox=` text; `slot-refused=` leaves with + the fallback. `params.rs`' last-one-wins and kernel `main.rs`' branch for a format never emitted go. `kernel_stack_addr` is renamed for the offset it holds. x86's `_start` reads by `const offset_of!`. @@ -195,27 +195,22 @@ Each stage lands on its own, in this order. `bar_placement_is_proven`, `root_withheld_refused`, `blackbox_*` and `update_*`. -5. **Tries and a good flag per slot, decided on the host.** Each slot in the - table carries a priority, the tries it has left and a good flag, in the - word the format reserves; the table's format rises. They replace the - attempts file (`attempt.rs`, `lib/update/src/record.rs` and `main.rs`' - accounting), the dead-slot retry and `slot::proven`. - - A freshly built image's slot A starts untried with 3 tries. An install - writes its slot untried with 3 tries, the good flag clear and a priority - above the kept slot's, whatever the slot held before. - - A pass boots the highest-priority slot that verifies and is good or has - tries left. A slot out of tries and never good is not booted again. +5. **A good flag per slot, decided on the host.** Each slot in the table + carries a good flag, in the word the format reserves; the table's format + rises. It replaces the attempts file (`attempt.rs`, `lib/update/src/record.rs` + and `main.rs`' accounting) and `slot::proven` as how an image is proven. + Which slot a pass boots is the marked one or none: updates fix forward, and + the loader's fallback leaves in stage 2 of + `issues/the-machine-updates-itself-without-ubuntu.md`, which this stage + follows. The good flag replaces the black-box page's `DONE` record as the + proof of an image; #1196 decides whether this stage must land with the + page's removal. + - An install writes its slot with the good flag clear, whatever the slot + held before. - Any failure after `verify` in `start_kernel` that the image causes is a - refusal of that slot, like a bad signature, and the pass falls to the - other slot. Stage 5's PR lists which `start_kernel` refusals count as the - image's — every `load_kernel_elf` refusal of the kernel's bytes — and - which count as the loader's own. - - The loader spends a try of an untried slot before it hands over. That - write, with sealing `ARMED`, is the pass's last, after every refusal - `start_kernel` can make, so a loader refusal is never booked as the - image's. Where the decrement cannot be persisted, that slot is not booted. - - Where no slot can boot, the loader says so on the panel and in - `loader.log`, and powers the machine off. + refusal of that slot, like a bad signature. Stage 5's PR lists which + `start_kernel` refusals count as the image's — every `load_kernel_elf` + refusal of the kernel's bytes — and which count as the loader's own. - The health gate: `system.toml`'s `[boot] up` names the services that make the image up. The tree has no readiness signal (a swap's probation only asks whether a process still runs), so this stage adds the smallest @@ -227,19 +222,15 @@ Each stage lands on its own, in this order. `update` holds the `slots` claim's table and writes the running slot's good flag and nothing else. - An image whose `system.toml` grants no program the `slots` claim is never - good, so each boot spends a try and it boots three times. Today only - `system.toml` and `tests/updatecase/system.toml` grant it. - - `update_refusals_boot_the_other_slot` boots slot A five times and - `update_grant_refuses_a_stray_partition` four. Both run `updatecase`, and - each boot waits for the good flag before the guest is dropped. Every - other test hands one image's kernel at most three times. + good. Today only `system.toml` and `tests/updatecase/system.toml` grant + it. - The floor rises on a pass that boots a good slot, to the security version of the signed header that pass verified, never to a version the table names. - A one-shot request (`update --once`, `update --boot-first`) is cleared and saved before it is acted on, and ignored where the save fails. - - `update --once` asks for the idle slot once and leaves it at priority 0, - so its good flag is never read. The kept slot boots after it, and no floor + - `update --once` asks for the idle slot once, without moving the mark, so + its good flag is never read. The marked slot boots after it, and no floor rises for it. `KernelArgs`' slot field gains the once variant. The kernel reads the field's discriminant as a raw `u32` and refuses one it does not name, so an unrecognized value is a refusal rather than an out-of-range @@ -257,56 +248,43 @@ Each stage lands on its own, in this order. The decisions are pure `toyos-update` functions: `verify(signed, kernel, cmdline, root, key, floor) -> Result` - and `pass::decide(inputs) -> Decision` over the table, the verified slots, - the floor, the request and whether the last write persisted. The loader does - the I/O and carries the decision out. + and `pass::decide(inputs) -> Decision` over the table, the verified marked + slot, the floor, the request and whether the request's clearing persisted. + The loader does the I/O and carries the decision out. **Exit**: `decide`'s host tests hold each rule above, and `verify`'s hold each refusal; deleting the cmdline hash check or `policy::admits` fails a - host test. The `update_*` tests and `hang_bounded_by_the_stick`, rewritten - for tries, pass. The hang tests boot an image that never reaches its health - gate, instead of killing QEMU on `LOADER_LAST_LINE`. #539's + host test. The `update_*` tests pass, and #539's `update_boot_first_puts_the_loader_first` passes, and so do its host tests of the entry rules that survive. Negative controls: - - Skipping the countdown fails `update_hang_kills_an_unproven_image`. - Raising the floor on any pass that boots a slot fails `update_floor_waits_for_good`. It installs into B an image at the running - security version + 1 whose kernel hangs, signed by - `tests/common/update.rs`' `Rig::update` at the version it is handed. B - spends its tries and A boots, and `fwvars::live` reads the floor at A's - version. - - An install that leaves the good flag set, or its priority below the kept - slot's, fails `update_over_a_good_slot_starts_it_untried`. It updates - into B and boots B to good, then updates over A, which was good, with a - kernel that hangs. The slot table read after each pass shows A's kernel - booted three times, then B boots. + security version + 1 whose `[boot] up` names a service that never writes + its byte; B boots, and `fwvars::live` reads the floor at A's version. + - An install that leaves the good flag set fails + `update_over_a_good_slot_clears_its_flag`, which updates over a slot that + was good and reads its flag clear in the slot table. - Raising the floor to the table's version fails `update_floor_is_the_headers`, which plants a table version above the header's and reads the floor at the header's. - - `let persisted = true;` in the loader fails - `update_readonly_stick_boots_nothing`. It boots a fresh image off a - read-only stick: nothing boots, the no-bootable-slot line is on the - console, and QEMU reports `guest-shutdown`. - Turning the refusal of an unloadable kernel back into a panic fails - `update_unloadable_kernel_boots_the_other_slot`, which updates B with a - signed non-ELF kernel, finds the loader's refusal of B naming a kernel - that is not an ELF, and finds A booted. + `update_unloadable_kernel_is_refused`, which updates B with a signed + non-ELF kernel and finds the loader's refusal of B naming a kernel that + is not an ELF. - Running `update --good` right after init spawns `[boot] start` fails `update_dead_service_is_never_good`. It updates B with an image whose `[boot] up` names a live service beside one that exits at start; the live - service writes its byte and the dead one never does. B boots three - times, its good flag never set because one named service never wrote, - and then A boots. + service writes its byte and the dead one never does, and B's good flag is + never set. - Acting on a request before taking it off the table fails `update_boot_first_puts_the_loader_first`. - Oracles: Android's A/B and Fuchsia libabr's rules, except that libabr boots - a slot whose decrement failed; the floor `fwvars::live` reads out of the - vars store; the slot table after each boot, decoded by the test from the - format's bytes and not by `toyos_update::slots::Table::decode`; and OVMF's - boot manager booting the entry the loader wrote. + Oracles: the floor `fwvars::live` reads out of the vars store; the slot + table after each boot, decoded by the test from the format's bytes and not + by `toyos_update::slots::Table::decode`; and OVMF's boot manager booting the + entry the loader wrote. 6. **The T14 bench.** - It opens with a timed `ssh t14 update < image` of a session image of @@ -347,9 +325,8 @@ Each stage lands on its own, in this order. goes by `--once` (`deadlinewedge`, `hardlockup`, `usbload` and `foreignrecord` among them), gives the verdicts a per-boot run gave at the commit this stage branches from; a kernel change boots; a slot with a - flipped byte, no signature or a lower security version is refused and the - other boots; a slot that dies falls back on its own, and one whose `sshd` - refuses the host's key is never marked good; a `--once` image that panics + flipped byte, no signature or a lower security version is refused by name; + a slot whose `sshd` refuses the host's key is never marked good; a `--once` image that panics returns the machine to its session with its record judged; and a loader sent over ssh boots once, and one that brings no slot to good leaves the old one booting. diff --git a/issues/the-machine-updates-itself-without-ubuntu.md b/issues/the-machine-updates-itself-without-ubuntu.md index a0332860f0f..79d5a771878 100644 --- a/issues/the-machine-updates-itself-without-ubuntu.md +++ b/issues/the-machine-updates-itself-without-ubuntu.md @@ -17,8 +17,7 @@ stdin — and the machine installs nothing the owner did not sign. Done: the signed image (`toyos-update`), the two slots and their table, and the loader's verification. The loader boots the marked slot and today the other where the marked one is refused or died on its last boot; the kernel -logs which and why. Whether that fallback stays, against the fix-forward -update policy, is before the owner. +logs which and why. That fallback leaves in stage 2. - **`update`** holds the slot table and the idle slot's two partitions, which init claims against the ROOT the kernel holds, writes the image and moves the @@ -29,7 +28,40 @@ update policy, is before the owner. (`--owner-key`, `--update-image`, minted by `--signing-key-new`) for an image installed on the owner's machine, whose loader keeps the machine's floor. -## Stage 2 — the T14 installs ToyOS on its NVMe +## Stage 2 — fix forward: the loader boots the marked slot or nothing + +Owner ruling, 2026-11-05, answering #1191's open decisions: updates fix +forward, with no automatic fallback to the previous slot. + +The loader falls back today in three ways, all of which go: + +- `policy::order` names the other slot after the marked one, and + `slot::choose` boots it when the marked one is refused; +- `Refusal::Died` refuses an image whose last boot the record says died or + hung, so the other slot boots instead; +- where no slot verifies but a dead one, `slot::choose` boots that one, + which may be the other slot. + +A pass verifies the marked slot alone and boots it whether or not its last +boot died. Where it is refused, the loader names the refusal on the panel and +in `loader.log` and powers the machine off. `slot-refused=` and the kernel's +line that logs it go with the fallback. A one-shot `update --once` is a +request, not a fallback, and stays the loader track's stage 5. + +With the fallback gone, marking an image dead in the boot record on the +black-box page no longer refuses anything, but the next boot still reports the +last boot's death; #1196 (the memory-overwrite request) decides what carries +that report if the page goes. This stage removes only the fallback's use of +the record. + +**Exit**: `policy`'s host tests hold that a pass names only the marked slot +and that a dead image in it is still booted. +`a_bent_supervisor_is_refused_and_the_other_slot_boots` and its metal row +`bent_supervisor_falls_back` become a bent marked supervisor refused by name, +nothing booted and the machine powered off; putting the other slot back into +`policy::order` reds them. + +## Stage 3 — the T14 installs ToyOS on its NVMe The machine boots the stick and installs onto its own NVMe, then takes `ssh t14 update < image` for every change after. It waits on stage 5 of diff --git a/issues/the-update-rig-the-guest-cut-deleted-is-still-cited.md b/issues/the-update-rig-the-guest-cut-deleted-is-still-cited.md index 11ce07ce5c3..75e90892148 100644 --- a/issues/the-update-rig-the-guest-cut-deleted-is-still-cited.md +++ b/issues/the-update-rig-the-guest-cut-deleted-is-still-cited.md @@ -20,7 +20,7 @@ six lines in one file, each planning or describing a test on them: What it should name is the tier its test has now, and stage A of `issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` gives -it: `update_floor_is_the_images_own`, `update_refusals_boot_the_other_slot` +it: `update_floor_is_the_images_own`, `update_refusals_are_named` and `update_grant_refuses_a_stray_partition` are host tests there and guest tests in the loader track, and the tree holds no `update_*` test today. diff --git a/issues/toyos-has-a-desktop.md b/issues/toyos-has-a-desktop.md index b572d223707..1896d64466f 100644 --- a/issues/toyos-has-a-desktop.md +++ b/issues/toyos-has-a-desktop.md @@ -78,8 +78,9 @@ until the Rust target is upstream. waits on the users track (`issues/a-user-is-a-home-tree-and-a-login-row.md`). 4. **Compositor services**: clipboard, drag and drop, notifications and - screenshots. Clipboard reads and screenshots are privileges granted under - the package track's request ∩ grant ∩ ceiling rule + screenshots. Clipboard reads and screenshots are privileges an app asks for + at run time, granted within the image's ceiling under the package track's + stage 6 (`issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md`). 5. **Fractional scaling**: `issues/the-desktop-draws-at-the-panels-scale.md`. 6. **Accessibility**: accesskit, keyboard-only navigation, high contrast. diff --git a/issues/whether-pkg-alone-writes-apps.md b/issues/whether-pkg-alone-writes-apps.md deleted file mode 100644 index 8f442509735..00000000000 --- a/issues/whether-pkg-alone-writes-apps.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: owner -kind: question -opened: 2026-10-09 ---- - -# Whether pkg alone writes /apps - -Two of the owner's rulings in -`issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md` -pull against each other once a package's own directory is read-only to it: - -- **"The installer is an ordinary program … with no authority a shell does - not have"**: `pkg` writes under `/apps` because `/apps` is writable to it. -- **"`/apps/` is immutable by stage-then-commit … nothing writes a - committed package"** (2026-10-02). - -Today every row the image declares, `pkg` and the shell among them, holds -`/apps` read-write (`toyos_manifest::whole_tree`), so a shell and everything -it starts can rewrite an installed package. An installed package itself -holds its own directory read-only (`toyos_manifest::Program::view`). - -## The question - -Does `pkg` alone write `/apps`, which gives the installer an authority a -shell lacks, or does every row that may start `pkg` keep `/apps` writable, -which leaves a committed package writable by the shell? - -## Exit condition - -The owner's answer. If `pkg` alone writes `/apps`, every other declared row's -view holds `/apps` read-only, which needs the per-row views of -`issues/every-program-sees-only-the-files-it-was-given.md` stage 2. diff --git a/kernel/pure/sched/cpu.rs b/kernel/pure/sched/cpu.rs index 07a92fad47e..4e55c872d12 100644 --- a/kernel/pure/sched/cpu.rs +++ b/kernel/pure/sched/cpu.rs @@ -270,15 +270,8 @@ pub struct CpuSched { /// task", and why "the pick serves `rq` first whenever the RT band is /// occupied" is false. /// - /// **Superseded in design by the reservation model** - /// (`issues/cpu-time-is-a-band-and-not-a-reservation.md`), which - /// deletes the age, the chunk and the grant together: the deferral - /// above is bounded per *corpse* and not per CPU, so k corpses take k - /// consecutive grants, and a real-time band that briefly empties throws the - /// accumulated age away on every dispatch. What replaces it is a per-CPU - /// dying server holding an ordinary reservation, whose guarantee does not - /// depend on how many corpses are behind it or on how the other band's - /// occupancy happens to fall. + /// The deferral is bounded per corpse, not per CPU + /// (`issues/k-dying-corpses-take-k-deferral-grants-on-one-cpu.md`). /// /// **A queue and not a stack.** Two concurrent process teardowns put two /// corpses on one CPU; popping the end `keep_dying` pushes to would