feat(frame): an occupant of a place is handed a core, and finds a stack four pages wide - #48
Merged
Merged
Conversation
…uling
Increment 6 of E1-B05 is the join `kernel/src/runtime.rs` has described since
RFC 0033: *there, a component is spawned into a place and never scheduled; here,
a component is scheduled and never spawned into one.* This is the decision the
code needs before the code.
**The obvious join is a double free, and this refuses it.** The plan said to make
an `Instance` produce a `process::Prepared`, since the scheduled path already
knows how to run one of those. But `reap` returns `Prepared::pages` to the
`FrameAllocator` and checks the free count recorded in `Prepared::before`, while
an occupant's pages were derived from an `Untyped` and are owed back to that
account by `tear_down`. An `Instance::into_prepared` would be a value whose whole
purpose is to be passed to a function that must never see it, and nothing in the
type system would say so. It would also quietly falsify RFC 0008's sentence that
revoking the account ends the component.
**What the code actually needs turned out to be much smaller.** `smp::run_on`
does not take a process. It publishes a `process::Job` into a per-CPU slot and
the target core reads it — `{ root, entry, stack, argument, hz, target }`, six
fields, every one of which an `Instance` has or trivially knows. The scheduling
interface was already narrow; what was wide was the assumption that running
something meant owning it the way `prepare_runtime` owns it.
So: `Job` is the one scheduling interface, `Prepared` and `Instance` are two
ownership records that differ in who is owed the memory, and both can build a
`Job` while neither may become the other.
Two alternatives are recorded with why they lost: an account-aware `reap` (one
name for two return disciplines, where `Failure::Leaked` exists precisely because
the frame checks what it built against what it gave back), and a fourth `Plan`
beside the three that exist (the three differ by how a run is *judged*, and an
occupant has no verdict yet — a plan minted now would be shaped by its first
caller).
The plan's increment 6 is corrected in the same commit rather than left standing:
its file list named `kernel/src/runtime.rs`, which must **not** be touched —
that module's comment is `HEAP_GAP`'s needle, so editing it would turn the
constant red without the boot having changed, which is a red build for a false
reason. And it put `CHAOS_GAP` in this increment's gate, which was wrong:
that needle is `prepare_driver(` and closes when a *driver* is scheduled inside
its place, which this makes possible and does not do.
The gate is one constant going red on purpose. `HEAP_GAP`'s needle is the
sentence this increment falsifies, so the check written two increments ago is the
acceptance test for this one.
`cargo xtask lint` is 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ck four pages wide
RFC 0075's code. `kernel/src/runtime.rs` has described this since RFC 0033 —
*there, a component is spawned into a place and never scheduled; here, a
component is scheduled and never spawned into one* — and the first half of that
sentence is now false. The supervisor's occupant enters ring 3 under its own
address space on every boot.
**It dies on its first instruction, and that is the finding rather than a
regression.**
occupant killed: exception 14 at 0x0000000000410ff8, error 0x6, rip 0x0000000000400000
`0x415000 - 0x410ff8` is `0x4008`: the whole four-page stack plus eight bytes, a
frame's stack probe walking down and stepping one word past the end. The guard
page caught it exactly as designed — the fault is at `SPAWN_GUARD` and not in
somebody else's memory. Nothing had ever executed through the spawn path before
today (`user/init` is a flat image on another path, a runtime comes from
`prepare_runtime`), so `SPAWN_STACK_PAGES` had never been tested by anything but
arithmetic.
Raising it is a cross-crate move and not a constant. Four is pinned by three
compile-time assertions to constants owned elsewhere — `SPAWN_HEAP` against
`f_ring::heap::AT`, `BLK_BOARD` against the block and network drivers'
`routing::AT` — so `4 -> 8` does not build. The tree refusing a layout change
made in one place is the tree working. That is written down beside the constant
with the fault line, so the next reader meets it where it matters.
**Two things RFC 0075 did not anticipate, both found by running it.**
*Writing `Job` alone is not scheduling.* A core entering ring 3 runs against five
per-core shards, and a component given only a job runs against whatever the
previous occupant left behind. `State`, `IN_RING3`, the four `arm_entries`
counters and `Outcome` are now written beside it.
*The capability table is per-core, not per-instance.* `cap::of(cpu)` is what a
running component's calls resolve against; an `Instance` holds its own only while
dormant. Importing one built elsewhere breaks the generation arithmetic
`f_abi::door::Entry::granted` rests on, and it broke it in exactly the way
`prepare_runtime`'s comment predicts — `init process core 1, 3 call(s) answered,
1 refused, ended with status 5`. So the excursion is made invisible: the core's
previous table is restored afterwards, and only the occupant's own keeps what it
derived. A sibling session had reported two boots of one unchanged binary
disagreeing; `cargo xtask trace` is green now, and that non-determinism was this.
The boot reports how the occupant ended rather than asserting it, because a boot
that hands over a core and says only that it did so cannot tell *the component
ran* from *a core was told to run it* — which is the difference three wrong
diagnoses of `peak 0 byte(s)` turned on.
**The gate is not met and the plan is not ticked.** `HEAP_GAP`'s needle is still
in the tree and `peak` is still zero, because the component dies before it
allocates. What closes it is the stack, and that is its own diff.
`cargo xtask verify` is 0 — `verify: all green`. `cargo xtask trace` agrees on
the pair that must agree and disagrees on the control that must not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dicted The mechanism landed and the gate did not. The plan said increment 6 ends with `HEAP_GAP` going red; it ends with an occupant entering ring 3 and dying on its first instruction against a four-page stack, so the needle is still there and `peak` is still zero. A plan that only ever says what it intended is a plan nobody can use to find out what it cost. This says which half landed, what the fault was, and that the remaining half is a cross-crate constant move rather than a line in this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Increment 6 of
E1-B05: the decision, the mechanism, and the wall the mechanism found.Read the last section before the code if you read nothing else — the gate this increment was written against is not met, deliberately, and the branch says so rather than rounding up.
What this is
kernel/src/runtime.rshas carried this sentence since RFC 0033:The first half is now false. The supervisor's occupant is handed a core and enters ring 3 under its own address space on every boot. That is the first time any occupant of a place has executed.
RFC 0075 — the decision, made before the code
op::SPAWN/op::STOPlanded in #44; this is the other half of the join. The plan said to make anInstanceproduce aprocess::Prepared, since the scheduled path already knows how to run one of those.That is a double free.
reapreturnsPrepared::pagesto theFrameAllocatorand checks them against the free count recorded inPrepared::before. An occupant's pages were derived from one suppliedUntypedand are owed back to that account bytear_down. AnInstance::into_preparedwould be a value whose only purpose is to be handed to a function that must never see it, with nothing in the type system saying so — and it would quietly falsify RFC 0008's sentence that revoking the account ends the component.What the code actually needs turned out to be much smaller:
smp::run_ondoes not take a process. It publishes aJob { root, entry, stack, argument, hz, target }into a per-CPU slot. Six fields, every one of which anInstancehas.So: one scheduling interface, two ownership records.
Jobis what a core needs;PreparedandInstancediffer in who is owed the memory; both build aJob, neither becomes the other. Two alternatives are recorded with why they lost — an account-awarereap(one name for two return disciplines, whereFailure::Leakedexists precisely because the frame checks what it built against what it gave back) and a fourthPlan(the three that exist differ by how a run is judged, and an occupant has no verdict yet).Two things the RFC did not anticipate, both found by running it
Writing
Jobalone is not scheduling. A core entering ring 3 runs against five per-core shards. A component given only a job runs against whatever the previous occupant left behind — which is how the first version handed the supervisor a core and watched it not execute.State,IN_RING3, the fourarm_entriescounters andOutcomeare written beside it now.The capability table is per-core, not per-instance.
cap::of(cpu)is what a running component's calls resolve against; anInstanceholds its own only while dormant. Importing a table built elsewhere breaks the generation arithmeticf_abi::door::Entry::grantedrests on, and it broke it in exactly the wayprepare_runtime's comment predicts:So the excursion is made invisible: the core's previous table is restored afterwards, and only the occupant's own keeps what it derived.
cap::Table's comment warns that a table copied by value is a second authority that can drift from the first — this is a swap, never two live copies, and the restore is what keeps it one.A sibling session had reported two boots of one unchanged binary disagreeing — green once,
status 5the next. That was this.cargo xtask traceno longer reproduces it.The wall: a four-page stack
The occupant dies on its first instruction, and this is the finding rather than a regression.
0x415000 - 0x410ff8is0x4008— the whole four-page stack plus eight bytes, a frame's stack probe walking down and stepping one word past the end. The guard page caught it exactly as designed: the fault is atSPAWN_GUARD, not in somebody else's memory.Nothing had ever executed through the spawn path before this branch.
user/initis a flat image on another path; a runtime comes fromprepare_runtime. SoSPAWN_STACK_PAGES = 4had never been tested by anything except arithmetic.Raising it is a cross-crate move and not a constant. Every spawn address above the stack derives from it, and three compile-time assertions pin them to constants owned elsewhere —
SPAWN_HEAPagainstf_ring::heap::AT,BLK_BOARDagainst the block and network drivers'routing::AT.4 -> 8does not build:The tree refusing a layout change made in one place is the tree working. That finding is written beside the constant with the fault line, so the next reader meets it where it matters rather than in this description.
What a reviewer should know
The gate is not met and nothing here claims it is.
HEAP_GAP's needle is still inkernel/src/runtime.rsand the boot still reportspeak 0 byte(s), because the component dies before it allocates.intent/0005-the-datapath/plan-e1-b05.mdrecords that rather than the outcome it predicted — a plan that only says what it intended is one nobody can use to find out what it cost.The boot reports how the occupant ended rather than asserting it. A boot that hands over a core and says only that it did so cannot tell the component ran from a core was told to run it — which is the difference three wrong diagnoses of
peak 0turned on. Hence theoccupant killed: …line naming exception, address and rip.CHAOS_GAPis untouched. Its needle isprepare_driver(and it closes when a driver is scheduled inside its place, which this makes possible and does not do.E1-P06's remaining half stays open.Evidence
cargo xtask verify—verify: all green, exit 0cargo xtask trace— agrees on the pair that must agree, disagrees on the mutated control that must notcargo xtask run— reachesM0 ok;init process core 1, 4 call(s) answered, 0 refused, ended with status 0Three failures the gate found before this was committed, each a real defect in my own diff: a
SAFETY:comment orphaned from itsunsafeblock by an inserted line — twice, and an orphaned safety comment reads as discharged when nothing checked it, which is why the lint treats it as absent — and aneedless_borrow.🤖 Generated with Claude Code