Repository navigation
Conversation
Review of #1250 at daf84f4 (base main 45935f4)ArchitectureThe responsibilities sit in the right places. The address space places the stack and its guard ( Net change: production +67/−34 lines (about +33), tests +15 lines (one host test). FindingsBLOCKER 1.
An honest ELF hits the same path when its RW
BLOCKER 2.
daf84f4 adds code to this path: a second caller of
On the question asked: the new "outside every range" panic is a real invariant. BLOCKER 3. SUGGESTION. SUGGESTION. Once Kept is gone, Evidence
PR body corrections (for the orchestrator):
SEND BACK. It becomes LAND-pending-metal once B1–B3 are resolved. |
…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>
Review round 2 of #1250 at 1d151cb (focused: the round-1 blockers and the code that changed)ArchitectureThis round deletes the keep-a-pinned-frame machinery instead of rebuilding it:
Line counts:
Round-1 blockers
FindingsBLOCKER 1.
SUGGESTION.
SUGGESTION. SUGGESTION. Evidence
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. |
…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>
Review round 3 of #1250 at 8245c9d (focused on round 2's points)ArchitectureNothing 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
FindingsSUGGESTION.
Evidence
Since the specified head
LAND, once guest run 43080189189 passes. |
… 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>
…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>
… 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>
Review of #1250's rebase onto main 8188095, at 4e9f6f2 (focused on the resolution)ArchitectureNothing 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
FindingsNone. 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
LAND, once guest run 43112778072 passes. |
…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>
… 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>
…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>
… 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>
…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>
… 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>
|
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 Two |
`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>
… 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>
Problem
Two local costs on every spawn, found by the spawn-stage diagnosis on both metal benches:
loader::spawnreserved the stack's guard and the stack as one range and then reserved the stack again over it. Thesecond 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_framezeroed a copied range's fresh frame and then copied the program's bytes over it: every byte asegment's first touch copies was written twice.
Behavior
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.
owned_frame): zeros for a private page; for a copied onevm::copied_pageworks out how many bytes the page takes and from where, as far as the range's copy and the sourceboth 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 ismm's own, so everyFramestays zeroed as its doc says).CopySource::readmust write every byte it is asked for,which its trait states and
SharedMemObject::readdoes or panics.(b420e5c), and the only pins are a paged object's on its own frames, which no space owns. So
discardfrees andpromises again every frame it takes, and a pinned frame discarded is an invariant broken (
Frames::discardpanics).Discarded, the kept arm,discard_ownedon both architectures andpopulate's held-frame arm (nowunreachable!:only a protection to
Noneleaves 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
osbenchimage costs 4.9 us on t14 and 8.9 us on amd, against 3.3and 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
/systemfile, which belongs to the file-call work.Declined from review: making
Kind::Copyhold anArc<SharedMemObject>in place ofCopySource. Mapped pagedobjects' 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
Landing evidence: one run at the stack's top, 43173093523 at A shared-memory region draws each page's frame at its first touch #1270's head e37586c (which contains this PR's
head), pending:
testcases_members_exit_0andshared_debug_members_exit_0(every shared member,abuse_spawn_mapsamong them) and
a_spawn_from_an_image_frees_it_when_the_child_ends,osbench_capability_groups_exit_0,virt_user_mode,virt_smp. The earlier row runs no longer cover the landed heads.Rebased onto main 29c5f2d (2026-11-12), with A spawn reserves its stack once, and a committed page's frame is written once #1250's member fix e343eb5:
abuse_spawn_maps's past-the-objectrange names
backing: SpawnBacking::Copy, the field A memory object's range is mapped where the caller says, and an immutable one is shared and executable: /system text is ROOT's own pages #1259 added after the case was written (no earlier runcompiled it: it is a shared member). At each head the testcases image builds,
vm::andframes::pass and thekernel's clippy shapes exit 0; every guest member compiles (
rebase5-checks.login the evidence).Rebased onto main 3c5c555 (after A switch loads only what the incoming thread does not already have #1251 and The shared boots run as guest rows, each member judged by its own exit #1271, 2026-11-12) at db23484: comment-only. A switch loads only what the incoming thread does not already have #1251 added "no CPU holds
its root" to the doc on
impl Drop for AddressSpace, which A spawn reserves its stack once, and a committed page's frame is written once #1250 rewrote; both stand. Every other commit isunchanged (
rebase4-range-diff.txtin the evidence).Rebased onto main a7ca8ad (after An unmap walks no futex bucket while no thread of its space is in a futex wait #1223 and A poll belongs to the handle it was made through, and a ring to its one submitter #1261, 2026-11-12) at 9d9d5ee. An unmap walks no futex bucket while no thread of its space is in a futex wait #1223 gave
sched::futex::revoke_framesthe space's wait count,so
discard's call composes asrevoke_frames(&self.futex, frames); the range-diff shows that one line and noother change. At 9d9d5ee:
vm::andframes::exit 0, kernel clippy for
x86_64-unknown-none, withboot-actuators,test-actuators, foraarch64-unknown-none-softfloatand the library's tests, exit 0 each (rebase3-*in the evidence).Rebased onto main 75bdec3 (after A memory object's range is mapped where the caller says, and an immutable one is shared and executable: /system text is ROOT's own pages #1259, 2026-11-12). The one conflict was
kernel/pure/vm.rs's tests: A memory object's range is mapped where the caller says, and an immutable one is shared and executable: /system text is ROOT's own pages #1259'san_object_maps_as_what_it_isand this stack's copied-page tests both stay. At 308e990:vm::andframes::exit 0,kernel clippy for
x86_64-unknown-none, withboot-actuators,test-actuators, foraarch64-unknown-none-softfloatand the library's tests, exit 0 each.
Targeted, per the owner's testing policy: the loader's stack placement,
kernel/src/mm(space, pmm),kernel/pure(vm,frames), both architectures'paging(a deletion) and one guest test. The merge queue runs thefull host checks.
At 8245c9d (review r2: doc fixes, and the past-the-object page as a table row the loader would accept, in place
of a separate test of a copy it refuses):
cargo test -p kernel --lib --features sched-check vm::exit 0 (16passed) and
frames::exit 0 (39 passed),a_copied_page_keeps_nothing_its_frame_held(none, some and all of apage, a source ending inside it, a page past the copy, a page wholly past the object) and
a_pinned_frame_discarded_is_a_panicamong them. Clippy of the kernel library's tests withsched-check, adoptedlints and
-D warnings: exit 0.Negative control, same run: a patch that reads the source even for a page that takes no bytes builds, and the table
test fails, exit 101 (its first such row asks for
Some((0x1000, 0))); reverted in the run. (The earlier control,dropping the zeros past the copy, failed the same test's zero-tail assertion, which is unchanged.)
At 2a9e60755 (which differs from 8245c9d only in test modules, comments and an assert's message): clippy with the adopted lints and
-D warningsforthe kernel on
x86_64-unknown-none, the same withboot-actuators,test-actuators, and onaarch64-unknown-none-softfloat, exit 0 each; and the guest test binaries build (abuse_spawn_mapswith its newcase), by staging the
spawn_costrow's image, which touches no machine.Rebased onto main 8188095 (2026-11-12) at 4e9f6f2. Conflicts:
kernel/src/mm/space.rs, where main's populatenow computes a run before matching the range's kind and fills an object range's run too (A paged read takes two passes of its pager's lock and populates nothing ahead #1245); the resolution
keeps main's shape with this PR's held-frame arm (
unreachable!); and the spawn record, where issues: the all-CPU build waits on the frame allocator's lock, for the 2 MiB windows #1265's paragraphsand this PR's owed bullet both stay. At 4e9f6f2:
vm::exit 0 (16 passed),frames::exit 0 (39 passed), kernelclippy for
x86_64-unknown-none, withboot-actuators,test-actuators, foraarch64-unknown-none-softfloatand thelibrary's tests, exit 0 each.
Guest rows, run 43135806312 at 9d9d5ee (code-identical to db23484, which a comment-only rebase made; the old head is kept on
hold-*), pending: x86a_spawn_from_an_image_frees_it_when_the_child_endsandosbench_capability_groups_exit_0, aarch64virt_user_modeandvirt_smp. (43080189189 named a shared-boot memberand failed before running; 43110109115 at the pre-rebase head was cancelled unrun.) Run 43052895137 at f3a8c40
passed those 4 on the code before review.
abuse_spawn_mapsnow spawns a child with a writable copy range whose last page lies wholly past its object andreads zeros there; before this PR that touch panicked the kernel (by inspection: the empty read starts a page past
the object's size, which
SharedMemObject's bound refuses). It is a shared-boot member, so it runs on theshared-boot guest row (The shared boots run as guest rows, each member judged by its own exit #1271) once that lands, on main.
Map7b's step 2 (branch
map7b-2, at 2503ec7 and 386dff5): a paged range's leaves map the pager's frames withowned: false, and its pins stay on the pager's frames, so no space's discard meets a pinned frame there either.That branch still carries the kept arm and its comments, so whichever lands second deletes them.
Evidence (every round's logs and mutation patches, the boot-2 lines and the script that tabulates them):
https://github.com/ToyOSOrg/evidence/tree/409861f5297b5004b4c8d16f27051f1898281bed/spawnperf
Metal: not run. The benches are down, so this lands on review, the host checks and the guest rows, and the
speed-up is not yet measured on metal. Owed on metal, recorded in the spawn record: an A/B of main before and after
this on the T14 and the AMD bench, of
spawn_costandosbench procs, where main after must showproc.spawn_wait.tinyandproc.posix_spawn_wait.tinyp50 lower by about the stack stage removed (7.0 and 7.4 us)and no
procrow slower beyond the before run's spread. The staged images are deleted; their request stays as therecord of what was owed.
Risks
CopySource::readleaving a byte unwritten would hand a processwhat the frame's last owner left. Its one implementation,
SharedMemObject::read, copies every piece of the range orpanics, and is now asked only for bytes inside the source.
map7b-2pins a page's frame; a later change thatpins one has to deal with this.
🤖 Generated with Claude Code