Skip to content

A spawn reserves its stack once, and a committed page's frame is written once - #1250

Draft
Japabu wants to merge 7 commits into
mainfrom
spawnperf
Draft

Japabu wants to merge 7 commits into
mainfrom
spawnperf

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Two local costs on every spawn, found by the spawn-stage diagnosis on both metal benches:

  • loader::spawn reserved the stack's guard and the stack as one range and then reserved the stack again over it. The
    second reservation replaced part of the first, so every spawn removed a range, cut another, and walked, to clear
    them, the 8 MiB of empty leaves it had just built tables for. Boot 2 of the diagnosis timed the stack step, hot
    p50, at 7.0 us on t14 and 7.4 us on amd, of a tiny program's 18.9 and 22.3 us SYS_SPAWN.
  • space::owned_frame zeroed a copied range's fresh frame and then copied the program's bytes over it: every byte a
    segment's first touch copies was written twice.

Behavior

  • The stack is placed once with its guard below it (AddressSpace::reserve_above_guard): two ranges side by side,
    nothing replaced, all or nothing. The guard is still reserved and unbacked, so running off the stack still faults.
  • A committed page's fresh frame is written once (owned_frame): zeros for a private page; for a copied one
    vm::copied_page works out how many bytes the page takes and from where, as far as the range's copy and the source
    both go, reads those and zeros the rest. A page that takes none, such as one wholly past its object's end, reads
    nothing from the source. The frame comes unzeroed (pmm::alloc_promised_unzeroed, which answers an address and is
    mm's own, so every Frame stays zeroed as its doc says). CopySource::read must write every byte it is asked for,
    which its trait states and SharedMemObject::read does or panics.
  • Nothing pins a page's frame: user copies stopped pinning when they went through the process's own mapping
    (b420e5c), and the only pins are a paged object's on its own frames, which no space owns. So discard frees and
    promises again every frame it takes, and a pinned frame discarded is an invariant broken (Frames::discard panics).
    Discarded, the kept arm, discard_owned on both architectures and populate's held-frame arm (now unreachable!:
    only a protection to None leaves a frame held, in a range that allows no access) go. Pins stay for paged objects.

Not done, and why

The diagnosis also listed the parent mapping the whole image to lay it out, where the layout reads only headers. That
is dropped: the mapping is one range populated at first touch, and layout touches the first page. Boot 2 bounds what
is left at a few microseconds (dropping the mapped osbench image costs 4.9 us on t14 and 8.9 us on amd, against 3.3
and 2.7 us for a one-page copy), and a header-only map needs another syscall and object, which could cost more than it
saves. The real cost on that path is the open of a /system file, which belongs to the file-call work.

Declined from review: making Kind::Copy hold an Arc<SharedMemObject> in place of CopySource. Mapped paged
objects' design (step 2 and after) replaces the trait with a source that is either kind of object, so it changes
there; and batching discard's frees under one hold is separate work.

Checks

Risks

  • A copied page's frame is no longer zeroed first, so CopySource::read leaving a byte unwritten would hand a process
    what the frame's last owner left. Its one implementation, SharedMemObject::read, copies every piece of the range or
    panics, and is now asked only for bytes inside the source.
  • A pinned frame discarded panics the kernel. Nothing on main or map7b-2 pins a page's frame; a later change that
    pins one has to deal with this.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1250 at daf84f4 (base main 45935f4)

Architecture

The responsibilities sit in the right places. The address space places the stack and its guard (AddressSpace::reserve_above_guard), and the loader names only the sizes. A committed page's bytes are written where the page is populated. The stack change is direct: two adjacent ranges and nothing replaced, so the remove, cut and clear walk over 8 MiB goes away. Writing a frame once is the right idea, but it introduces a kernel panic that userland can trigger (B1). The third commit also rebuilds a discard arm that no code reaches (B2).

Net change: production +67/−34 lines (about +33), tests +15 lines (one host test).

Findings

BLOCKER 1. kernel/src/mm/space.rs:875-878 with kernel/pure/vm.rs:40-44: a page of a copied range that lies past its source's end panics the kernel.
fill_as_new computes n = 0 for such a page, but copied_page still calls source.read(offset + at, &mut []). SharedMemObject::read then reaches each_piece, which asserts offset + len <= size (kernel/src/object/shm.rs:244-249). The removed copy_in returned early when n == 0.

Map::checked (kernel/src/loader/mod.rs:72-88) bounds offset + copied by the object's size. It does not bound the range's length. Example: a one-page object and SpawnMap { len: 3 pages, offset: 0, copied: 0 or 100 }. The first touch of page 2, or a 64 KiB run fill that includes it, calls read(0x2000, []) on a 0x1000-byte object, and the kernel panics.

An honest ELF hits the same path when its RW PT_LOAD reaches two or more pages past the file's page-rounded end, that is, when .bss is larger than what follows the segment in the file. sdk/toyos/src/elf/program.rs:97-127 builds exactly that kind of range. abuse_spawn_maps.rs:84-86 names this assert as the panic that a copy past the object must never reach.

  • Consequence: userland input panics the kernel.
  • Fix: call read only when n > 0. The cleanest version moves the computation of n (from at, offset, len and the source's size) into the pure function, which then skips read when n is zero.
  • Check: a host case for a page wholly past the source's end: read is not called and the page is all zeros. No guest row is needed.

BLOCKER 2. space.rs:525-529, :563-570, :800-801, :866-869: the Kept arm cannot be reached, and the PR rebuilds it around a pin that no code takes.
Since b420e5c ("user copies through the process's own mapping"), user copies pin nothing. pmm::pin_range has two callers, kernel/src/object/paged.rs:593 and :817, and both pin a paged object's own frames. Paged objects are never mapped (see the paged.rs header). A pinned frame's free waits for its last unpin, so it never becomes a space's owned frame. Therefore:

  • pmm::discard never answers Kept for a space's frame.
  • populate's Leaf::Held arm is never reached either: only a kept frame leaves a held leaf in an accessible range.
  • These statements are false:
    • discard's doc: "one a user copy pins … a copy pins under this space's lock".
    • fill_as_new's Safety section: "nothing reaches meanwhile but a user copy that pins the page".
    • The Kept arm's SAFETY comment and populate's Held comment.
    • The PR body: "a frame a discard keeps because a copy pins it" and "race-dependent and no test reaches it".

daf84f4 adds code to this path: a second caller of fill_as_new, a new panic, and a &mut [u8] over memory that the function's own contract says another CPU may be writing. It also documents a race that does not exist.

  • Fix: treat Kept from a space's discard as a kernel invariant violation, with one panic saying that nothing pins a space's frame, and delete the false text. fill_as_new then has one caller and folds back into owned_frame, whose comment ("no leaf maps it yet, so nothing else reaches it") is true.
  • Preferred end state: also delete the rest of the dead machinery here: Discarded::Kept, the pin check in Frames::discard and its test in frames.rs, and populate's Held arm. Record in issues/ whatever you leave.

On the question asked: the new "outside every range" panic is a real invariant. held() refuses with BadAddress unless the inventory covers every page, discard_owned only yields addresses in [start, end), and everything runs under one &mut hold. It sits in the unreachable arm, though, so B2 supersedes it.

BLOCKER 3. kernel/src/mm/pmm.rs:53-54 and :310-314: Frame's doc ("zeroed when it was allocated") is false for the Frame that alloc_promised_unzeroed returns.
That function is public and safe, so the whole kernel can call it. Return the raw physical address instead, as alloc_held does for frames that still hold their last owner's bytes (pmm.rs:329-336), and make it pub(super). Every Frame then stays zeroed. In the same edit, promise's doc (pmm.rs:264-266) still names alloc_promised as the function that draws the frame.

SUGGESTION. CopySource has had one implementation since tmpfs left the kernel (4f2efdb), and space.rs already names SharedMemObject. Using Arc<SharedMemObject> in Kind::Copy would make the every-byte guarantee a property of the code rather than of a trait doc. This predates the PR, so it is optional.

SUGGESTION. Once Kept is gone, discard can free each table's batch under one hold of the allocator's lock, as clear does, instead of one hold per frame. That is the lock #1265 measures. Separate work.

Evidence

  • Host checks. At daf84f4, vm:: ran 16 tests, all passed. Clippy for x86, x86 with the actuator features, aarch64 and the kernel library exited 0 each (checks-pr-3.log, read).
  • Negative control. The mutation exited 101 at 5c61b1c, and kernel/pure is identical at daf84f4. The test covers copied_page's zero tail. It does not cover the computation of n or the CopySource contract, which is where B1 lies.
  • Guest rows. Run 43052895137 succeeded at head f3a8c40, 4 of 4 (checked with gh run view). The rebased diff's changed lines equal f3a8c40's (compared). Between 34cb4c9 and 45935f4, main also landed Every kernel stack sits above unmapped memory, and an overflow names its stack #1211 (kernel stacks: loader/start.rs, mm/stack.rs, both architectures' paging.rs), not only A spawn's path is one line, and no judge finds a child by it #1238. Neither touches user page population or the user stack's reservation, so reusing the run stands; the PR body should name Every kernel stack sits above unmapped memory, and an overflow names its stack #1211 as well. The B1–B3 fixes need a compile and the host tests, not a guest rerun.
  • A/B, one boot a side. Enough for the stack-stage claim:
    • The stage is timed by marks around exactly the changed call, with 60 hot repetitions for each of five program kinds.
    • The A side has already reproduced twice. Minimum on the T14: 6,416 ns in boot 1 against 6,395 ns in boot 2. p50 on both benches is 6.7–7.4 µs hot, except AMD large/tmpstd at about 9.3 µs.
    • "Mostly goes" is several times that spread and must show up in all five kinds independently.
    • It is not enough for request4's further claims ("a copied segment's faults cost less", "nothing else in a spawn moves"). The child-run and fault rows vary between boots by more than the expected effect (T14 std child run after idle: p50 378 µs in boot 1 against 127 µs in boot 2). Report those claims as unresolved unless they exceed that spread.
    • The fixes leave the A/B valid: the stack path is untouched, B1 adds one branch for pages with no copied bytes, and B2 removes code that never runs.
  • A futex wait translates its word once and checks it with one load #1253. git merge-tree of daf84f4 and bb5ae53 exits 0 with no conflict. A futex wait translates its word once and checks it with one load #1253 changes only discard's shootdown call (to bump its counter), not the kept arm, and there is no semantic interaction. "Whichever lands second resolves the kept-frame arm" is inaccurate.

PR body corrections (for the orchestrator):

SEND BACK. It becomes LAND-pending-metal once B1–B3 are resolved.

Japabu added a commit that referenced this pull request Nov 11, 2026
…ever kept

Review of #1250, round 1.

A copied range's page that takes no bytes, such as one wholly past its
object's end, still called `CopySource::read` with an empty slice from
past the object, whose bound panics the kernel: a spawn's range longer
than its object reaches it, and so does a program whose .bss runs two
or more pages past its file. `vm::copied_page` now works out how many
bytes a page takes, from where, and reads only when there are some.
Host tests cover pages of none, some and all, a source ending inside
the page and a page wholly past it; `abuse_spawn_maps` spawns a child
with such a range and reads zeros there.

`AddressSpace::discard` kept a frame a user copy pinned, but user copies
stopped pinning when they went through the process's own mapping, and
a pin is taken only on a paged object's frames, which no space owns.
A pinned frame discarded is now an invariant broken (`Frames::discard`
panics), and `Discarded`, the kept arm, `discard_owned` on both
architectures, `fill_as_new` and populate's held-frame arm go:
`owned_frame` writes a fresh frame's every byte again.
`pmm::alloc_promised_unzeroed` answers an address and is the module's
own, so every `Frame` is zeroed as its doc says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2 of #1250 at 1d151cb (focused: the round-1 blockers and the code that changed)

Architecture

This round deletes the keep-a-pinned-frame machinery instead of rebuilding it:

  • Discarded, both architectures' discard_owned, fill_as_new and populate's held-frame arm are gone.
  • discard now uses take_owned, as clear does.
  • copied_page now computes all of its bytes itself, so the empty read cannot happen at all; it is not merely guarded at one call site.

Line counts:

  • This round: +128/−193.
  • The whole PR against 45935f4: +160/−177 over 8 files. Most of the deletions are main's dead kept-frame path.

Round-1 blockers

  • B1 resolved. kernel/pure/vm.rs:43-51 sets n = min(copied − at, page, size − from) and calls read only when n > 0. Every read therefore ends at or before size.
    • The guest case reaches the original panic (by inspection). Page 2 at PAST_AT lies at object offset size + PAGE. Its neighbours are either empty or outside the range, so the touch is a lone one. The old code called read(size + 0x1000, []) there, which each_piece's bound refuses.
    • The parent's range passes Map::checked. The object's size is page-rounded (shm::pages), so offset: size - PAGE, copied: PAGE is within bounds.
    • The host control is real. With round1/mutation-read-empty.patch applied, both vm tests fail with exit 101 (round1/checks-2.log, rev 1d151cb).
  • B2 resolved.
    • Pins. At this head, pin_range's only callers are paged.rs:593 and :817. Current main (4280c31) has the same two, and both pin a paged object's own frames.
    • map7b-2. At 2503ec7 and 386dff5, a paged run is filled with owned: false (space.rs:892, :905). take_owned collects only Held or owned: true leaves, and discard's edit empties unowned leaves. So no pager frame reaches pmm::discard.
    • The unreachable! held arm. Only protect(None) leaves a Held leaf (space.rs:509), and protect(Some) maps it again (:511). clear and discard empty theirs through take_owned under the same &mut. Populate returns before it reads the leaf for a None range (allows, leaf_prot(..)?).
    • No stale text. Searching kernel, sdk, docs and issues finds no Discarded, discard_owned, fill_as_new, "copy pins" or "user cop".
  • B3 resolved. alloc_promised_unzeroed (pmm.rs:312) returns u64 and is pub(super). Its one caller is owned_frame, and promise's doc names it.

Findings

BLOCKER 1. kernel/src/mm/pmm.rs:136: free_drawn's Safety section still requires "a frame [Frame::into_raw] gave up".

  • Problem: since this round, a space's frames are drawn by alloc_promised_unzeroed as raw addresses and never become a Frame. clear (space.rs:740) and Drop (:856) pass exactly those frames to free_drawn. alloc_promised_unzeroed's own doc (pmm.rs:311) also sends its caller there.
  • Consequence: the unsafe function's stated precondition is one its main callers cannot meet. B3's fix moved the false statement here.
  • Fix: state the requirement instead of a provenance, for example: "Each is a frame drawn for a committed page of job's, which no leaf on any CPU maps any more and nothing else frees." alloc_promised_unzeroed's "gives it back through [free_drawn]" also leaves out discard; drop the clause or name both.
  • Check: this is comment-only, so no check is needed.

SUGGESTION. vm.rs:418: the separate test models an input that is refused. a_page_wholly_past_its_source_reads_nothing_from_it passes copied = 3 * PAGE with a one-page source, which Map::checked refuses (offset + copied <= size).

  • The case its doc describes, a range longer than its object with an honest copy, is (2 * PAGE, 0, ≤ PAGE, PAGE). There n is already zero because of copied.
  • The table's (PAGE, 0, 100, 4 * PAGE) row exercises that same branch, and the mutation fails both tests.
  • Put a realistic row such as (2 * PAGE, 0, 100, PAGE, 0) in the table and drop the separate test.

SUGGESTION. copied_page's size bound never applies. Given Map::checked, copied ≤ size − offset, so the bound never binds. Where that invariant is broken, the bound zero-fills quietly instead of failing. Stating offset + copied <= source.size() as AddressSpace::copy's contract (or asserting it there) would remove the parameter. This predates the PR, so it is optional.

SUGGESTION. frames.rs:12: "a page's frame is never pinned" is ambiguous. A paged object's frame is also a page's frame. "An address space's frame" says which one is meant.

Evidence

  • Host checks. round1/checks-2.log at 1d151cb:
    • vm::: 17 passed, exit 0. frames::: 39 passed, exit 0.
    • The mutation exits 101 and is reverted.
    • Library clippy exits 0.
    • Clippy for the bare targets (x86, x86 with the actuator features, aarch64) exited 0 at 2a9e60755. That revision differs only in a test module's import, so the result stands.
  • Guest. Run 43080189189 (abuse_spawn_maps at 1d151cb) is still queued (gh run view). Under the orchestrator's rule, it also has to pass before the A side of A draw of many promised frames takes the allocator's lock once #1268's metal A/B is flashed.
  • Metal. Round 1's A/B (request4) stays valid: the stack path is untouched.
  • Main. git merge-tree against main 4280c31 exits 0. Since 45935f4, main has changed no mm, frames, vm, shm or paging file (only x86_64/pio.rs) and added no pin.

SEND BACK for BLOCKER 1, a doc-only fix. Once it is fixed, this is LAND pending guest run 43080189189 and the metal A/B.

Japabu added a commit that referenced this pull request Nov 11, 2026
…as a range the loader takes

`free_drawn`'s Safety named where a frame came from (`Frame::into_raw`),
which a space's frames, drawn by address, never meet; it now states the
requirement. `alloc_promised_unzeroed`'s frames also go back through
`discard`. "A page's frame is never pinned" says an address space's.
The page wholly past its object is a row of the copied-page table, with a
copy the loader accepts, in place of a test of one it refuses; every row
is now a range `Map::checked` takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
The benches are down, so #1250's A/B is owed rather than run: the spawn
record names its rows, benches and what main must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3 of #1250 at 8245c9d (focused on round 2's points)

Architecture

Nothing structural changed. This round touches comments, one assert's message and the copied-page test table: +16/−21 lines in 3 files.

Round-2 points

  • BLOCKER 1 resolved. kernel/src/mm/pmm.rs:136-137: free_drawn's Safety now states a requirement instead of where the frame came from. All four callers meet it:
    • space.rs:740 (clear) and :856 (Drop) free a space's frames, drawn by alloc_promised_unzeroed for committed pages.
    • paged.rs:140 and :1099 free a pager's frames. Those were drawn against pmm::promise(core.job(), …) (paged.rs:237, :695), which is a commitment of the pager's job.
    • alloc_promised_unzeroed's doc (:311) names both free_drawn and discard. [discard] resolves to pmm::discard (:296).
  • Suggestion taken: "an address space's frame". The phrase now appears in the module header (frames.rs:12), discard's doc and assert (:728-732) and the test doc (:1467). The only remaining "page's frame" in the kernel is paged.rs:897, which means a pager's page and is correct there.
  • Suggestion taken: the table row. Every row of a_copied_page_keeps_nothing_its_frame_held (vm.rs:399-407) now has offset + copied <= size. Map::checked requires that (loader/mod.rs:84-86), and the loader is AddressSpace::copy's only caller.
    • The new row (2 * PAGE, 0, 100, PAGE, 0) is the realistic page wholly past the object.
    • The changed row. "Source ends inside a page" was (PAGE, 0, 3 * PAGE, PAGE + 50), a copy that Map::checked refuses. It is now (PAGE, 0, PAGE + 50, PAGE + 50, 50), which it accepts.
    • The mutation still fails the test. The read-empty patch fails it with exit 101 (r2/checks.log, rev 8245c9d). The new row would also catch it, because it expects asked == None.

Findings

SUGGESTION. kernel/pure/vm.rs:45: no row now tests the size term.

  • Before this round, the old row 3 was the only row where size alone limited n.
  • In all six current rows, copied − at limits n at least as tightly. If .min(size.saturating_sub(from)) were deleted, n = min(copied − at, PAGE) would equal every row's expected n (by inspection).
  • This follows from round 2's second suggestion: under Map::checked, with a source whose size is fixed (SharedMemObject::size), the term cannot limit anything.
  • Round 2's optional fix still applies: state or assert offset + copied <= source.size() at AddressSpace::copy and drop the parameter. The term predates this PR, so this does not block.

Evidence

  • Host, implementer. r2/checks.log at 8245c9d:
    • vm:: 16 passed and frames:: 39 passed, exit 0 each.
    • The mutation exits 101 and is reverted.
    • Library clippy exits 0.
  • Host, this review. review-r3/checks-r3.log at 8245c9d, compiled from this tree:
    • vm:: 16 passed and frames:: 39 passed, exit 0 each.
    • cargo clippy -p kernel --lib --tests --features sched-check with the adopted lints and -D warnings exits 0.
  • Bare-target clippy. x86, x86 with the actuator features, and aarch64 exited 0 at 2a9e60755. That revision differs from 8245c9d only in tests, comments and Frames::discard's assert message (checked with git diff -U0), so the result stands.
  • Guest. Run 43080189189 (abuse_spawn_maps) is still queued at 1d151cb.
    • guest-rows.yml checks out github.sha, so the run tests 1d151cb.
    • 1d151cb differs from 8245c9d only in comments, #[cfg(test)] code and one assert's message, and no harness reads that message.
    • No row needs rerunning. Landing waits for this run to pass.
  • Metal. Under the benches-down ruling, metal is not a landing condition.

Since the specified head

spawnperf has moved to 8be9cc4, adding one records-only commit, which I checked for consistency:

LAND, once guest run 43080189189 passes.

Japabu added a commit that referenced this pull request Nov 11, 2026
… lock

Two touches of a hole both drew before racing to install, so the loser drew
against a promise its page did not hold, and the machine's promised count
fell short of the promises outstanding until it gave the frame back. Each
region now has a first-touch lock (`sync::Lock`): a touch takes an installed
frame with one Acquire load, and a hole is drawn by the touch that, holding
the lock, still finds it a hole, and installed with a Release store. Every
draw is the page's own promise's; `discard` keeps #1250's caller and Safety
text. The lock order is space, region, allocator, and nothing else takes the
region's lock. The loom model asserts one draw a page, and
`slot-install-relaxed` still reds the publication.

The batched draw of an object run's holes is recorded without condition,
owed by the object fault-around (#1259): whichever of #1259 and this lands
second draws a run's holes in one hold. The free batch is a superframe's
worth by name, and the promise doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…ever kept

Review of #1250, round 1.

A copied range's page that takes no bytes, such as one wholly past its
object's end, still called `CopySource::read` with an empty slice from
past the object, whose bound panics the kernel: a spawn's range longer
than its object reaches it, and so does a program whose .bss runs two
or more pages past its file. `vm::copied_page` now works out how many
bytes a page takes, from where, and reads only when there are some.
Host tests cover pages of none, some and all, a source ending inside
the page and a page wholly past it; `abuse_spawn_maps` spawns a child
with such a range and reads zeros there.

`AddressSpace::discard` kept a frame a user copy pinned, but user copies
stopped pinning when they went through the process's own mapping, and
a pin is taken only on a paged object's frames, which no space owns.
A pinned frame discarded is now an invariant broken (`Frames::discard`
panics), and `Discarded`, the kept arm, `discard_owned` on both
architectures, `fill_as_new` and populate's held-frame arm go:
`owned_frame` writes a fresh frame's every byte again.
`pmm::alloc_promised_unzeroed` answers an address and is the module's
own, so every `Frame` is zeroed as its doc says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…as a range the loader takes

`free_drawn`'s Safety named where a frame came from (`Frame::into_raw`),
which a space's frames, drawn by address, never meet; it now states the
requirement. `alloc_promised_unzeroed`'s frames also go back through
`discard`. "A page's frame is never pinned" says an address space's.
The page wholly past its object is a row of the copied-page table, with a
copy the loader accepts, in place of a test of one it refuses; every row
is now a range `Map::checked` takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
The benches are down, so #1250's A/B is owed rather than run: the spawn
record names its rows, benches and what main must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
… lock

Two touches of a hole both drew before racing to install, so the loser drew
against a promise its page did not hold, and the machine's promised count
fell short of the promises outstanding until it gave the frame back. Each
region now has a first-touch lock (`sync::Lock`): a touch takes an installed
frame with one Acquire load, and a hole is drawn by the touch that, holding
the lock, still finds it a hole, and installed with a Release store. Every
draw is the page's own promise's; `discard` keeps #1250's caller and Safety
text. The lock order is space, region, allocator, and nothing else takes the
region's lock. The loom model asserts one draw a page, and
`slot-install-relaxed` still reds the publication.

The batched draw of an object run's holes is recorded without condition,
owed by the object fault-around (#1259): whichever of #1259 and this lands
second draws a run's holes in one hold. The free batch is a superframe's
worth by name, and the promise doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1250's rebase onto main 8188095, at 4e9f6f2 (focused on the resolution)

Architecture

Nothing structural changed since round 3's LAND. The rebase only resolves conflicts. The PR's diff against main is +166/−179 lines over 9 files, the same as before apart from hunk context.

The resolution

Findings

None.

Note for the orchestrator, not blocking: the body says 43080189189 "named a shared-boot member and failed before running". It was cancelled while queued and has no steps. The run that failed on the shared-boot member is #1268's 43081124750.

Evidence

  • Host, this review, at 4e9f6f2 in its own detached worktree and target directory (checks-1250.log):
    • vm:: 16 passed and frames:: 39 passed, exit 0 each.
    • Clippy with the adopted lints and -D warnings exits 0 for:
      • the library's tests with sched-check;
      • x86_64-unknown-none, with and without boot-actuators,test-actuators;
      • aarch64-unknown-none-softfloat, with and without boot-actuators,test-actuators.
    • The implementer's rebase-checks.log reports the same counts and exits at this revision.
  • Guest. Run 43112778072 is still queued.
    • It runs at 4e9f6f2 (headSha), on x86 a_spawn_from_an_image_frees_it_when_the_child_ends and osbench_capability_groups_exit_0, and on aarch64 virt_user_mode and virt_smp.
    • These rows exercise object-range run fills (the parent's image mapping) together with copied and private runs on both architectures, which is what the resolution combines.
  • Metal. Not a landing condition while the benches are down. What is owed is recorded.

LAND, once guest run 43112778072 passes.

Japabu added a commit that referenced this pull request Nov 11, 2026
…ever kept

Review of #1250, round 1.

A copied range's page that takes no bytes, such as one wholly past its
object's end, still called `CopySource::read` with an empty slice from
past the object, whose bound panics the kernel: a spawn's range longer
than its object reaches it, and so does a program whose .bss runs two
or more pages past its file. `vm::copied_page` now works out how many
bytes a page takes, from where, and reads only when there are some.
Host tests cover pages of none, some and all, a source ending inside
the page and a page wholly past it; `abuse_spawn_maps` spawns a child
with such a range and reads zeros there.

`AddressSpace::discard` kept a frame a user copy pinned, but user copies
stopped pinning when they went through the process's own mapping, and
a pin is taken only on a paged object's frames, which no space owns.
A pinned frame discarded is now an invariant broken (`Frames::discard`
panics), and `Discarded`, the kept arm, `discard_owned` on both
architectures, `fill_as_new` and populate's held-frame arm go:
`owned_frame` writes a fresh frame's every byte again.
`pmm::alloc_promised_unzeroed` answers an address and is the module's
own, so every `Frame` is zeroed as its doc says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…as a range the loader takes

`free_drawn`'s Safety named where a frame came from (`Frame::into_raw`),
which a space's frames, drawn by address, never meet; it now states the
requirement. `alloc_promised_unzeroed`'s frames also go back through
`discard`. "A page's frame is never pinned" says an address space's.
The page wholly past its object is a row of the copied-page table, with a
copy the loader accepts, in place of a test of one it refuses; every row
is now a range `Map::checked` takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
The benches are down, so #1250's A/B is owed rather than run: the spawn
record names its rows, benches and what main must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
… lock

Two touches of a hole both drew before racing to install, so the loser drew
against a promise its page did not hold, and the machine's promised count
fell short of the promises outstanding until it gave the frame back. Each
region now has a first-touch lock (`sync::Lock`): a touch takes an installed
frame with one Acquire load, and a hole is drawn by the touch that, holding
the lock, still finds it a hole, and installed with a Release store. Every
draw is the page's own promise's; `discard` keeps #1250's caller and Safety
text. The lock order is space, region, allocator, and nothing else takes the
region's lock. The loom model asserts one draw a page, and
`slot-install-relaxed` still reds the publication.

The batched draw of an object run's holes is recorded without condition,
owed by the object fault-around (#1259): whichever of #1259 and this lands
second draws a run's holes in one hold. The free batch is a superframe's
worth by name, and the promise doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…ever kept

Review of #1250, round 1.

A copied range's page that takes no bytes, such as one wholly past its
object's end, still called `CopySource::read` with an empty slice from
past the object, whose bound panics the kernel: a spawn's range longer
than its object reaches it, and so does a program whose .bss runs two
or more pages past its file. `vm::copied_page` now works out how many
bytes a page takes, from where, and reads only when there are some.
Host tests cover pages of none, some and all, a source ending inside
the page and a page wholly past it; `abuse_spawn_maps` spawns a child
with such a range and reads zeros there.

`AddressSpace::discard` kept a frame a user copy pinned, but user copies
stopped pinning when they went through the process's own mapping, and
a pin is taken only on a paged object's frames, which no space owns.
A pinned frame discarded is now an invariant broken (`Frames::discard`
panics), and `Discarded`, the kept arm, `discard_owned` on both
architectures, `fill_as_new` and populate's held-frame arm go:
`owned_frame` writes a fresh frame's every byte again.
`pmm::alloc_promised_unzeroed` answers an address and is the module's
own, so every `Frame` is zeroed as its doc says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…as a range the loader takes

`free_drawn`'s Safety named where a frame came from (`Frame::into_raw`),
which a space's frames, drawn by address, never meet; it now states the
requirement. `alloc_promised_unzeroed`'s frames also go back through
`discard`. "A page's frame is never pinned" says an address space's.
The page wholly past its object is a row of the copied-page table, with a
copy the loader accepts, in place of a test of one it refuses; every row
is now a range `Map::checked` takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
The benches are down, so #1250's A/B is owed rather than run: the spawn
record names its rows, benches and what main must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
… lock

Two touches of a hole both drew before racing to install, so the loser drew
against a promise its page did not hold, and the machine's promised count
fell short of the promises outstanding until it gave the frame back. Each
region now has a first-touch lock (`sync::Lock`): a touch takes an installed
frame with one Acquire load, and a hole is drawn by the touch that, holding
the lock, still finds it a hole, and installed with a Release store. Every
draw is the page's own promise's; `discard` keeps #1250's caller and Safety
text. The lock order is space, region, allocator, and nothing else takes the
region's lock. The loom model asserts one draw a page, and
`slot-install-relaxed` still reds the publication.

The batched draw of an object run's holes is recorded without condition,
owed by the object fault-around (#1259): whichever of #1259 and this lands
second draws a run's holes in one hold. The free batch is a superframe's
worth by name, and the promise doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…ever kept

Review of #1250, round 1.

A copied range's page that takes no bytes, such as one wholly past its
object's end, still called `CopySource::read` with an empty slice from
past the object, whose bound panics the kernel: a spawn's range longer
than its object reaches it, and so does a program whose .bss runs two
or more pages past its file. `vm::copied_page` now works out how many
bytes a page takes, from where, and reads only when there are some.
Host tests cover pages of none, some and all, a source ending inside
the page and a page wholly past it; `abuse_spawn_maps` spawns a child
with such a range and reads zeros there.

`AddressSpace::discard` kept a frame a user copy pinned, but user copies
stopped pinning when they went through the process's own mapping, and
a pin is taken only on a paged object's frames, which no space owns.
A pinned frame discarded is now an invariant broken (`Frames::discard`
panics), and `Discarded`, the kept arm, `discard_owned` on both
architectures, `fill_as_new` and populate's held-frame arm go:
`owned_frame` writes a fresh frame's every byte again.
`pmm::alloc_promised_unzeroed` answers an address and is the module's
own, so every `Frame` is zeroed as its doc says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…as a range the loader takes

`free_drawn`'s Safety named where a frame came from (`Frame::into_raw`),
which a space's frames, drawn by address, never meet; it now states the
requirement. `alloc_promised_unzeroed`'s frames also go back through
`discard`. "A page's frame is never pinned" says an address space's.
The page wholly past its object is a row of the copied-page table, with a
copy the loader accepts, in place of a test of one it refuses; every row
is now a range `Map::checked` takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
The benches are down, so #1250's A/B is owed rather than run: the spawn
record names its rows, benches and what main must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
… lock

Two touches of a hole both drew before racing to install, so the loser drew
against a promise its page did not hold, and the machine's promised count
fell short of the promises outstanding until it gave the frame back. Each
region now has a first-touch lock (`sync::Lock`): a touch takes an installed
frame with one Acquire load, and a hole is drawn by the touch that, holding
the lock, still finds it a hole, and installed with a Release store. Every
draw is the page's own promise's; `discard` keeps #1250's caller and Safety
text. The lock order is space, region, allocator, and nothing else takes the
region's lock. The loom model asserts one draw a page, and
`slot-install-relaxed` still reds the publication.

The batched draw of an object run's holes is recorded without condition,
owed by the object fault-around (#1259): whichever of #1259 and this lands
second draws a run's holes in one hold. The free batch is a superframe's
worth by name, and the promise doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator note. Run 43135806312 at 9d9d5ee passed all 4 rows. That head is code-identical to db23484, since the rebase changed only a comment, and the body now says so. #1268's 43135817474 and #1270's 43135829383 are green too.

Since #1271, the testcases and shared-debug boots are guest rows. All three PRs of this stack change spawn and memory paths that most shared-boot members exercise, so before landing I dispatched the shared rows once at the stack's top, #1270's head 5a372f4, which contains all three: run 43163538956 (testcases_members_exit_0 shared_debug_members_exit_0).

Two testcases members, abuse_listener_hijack and adopted_services_stay, are red on main 302fb36 until #1280 lands, and they are independent of this stack. Every other member is expected to pass. The stack lands in order #1250, #1268, #1270 once that run is judged.

Japabu and others added 7 commits November 12, 2026 09:00
`loader::spawn` reserved the guard and the stack as one range and then
reserved the stack again over it, so every spawn removed a range, cut
another and walked, to clear them, the 8 MiB of empty leaves it had just
built tables for. The
stack is now placed once with its guard (`AddressSpace::reserve_above_guard`):
two ranges side by side, nothing replaced. Metal boot 2 timed the stack
stage at 6.4-7.4 us of a tiny program's 17-22 us SYS_SPAWN on both benches.

`space::owned_frame` zeroed a copied range's frame and then copied the
program's bytes over it. It now takes the frame as its last owner left it
(`pmm::alloc_promised_unzeroed`) and writes each byte once
(`vm::copied_page`): the copied bytes, and zeros only past them.
`CopySource::read` writes every byte it is asked for, so nothing the frame
held survives; the host test checks that for none, some and all of a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copied range's page now takes its frame unzeroed, so `CopySource::read`
leaving any byte of `out` unwritten would hand the process what the frame's
last owner left there. The trait states it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`AddressSpace::discard` kept a frame a user copy pins and zeroed it in
place, then copied a copied range's source bytes over it through
`copy_in`, which the previous commit removed while this caller still named
it: the kernel did not build. Both a fresh frame (`owned_frame`) and a
kept one now go through `fill_as_new`, which writes every byte of the
frame once as its range starts the page: zeros for a private page, the
source's bytes and zeros past them (`vm::copied_page`) for a copied one.
A fresh private page's frame is zeroed there instead of in `pmm`, the same
writes. A kept frame outside every range panics: `discard` checked that
its range is covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ever kept

Review of #1250, round 1.

A copied range's page that takes no bytes, such as one wholly past its
object's end, still called `CopySource::read` with an empty slice from
past the object, whose bound panics the kernel: a spawn's range longer
than its object reaches it, and so does a program whose .bss runs two
or more pages past its file. `vm::copied_page` now works out how many
bytes a page takes, from where, and reads only when there are some.
Host tests cover pages of none, some and all, a source ending inside
the page and a page wholly past it; `abuse_spawn_maps` spawns a child
with such a range and reads zeros there.

`AddressSpace::discard` kept a frame a user copy pinned, but user copies
stopped pinning when they went through the process's own mapping, and
a pin is taken only on a paged object's frames, which no space owns.
A pinned frame discarded is now an invariant broken (`Frames::discard`
panics), and `Discarded`, the kept arm, `discard_owned` on both
architectures, `fill_as_new` and populate's held-frame arm go:
`owned_frame` writes a fresh frame's every byte again.
`pmm::alloc_promised_unzeroed` answers an address and is the module's
own, so every `Frame` is zeroed as its doc says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as a range the loader takes

`free_drawn`'s Safety named where a frame came from (`Frame::into_raw`),
which a space's frames, drawn by address, never meet; it now states the
requirement. `alloc_promised_unzeroed`'s frames also go back through
`discard`. "A page's frame is never pinned" says an address space's.
The page wholly past its object is a row of the copied-page table, with a
copy the loader accepts, in place of a test of one it refuses; every row
is now a range `Map::checked` takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The benches are down, so #1250's A/B is owed rather than run: the spawn
record names its rows, benches and what main must show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The past-the-object case pushed a `SpawnMap` without `backing`, a field
the ABI gained with #1259 after the case was written; it is a copy range
(`SpawnBacking::Copy`), as every range this test builds by hand is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
… lock

Two touches of a hole both drew before racing to install, so the loser drew
against a promise its page did not hold, and the machine's promised count
fell short of the promises outstanding until it gave the frame back. Each
region now has a first-touch lock (`sync::Lock`): a touch takes an installed
frame with one Acquire load, and a hole is drawn by the touch that, holding
the lock, still finds it a hole, and installed with a Release store. Every
draw is the page's own promise's; `discard` keeps #1250's caller and Safety
text. The lock order is space, region, allocator, and nothing else takes the
region's lock. The loom model asserts one draw a page, and
`slot-install-relaxed` still reds the publication.

The batched draw of an object run's holes is recorded without condition,
owed by the object fault-around (#1259): whichever of #1259 and this lands
second draws a run's holes in one hold. The free batch is a superframe's
worth by name, and the promise doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant