Skip to content

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

Merged
Japabu merged 6 commits into
mainfrom
map7b
Nov 11, 2026
Merged

Japabu merged 6 commits into
mainfrom
map7b

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A running /system program's read-only segments were copied out of ROOT's boot pages, a page at each first touch, so every process of one binary held its own copy of its text and paid a fault and a 4 KiB copy per page. No memory object could be mapped executable, and shm_map mapped only a whole object, anywhere. This is memory track stage 5 for /system and step 1 of mapped paged objects (7b), whose design the orchestrator approved.

Change

  • ABI. vm_map(object, offset, len, prot, at) (SYS 106, arguments in VmMapArgs) replaces shm_map: whole pages of a memory object, anywhere or at a fixed place, as vm_reserve places. SpawnMap gains backing (SpawnBacking::Copy or Share; 56 bytes), and kimage FORMAT 2 carries it for the supervisor.
  • The rule. One pure rule, kernel::vm::Shared::maps, decides what a space may map of a memory object: executable only where nobody can write it (a boot image or part of one), writable only where somebody can, a device's window whole. SharedMemObject::mappable applies it to vm_map and to a spawn's shared range; the handle's rights still bound the protection (MAP, and WRITE for a writable mapping).
  • Spawn and libraries. toyos::elf::program::lay_out shares a segment the program cannot write whose bytes are its file's own pages with nothing past them, from an image nobody can write (Source::Immutable), and copies the rest. toyos::dl maps such a library segment from its immutable image and copies only the rest; a segment inside the window relocations write is never mapped.
  • Faults. An object range is populated as main already populates it since A paged read takes two passes of its pager's lock and populates nothing ahead #1245 (kernel::vm::around): a lone touch maps its one page, and a touch beside a populated page maps its aligned 64 KiB run. This PR adds nothing to the fault path; its own hunk was dropped in the rebase.
  • Behaviour change. A shared segment's mapping is never widened, so a program can no longer mprotect its own read-only segment writable. A page shared until its first write needs 7b's write faults (step 5); recorded in the memory track record.
  • Manual. Memory (requirements: shared memory executed only where nobody can write it; a range of an object mapped at a requested address; read-only segments held once) and the interface page (a child's ranges are an object's pages or a private copy).
  • Records. The memory track's stage 5 is built for an image nobody can write; stage 6 records the orchestrator's decision that 7b's step 3 maps images from every pager-backed volume; the 7b steps are listed. root-in-memory-costs-its-whole-size now says a /system program's read-only pages are ROOT's own; its exit (the T14's boot line, and the used memory of 255 held children read again) stays owed.
  • Tests. vm_map_shares (a range lands at its fixed place, a writable region is never executed, an image is executed and never written, a program's read-only segments take no private frame and fault a store): its x86-64 verdict is the shared metal boot's; virt_vm_map_shares runs it on AArch64's job case, where no other row maps a shared page executable. It is declared in DRIVEN_AND_SHARED.

Size at the head 3baf239, measured with git diff --numstat against the merge base b566931: +820 / -254 over 39 files. Kernel +204 / -80 (of it the rule's host test, +39), ABI +86 / -30, SDK +265 / -86 (of it host tests, +134 / -45), guest tests and harness +198 / -19, build and kimage +10 / -6 (kimage's test +5 / -2), records +44 / -25, manual +13 / -8.

Checks

Targeted under the owner's testing policy; the full --ci host because the change touches the build system (kimage) and the harness. The head 3baf239 is c991bfd plus review r1's comment, doc and record changes, which no check below can see. c991bfd is b29fb2b plus one commit that moves three guest test sites to lay_out's Source (Image::source); no host check builds those binaries, which is why only the metal staging found them, so every host result at b29fb2b holds for the head.

Host, on macOS arm64:

  • cargo test -p kernel --lib vm::, cargo test -p toyos --test elf_program, cargo test -p toyos --lib dl::, cargo test -p toyos-kimage: exit 0 each. an_object_maps_as_what_it_is holds the rule; a_segment_that_is_its_files_pages_is_mapped_and_reads_as_its_copy and a_segment_a_copy_alone_holds_is_never_mapped hold dl's mapping. Run in the step 2 worktree, whose sdk/, lib/kimage/ and kernel/pure/vm.rs are byte-identical to b29fb2b (git diff b29fb2b2d HEAD -- sdk lib/kimage kernel/pure/vm.rs is empty), while this branch's own tree was staging images.
  • Controls, each a checked patch on the same sources: making ReadExec mappable from a writable object turns an_object_maps_as_what_it_is red; dropping dl's relocation-window check turns a_segment_a_copy_alone_holds_is_never_mapped red. Exit 101 each, restored identical.
  • cargo test --test toyos-checks -- checks::suite_split at b29fb2b: exit 0 (it was red before vm_map_shares joined DRIVEN_AND_SHARED).
  • cargo run -- --ci host at b29fb2b: exit 1, 109 of 110 steps green, clippy and the kernel's models with their controls among them. The one red is llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key (and keyed_and_built, the child it re-runs), red on every macOS host and recorded in issues/the-llvm-environment-test-breaks-git-on-macos.md; this branch touches nothing it reads.
  • The image and every x86-64 guest test binary at c991bfd were built by a metal staging (exit 2, staged); the shared boots were staged again at 470bf6f. Neither ran: the benches are down.

Guest: run 43062438931 at c991bfd (virt_vm_map_shares virt_user_mode bar_map_again bar_map_behind_a_bridge lent_device_memory_refused a_spawn_from_an_image_frees_it_when_the_child_ends libc_children a_drives_row_holds_one_process_per_function session_lifecycle) was green but for virt_vm_map_shares. That red was the test's own: it read its own statistics through a handle that carries no READ (PermissionDenied); 470bf6f reads each of three child runs' peaks through the spawner's handle instead (comment #issuecomment-6804509956). Every one of those rows boots /system with shared text; beyond that, virt_vm_map_shares is the claim on AArch64, the BAR and lent-memory rows exercise vm_map's whole-window and rights rules, the spawn and libc rows a spawn's shared and copied ranges, and session_lifecycle the shared memory of a desktop session. Run 43081676127 at 470bf6f (virt_vm_map_shares bar_map_again bar_map_behind_a_bridge lent_device_memory_refused) is green, 4 passed of 4 (https://github.com/ToyOSOrg/evidence/tree/26147d225c0dca7e711a265b842bdfa6ecaa1f0d/r2); it stands for every row but virt_vm_map_shares, whose binary review r2 changed. 3970ec0 (the head) spawns each reader with Command, so its own assertion messages reach the log; run 43093948691 at 3970ec0 (virt_vm_map_shares) is this PR's landing run for it. Two earlier dispatches gave no result: 43049845595 named shared binaries as QEMU rows and was cancelled before it ran; 43051591773 was cancelled once the guest binaries were found not to build.

Owed on metal (both benches are down; recorded in the memory track record, and run on main in one batched boot when a bench returns):

  • On the T14 and the AMD bench: the whole testcases boot, whose vm_map_shares member is the x86-64 verdict that a program's read-only segments take no private frame (spawn_cost among its other members), and the shared-debug and proctreecase boots. Main must show every job exit 0.
  • On the T14 and the AMD bench: osbench's process.spawn_wait, proc.spawn_wait.* and proc.posix_spawn_wait.*, main before this PR against main after, four boots a side. Main after must be no slower. Whether sharing the text makes a spawn faster is not measured on metal.

Risks and owed

  • No AArch64 metal exists; AArch64 is held by virt_vm_map_shares under emulation.
  • The 147x spawn row is step 3's: a program outside ROOT is still read whole into the spawner's memory.
  • The record's re-read of 255 held children's used memory is owed (a QEMU count with a scratch patch, as the record's table was made).
  • Overlaps with in-flight work; whichever lands second resolves.
  • Until step 2's revocation exists, an object a space maps must keep its frames for its life: ROOT's pages stay non-reclaimable, and ROOT is not served through a pager, until then (recorded in the memory track record).

Evidence: https://github.com/ToyOSOrg/evidence/tree/8beddb49b730284eceb9564223da3faa551b729a (README.md maps each check to its command, revision, exit and log).

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review: #1259 at c991bfd (round 1)

Architecture. Step 1 conforms to design rev 4.

  • The handle is the data capability. vm_map(handle, offset, len, prot, at) replaces shm_map. Mapping needs MAP, and WRITE as well for a writable mapping.
  • The object decides execute. One pure rule, kernel::vm::Shared::maps, allows execute only where nobody can write the object and write only where somebody can, and keeps an aperture whole.
  • Leaves map the object's own frames. They map object.page(...). A ROOT program's read-only segments are SYSTEM's Sub of ROOT's Boot pages; they are no longer copied.
  • Spawn and dl. SpawnBacking::Share applies the same rule in a spawn, and dl maps library segments outside the relocation window.
  • No paged object can reach Kind::Object. Both vm_map and the spawn look the handle up with handles.get::<SharedMemObject>, so the type keeps paged objects out until step 2.
  • Manual. The memory requirements and the interface page are updated, and they name no code.
  • Size. Production grows by about +230 net (kernel about +84, ABI +54, SDK about +90). Tests and harness grow by about +310.

W^X and COW (verified).

  • Protection never widens. Kind::Object keeps max equal to its mapped protection. Prot::within lets a ReadExec range drop only to Read or None, never to a writable protection, and a ReadWrite range never gains execute.
  • Nothing writes a boot page. The kernel never writes Backing::Boot (write asserts is_ram), never lends it to a device (run() is None), and never frees it (Drop does nothing).
  • Kernel copies honour the leaf. reach checks prot.allows, x86-64 sets CR0.WP, and AArch64 uses unprivileged stores. No syscall writes another process's memory.
  • AArch64 I/D coherence holds. LeafTable::write and edit run make_executable on every ReadExec leaf, including the shared fault-around fill, so the loader-written pages are made coherent before they execute.
  • Sharing a non-writable segment on the spawn path is safe. Self-relocation writes only into writable PT_LOADs (relocate::Image::apply).
  • dl's extra window check is correct. Its reservation is writable during relocation, so a mapped segment must lie outside the window.
  • No COW yet. mprotect can no longer widen a read-only segment; the PR reports this as a temporary gap.

ROOT's pages withdrawn while mapped. This cannot happen in step 1, by construction:

  • A range holds an Arc<SharedMemObject>.
  • ROOT's Boot frames live for the machine's life.
  • Only apertures are revocable. They map whole, and vm_map records them through mapped_at; a spawn refuses them.

Until step 2's revocation exists, step 1 must keep one guarantee: Kind::Object holds only objects whose frames are fixed for the object's life. Any change that makes ROOT's pages reclaimable, such as serving ROOT through a pager, waits for step 2.

Deleted shm_map callers. All have moved: toyos::shm::map, log/stdio.rs, pciholder, abuse_shared_grant, bar_map_again and shm_read_only. No tracked file outside issues/ names shm_map or SYS_SHM_MAP. The std fork pin a189b3a6b does not use it, and neither do the pinned forks that depend on toyos-abi (cpal, mio, sysinfo, getrandom ×3).

The two fixed defects.

  • suite_split: vm_map_shares joins DRIVEN_AND_SHARED in the existing pattern. checks-split.log shows exit 0 at b29fb2b.
  • Image::handle: no caller remains, and spawn_image_object correctly lays out as Source::Copied. The head staging built every x86-64 guest binary.
  • The gap that let both through (host checks do not build the guest crate) is already recorded in issues/only-a-metal-or-debug-run-builds-every-binary-of-the-guest-test-crate.md.

Overlaps (checked with git merge-tree). Whichever PR lands second owns the resolution below.

Required

None.

Suggestions

  • S1. kernel/src/loader/mod.rs:69-70: "What the handle raw names its object by carries" does not parse. Suggested wording: "The rights the handle naming raw's object must carry: MAP, and WRITE for a writable share."
  • S2. issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md:97: the record says the lost mprotect widening waits for step 5. Design B5's convert-on-protect (step 2: commit, shoot down, become a copy range) restores it without write faults. Name that as the exit, or say why shared spawn ranges must wait for step 5.
  • S3. sdk/abi/src/syscall.rs, SpawnMap doc: the sentence "PermissionDenied for an object handle without MAP" was dropped. The struct no longer says which error a handle without the needed rights gets (MAP, plus WRITE for a writable share).
  • PR text (the orchestrator's to correct):
    • The size line is b29fb2b's; c991bfd is +811/−254 over 39 files.
    • "The 64 KiB run around the touch" is loose. A lone touch takes one page; the run follows only once a neighbour is populated (vm::around).

Evidence, and what the pending runs must show

Host evidence holds.

  • --ci host at b29fb2b: 109 of 110 steps green. In the one red step (cargo test --lib), the only failures are the recorded macOS llvm tests (384 passed, 1 failed). The loom reds in the log are the controls.
  • The targeted tests and both controls ran at 7d9644b. Its sdk/, lib/kimage/ and kernel/pure/vm.rs are identical to b29fb2b; I confirmed this. c991bfd touches only tests/guest/.

No boot has run this head yet. Every boot now spawns the supervisor through the new shared path. Before landing:

  • Guest run 43062438931 (still queued; all nine names are QEMU rows): all nine rows green, and virt_vm_map_shares prints its marker and exits 0 on AArch64.
  • Metal shared boots at c991bfd, on the T14 and the AMD bench: testcases, shared-debug and proctreecase green. In particular:
    • vm_map_shares: its x86-64 verdict.
    • startup_block, spawn_image_object, shm_read_only and abuse_shared_grant.
    • The dl users (dlopen_dedup, std_tls_dlopen, std_unwind_so): the only runs of dl's mapped path anywhere.
  • osbench A/B (four boots a side, p50 per run, exact Mann-Whitney): no regression in proc.spawn_wait.* or in the design's no-regression set (fs, pipe, ipc, mem, proc), because populate changed for every object range. Report any gain with its numbers.
  • If the overlaps are resolved on this branch: compile, and rerun the targeted host tests at the merged head.

Not a landing precondition: the used memory of 255 held children. The issue record carries it as owed, and this PR claims no figure.

LAND, pending the guest and metal results above.

Japabu added a commit that referenced this pull request Nov 11, 2026
…t step 1 keeps until step 2

- `loader::Map::rights` says what it answers: the rights the handle naming
  the range's object must carry.
- `SpawnMap`'s doc names `PermissionDenied` again: a handle without `MAP`,
  or without `WRITE` beside it for a writable share.
- The memory track record names the exit for the lost `mprotect` widening
  of a shared segment: step 2's convert-on-protect.
- The record says what step 1 relies on until step 2's revocation exists:
  an object a space maps keeps its frames for its life, so ROOT's pages stay
  non-reclaimable and ROOT is not served through a pager until then.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Response to review r1 (head 3baf239)

3baf239 is c991bfd plus comments, one doc sentence and the record: no behaviour changes, so the staged images and guest run at c991bfd stand for it.

  • S1 taken: Map::rights now says "The rights the handle naming raw's object must carry: MAP, and WRITE beside it for a writable share."
  • S2 taken: the memory track record names step 2's convert-on-protect as the exit for the lost mprotect widening; only a page shared until its first write waits for step 5.
  • S3 taken: SpawnMap's doc names PermissionDenied for a handle without MAP, or without WRITE beside it for a writable share.
  • The description is corrected: the measured size at the head, and the fault-around (a lone touch maps one page; a touch beside a populated page maps its aligned 64 KiB run).
  • The A thread carries its process's handles, and a lookup takes no lock but the table's #1246 break you found (syscall/vm.rs:93 merges but stops compiling) is in the description's overlap list with the exact replacement.
  • The step 1 guarantee is recorded in the memory track record: until step 2's revocation exists, an object a space maps keeps its frames for its life, so ROOT stays non-reclaimable and is not served through a pager.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: verified 3baf239 against the review's optional items. git diff c991bfdb6 3baf2394e changes only doc comments (loader/mod.rs and syscall.rs: the rights a share needs and the PermissionDenied case) and one issue record. No code changed, so guest run 43062438931 and the queued metal at c991bfd still stand. Review: LAND pending those.

Japabu added a commit that referenced this pull request Nov 11, 2026
…t step 1 keeps until step 2

- `loader::Map::rights` says what it answers: the rights the handle naming
  the range's object must carry.
- `SpawnMap`'s doc names `PermissionDenied` again: a handle without `MAP`,
  or without `WRITE` beside it for a writable share.
- The memory track record names the exit for the lost `mprotect` widening
  of a shared segment: step 2's convert-on-protect.
- The record says what step 1 relies on until step 2's revocation exists:
  an object a space maps keeps its frames for its life, so ROOT's pages stay
  non-reclaimable and ROOT is not served through a pager until then.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Guest red in run 43062438931 (virt_vm_map_shares): a defect in the test, fixed in 470bf6f.

The test read its own peak memory through this_process().stats(). A process's handle to itself carries WRITE, DUP and TRANSFER (the loader's Own), and SYS_PROCESS_STATS needs READ, so the call answered PermissionDenied at vm_map_shares.rs:57 before any claim was checked. The AArch64 job case ran it first; the x86-64 shared boots run the same binary and would have failed the same way. No kernel or ABI behaviour is involved: the other eight rows were green.

The fix measures in children: three runs of the program read nothing, the read-only table and the writable table, and the spawner reads each one's peak off its end through the handle its spawn answers (READ is the spawner's). The claims are unchanged: the read-only table costs less than an eighth of its bytes over the run that reads nothing, the writable one at least three quarters.

  • Guest: virt_vm_map_shares bar_map_again bar_map_behind_a_bridge lent_device_memory_refused dispatched at 470bf6f as run 43081676127.
  • Metal: the shared boots (boot:testcases boot:shared-debug boot:proctreecase) are restaged at 470bf6f on both benches, since they carry this binary. The osbench A/B images staged at c991bfd stand: between c991bfd and 470bf6f the kernel and ABI change only in comments, and the benchcase runs no changed binary.

Japabu added a commit that referenced this pull request Nov 11, 2026
…res as SHM_CREATE's

The first-touch record states the orchestrator's decision as made: one
draw, `pmm::alloc_promised`, for a region's hole (`SharedMemObject::page()`,
into the region's slot) and a MEMORY volume's hole (into the pager's index
under its lock), two stores because one is revoked and the other never;
and an inbox's rings stay drawn whole beside a device's run, since the
kernel writes them in interrupt context, in the decision and its exit.
The build plan (on #1259) leaves the decision. Its figures are
`SHM_CREATE`'s, and that those are the windows is labelled an inference.
Every record links the evidence whose script sorts deterministically.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Guest run 43081676127 at 470bf6f is green: virt_vm_map_shares, bar_map_again, bar_map_behind_a_bridge, lent_device_memory_refused, 4 passed of 4 (evidence https://github.com/ToyOSOrg/evidence/tree/26147d225c0dca7e711a265b842bdfa6ecaa1f0d/r2). The head ae99d1d adds only the memory track's Owed on metal bullets, so it is code-identical to the run. The body's metal conditions are replaced by that owed list, as the benches-down ruling says.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review: #1259's test fix, at ae99d1d (commits 470bf6f, ae99d1d)

Scope. 470bf6f changes only tests/guest/rust-tests/src/bin/vm_map_shares.rs (+54/−16). ae99d1d changes only the memory track record (+13). No kernel, ABI, SDK or harness file has changed since 3baf239, and that commit's own diff from c991bfd is doc comments and the record.

Diagnosis: correct. The loader's Own::new gives a process's handle to itself WRITE | DUP | TRANSFER. sys_process_stats looks the handle up with Rights::READ, and HandleError::Rights is refused as PermissionDenied. Run 43062438931 at c991bfd shows exactly this: panicked at src/bin/vm_map_shares.rs:57:42: vm_map_shares: its own statistics: PermissionDenied, before any claim was checked. The x86-64 statement ("would have failed too") is an inference, and it holds: the rule is in shared kernel code, and on x86-64 this binary runs only in the metal testcases boot, which has not run.

Claims: unchanged, and the measurement holds.

  • A spawner's handle carries BASE | READ | MANAGE. stats_of answers an exited process from the snapshot published with its exit, so the peak read after wait is the reader's whole-life high-water mark of private frames.
  • Comparing three runs, instead of two readings in one process, stays sound because the writable run is a positive control. Both reads happen at the same point in the program. If the reads-nothing run's baseline peak hid a read's cost, it would hide the writable read's cost too, and the private assertion (at least 3/4 of the table) would fail. A defect that charged the read-only table a frame a page would therefore fail the shared assertion. The thresholds are unchanged.
  • vm_protect now runs on a read-only page the parent has not touched. Its refusal comes from the inventory's max in AddressSpace::protect, not from whether the page is populated, so the check is the same.
  • The final unchanged-bytes check still reads the shared pages after the storer has run.

Evidence.

  • Run 43081676127 checked out 470bf6f (its git log -1 says so) and passed 4 of 4. virt_vm_map_shares printed its line on aarch64.
  • ae99d1d is code-identical to 470bf6f.
  • The other rows that passed in 43062438931 are virt_user_mode, a_spawn_from_an_image_frees_it_when_the_child_ends, libc_children, a_drives_row_holds_one_process_per_function and session_lifecycle. Those results still stand: since c991bfd only comments, the record and this one test binary have changed.

Record (ae99d1d). Accurate, and it follows the benches-down ruling. It names the rows, both benches and what main must show. The shared-debug and proctreecase boot names, the osbench row names and spawn_cost's place in the testcases boot all check out. It says plainly that the spawn speed-up is not measured on metal.

Findings

BLOCKER tests/guest/rust-tests/src/bin/vm_map_shares.rs:70-92 (with :49-56): peak_of builds a 13-field toyos::spawn::Spawn by hand, reads the image itself and passes fds: &[], so each reader starts with no stdout or stderr.

  • Lost messages. On ToyOS, a write to a descriptor that is not open drops its bytes (log/stdio.rs write; std's is_ebadf). So the readers' own assertion messages ("the read-only table is not the file's bytes", and the writable table's) can never be seen. A failure shows up only as the reads-shared reader with Ok(Exited(101)).
  • Duplicated spawn code. The same file already spawns its storer with Command, which takes the same path: std's read_image_for, then Helper::spawn, with the same immutable image. Command inherits stdio, and its Child holds the spawn's full handle; job_fairness.rs:126 and toybox's stats.rs read a child's statistics this way.
  • Fix. Replace the body of peak_of with:
let mut reader = Command::new(SELF_PATH).arg(role).spawn().expect("vm_map_shares: a reader starts");
let status = reader.wait().expect("vm_map_shares: wait a reader");
assert_eq!(End::from_word(status.into_raw()), Some(End::Exited(0)), "vm_map_shares: the {role} reader");
let mut stats = ProcessStats::default();
syscall::process_stats(RawHandle(reader.as_raw_handle()), &mut stats).expect("vm_map_shares: a reader's statistics");
stats.peak_memory

This removes about 14 lines, and a failure then names its cause. It needs one more virt_vm_map_shares guest row.

SUGGESTION vm_map_shares.rs:163-165: "takes no frame of this process's own" now describes the readers, not the process running this function. "of its reader's own" says what is measured.

SEND BACK

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 answered at 3970ec0.

  • BLOCKER (readers spawned with no descriptors): fixed as suggested. peak_of spawns each reader with Command, which inherits stdio, waits it, checks it exited 0, and reads its peak through its Child's handle (syscall::process_stats(RawHandle(reader.as_raw_handle()), ..), as job_fairness reads a child's accounting). A reader's own assertion message now reaches the log. The hand-built Spawn and the image read are gone (+13/−26).
  • SUGGESTION (:163-165): taken. The comment says the read-only table takes no frame of its reader's own.

Guest: run 43093948691 at 3970ec0, virt_vm_map_shares (the one row whose binary changed; the rows of 43081676127 and 43062438931 stand, as the review says). Carried onto #1269 as c64e8fd, whose run is 43093955847.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up review: #1259 at 3970ec0 (since ae99d1d)

Scope. This round adds one commit, 3970ec0, whose parent is ae99d1d. It changes only tests/guest/rust-tests/src/bin/vm_map_shares.rs, by +13/−26. Nothing else has changed.

The previous blocker is resolved. peak_of (:72-79) now has the proposed body, word for word. Each reader is a Command with inherited stdio, so its own assertion messages reach the log. The hand-built 13-field Spawn, which duplicated the storer's spawn path, is gone.

It compiles, by inspection.

  • The two new imports, AsRawHandle and toyos_abi::RawHandle, and the added ProcessStats are the ones job_fairness.rs:18,27-28 uses for the same call at :126.
  • Child: AsRawHandle (sdk/std/os/process.rs:270) returns a u32, which is what toyos_abi::RawHandle wraps.
  • Child::wait takes &mut self, and the binding is mut.
  • ExitStatusExt::into_raw with End::from_word is the form the file already uses at :175.
  • Every import is still used. toyos::files::read_image remains in use at :134. The old toyos::spawn uses were path-qualified, so removing them leaves no import behind.

The handle and its rights are unchanged.

  • std's Process (sdk/std/sys/process.rs:479-488) is the same toyos::process::Process that Helper::spawn returns, so it carries the spawner's rights as before.
  • Child::wait only waits on the handle. The handle stays owned until the function returns.
  • process_stats answers for a process that has exited, as the old reader.stats() after wait relied on.

The test's claims are unchanged.

  • A non-independent Command is a helper spawn. Its image comes from std's read_image, which is read_image_for(path, None), exactly what toyos::files::read_image calls. So each reader runs the same immutable image as before.
  • image_held only affects the helper-or-launch classification, so it changes nothing for this spawn.
  • The readers now differ from before in three ways: the environment and working directory are inherited, and descriptors 0-2 are open. All three roles get the same, so the differences cancel when reads-none is subtracted.
  • The thresholds and the storer are untouched.

The adopted comment is accurate. :150-152 now says the frames are the reader's, and :69-71 names the Child's handle.

Evidence. Guest run 43093948691 (virt_vm_map_shares, head 3970ec0) is still queued, so nothing has been measured at this head yet. The test binary changed, so that row must be green before landing; a red sends the PR back. Metal is not a landing condition while the benches are down.

Findings

None.

LAND, once run 43093948691 is green.

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
…t step 1 keeps until step 2

- `loader::Map::rights` says what it answers: the rights the handle naming
  the range's object must carry.
- `SpawnMap`'s doc names `PermissionDenied` again: a handle without `MAP`,
  or without `WRITE` beside it for a writable share.
- The memory track record names the exit for the lost `mprotect` widening
  of a shared segment: step 2's convert-on-protect.
- The record says what step 1 relies on until step 2's revocation exists:
  an object a space maps keeps its frames for its life, so ROOT's pages stay
  non-reclaimable and ROOT is not served through a pager until then.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main 06fcb81 (#1246, #1226, #1216 among what landed); head 1f8a0e4. Four conflicts, each resolved so both sides hold:

  • kernel/src/mm/space.rs (populate's empty-leaf arm): main's shape is kept, with around hoisted above the match. It already gives object ranges the fault-around this PR added, so this PR's own hunk there is dropped.
  • kernel/src/syscall/ipc.rs: this PR's deletion of sys_shm_map is kept. Main had only moved its handle lookup to with_handles.
  • kernel/src/syscall/proc.rs: both. A spawn range's rights come from Map::rights(&raw), as this PR has it, and the handle is looked up through A thread carries its process's handles, and a lookup takes no lock but the table's #1246's process::with_handles.
  • sdk/toyos/src/log/stdio.rs: A process holds its stdio stream state once, with its descriptor table #1226's shape is kept. Its stdio ring is now mapped in the descriptor table's owner (ask), so that site's shm_map becomes vm_map(handle, 0, size, ReadWrite, ANYWHERE), as this PR had it at the old site.

One more site merged cleanly but no longer compiled: sys_vm_map's handle lookup now goes through process::with_handles. The fix is folded into the commit that introduced sys_vm_map (83b9f64), so every commit compiles. The head differs from the pre-rebase tree, over main's own changes, only by these resolutions.

Checks at 1f8a0e4: cargo test -p kernel --lib --features sched-check 462 passed; cargo run -- --ci host lint 5 of 5 green. Guest: run 43110347183 at 1f8a0e4 (virt_vm_map_shares bar_map_again bar_map_behind_a_bridge lent_device_memory_refused a_spawn_from_an_image_frees_it_when_the_child_ends).

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
…cator's lock

Main's populate already fills an object range's whole run (#1245), so a
region written whole after it is made, such as a 32 MiB spawn image, drew
a frame a page, one hold of the frame allocator's lock each: 8,192 where
drawing the region whole took 16. The orchestrator decided to build the
batch here rather than wait for #1259.

`SharedMemObject::pages()` answers a run of pages at once, at most a
fault's run, and `page()` goes through it. For a region, `slot::frames`
takes every frame installed with one Acquire load each; if any page it was
asked for is a hole it takes the region's first-touch lock, looks again,
and has the holes still left drawn by one call,
`pmm::alloc_promised_zeroed_into`: one hold of the allocator's lock for the
draw, zeroing outside it, and one Release store a page to install. The
object arm of `populate` asks for its run this way, and `create_whole`
draws an inbox's rings a run at a time.

The loom model gains two runs sharing a page: each page draws once, the
shared page's one frame is handed to both, and each run draws in one call
at most; `slot-install-relaxed` still reds the hand-over models. The
first-touch record says it is built and carries what it owes on metal; the
window record's two bullets go, the batch being built and the reading
moved to the record of the change it measures. The slot's comments are
trimmed as review r2 suggested.

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

The rebase onto #1259 at main 06fcb81 kept step 2's per-arm `around` in
`populate` so every replayed commit compiled; this restores #1245's shape on
main, one `around` above the match for every kind of range, with the paged
arm's index lookup beneath it. No behaviour changes.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1259's rebase onto main 06fcb81 (head 1f8a0e4; LAND before at 3970ec0)

Architecture

The rebase does not change the design. #1259 still has one vm_map, and one pure rule (Shared::maps) for what a space may map. Main's changes keep their own shape: #1245's hoisted fault-around, #1246's handle lookups, and #1226's stream state in the descriptor table's owner. The rebase adds no production or test code. The only new lines are the four resolutions and the lookup fix.

What I checked

  1. git range-diff b56693152..3970ec033 06fcb81ea..1f8a0e42b shows nothing else changed.
    • Commits 2 to 6 are identical (=).
    • 83b9f64 differs from b29fb2b only in these places:
      • populate's object-range hunk in space.rs is dropped.
      • The body of the deleted sys_shm_map is main's with_handles form. The deletion itself is unchanged.
      • spawn_program combines Map::rights(&raw) with with_handles.
      • sys_vm_map's lookup uses with_handles.
      • In stdio.rs, the vm_map call moved to A process holds its stdio stream state once, with its descriptor table #1226's owner::ask.
      • Two context lines changed, in memory.md and sdk/abi/src/syscall.rs.
  2. Dropping the space.rs hunk loses nothing.
    • Main's populate (from A paged read takes two passes of its pager's lock and populates nothing ahead #1245, 77f15f6) already maps an object range's run. It uses the same vm::around predicate (leaves.covers(at) && leaves.get(at) != Leaf::Empty) and the same frames (object.page(offset + (at - start))). It also sets the same owned: false and cache: object.cache() that the dropped hunk wrote.
    • The PR's remaining space.rs change is map calling mappable.
    • No issue record or manual text claims the fault-around.
  3. Both sides' behaviour is kept.

Findings

No blockers.

SUGGESTION

Evidence

My checks at 1f8a0e4, in a detached worktree with its own target directory, on a macOS arm64 host:

  • cargo test -p kernel --lib --features sched-check: 462 passed, exit 0.
  • cargo run -- --ci host lint: 5 steps all green, 26 clippy runs, exit 0.
  • cargo run -- --ci host models: 83 steps all green, exit 0.

The author's numbers (462 passed, lint 5 of 5) agree with mine.

Guest: run 43110347183 at 1f8a0e4 had started but had not finished when I posted.

Metal: none, and none is a landing condition while the benches are down.

LAND

Japabu added a commit that referenced this pull request Nov 11, 2026
…t step 1 keeps until step 2

- `loader::Map::rights` says what it answers: the rights the handle naming
  the range's object must carry.
- `SpawnMap`'s doc names `PermissionDenied` again: a handle without `MAP`,
  or without `WRITE` beside it for a writable share.
- The memory track record names the exit for the lost `mprotect` widening
  of a shared segment: step 2's convert-on-protect.
- The record says what step 1 relies on until step 2's revocation exists:
  an object a space maps keeps its frames for its life, so ROOT's pages stay
  non-reclaimable and ROOT is not served through a pager until then.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Head 8a55a5f: 83b9f64's message reworded as 866fd8b, so it no longer claims object ranges' fault-around, which is main's (#1245); the other five commits are replayed unchanged. The tree is identical to 1f8a0e4 (git diff 1f8a0e42b 8a55a5fcf is empty), so run 43110347183 at 1f8a0e4 tests this head's tree.

Japabu and others added 2 commits November 12, 2026 01:12
…able one is shared and executable: /system text is ROOT's own pages

Memory track stage 5 for `/system`, and step 1 of 7b (mapped paged
objects). A running `/system` program's read-only segments were copied out
of ROOT's boot pages a page at each first touch, so every process of one
binary held its own copy of the text and paid a fault and a 4 KiB copy per
page. They are now ROOT's pages, mapped.

- ABI: `vm_map(object, offset, len, prot, at)` (SYS 106) replaces
  `shm_map`, which mapped a whole object anywhere. A range of whole pages of
  the object goes anywhere or at a fixed place, as `vm_reserve` places.
- Kernel: one pure rule, `kernel::vm::Shared::maps`, decides what a space may
  map: executable only where nobody can write the object (a boot image or
  part of one), writable only where somebody can, a device's window whole.
  `SharedMemObject::mappable` applies it for `vm_map` and a spawn. (Object
  ranges' 64 KiB fault-around is main's, from #1245.)
- Spawn: `SpawnMap::backing` says copy or share. `toyos::elf::program::
  lay_out` shares a segment the program cannot write, whose bytes are its
  file's pages with nothing past them, from an image nobody can write
  (`Source::Immutable`), and copies everything else. kimage FORMAT 2 carries
  the word for the supervisor.
- `toyos::dl` maps such a library segment from its immutable image and
  copies only the rest; a segment inside the window relocations write is
  never mapped.
- A shared segment's mapping is never widened, so a program can no longer
  `mprotect` its own read-only segment writable; recorded in the memory
  track record.
- Guest: `vm_map_shares` (x86-64, and `virt_vm_map_shares` on AArch64,
  where no other row maps a shared page executable): a range lands at its
  fixed place, a writable region is never executed, an image is executed
  and never written, and a program's read-only segments take no private
  frame and fault a store.
- The memory track records the orchestrator's stage-6 decision: 7b's step 3
  maps images from every pager-backed volume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Image::handle` became `Image::source` and `lay_out` takes a
`Source`, but three guest test sites still passed a bare handle: the shared
image helper, `startup_block`, and `spawn_image_object`, whose object is a
writable copy of its own and so lays out as `Source::Copied`, every range
copied, which is what that test is about. The guest test binaries did not
build. No host check builds them; staging the metal images did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu and others added 4 commits November 12, 2026 01:12
…t step 1 keeps until step 2

- `loader::Map::rights` says what it answers: the rights the handle naming
  the range's object must carry.
- `SpawnMap`'s doc names `PermissionDenied` again: a handle without `MAP`,
  or without `WRITE` beside it for a writable share.
- The memory track record names the exit for the lost `mprotect` widening
  of a shared segment: step 2's convert-on-protect.
- The record says what step 1 relies on until step 2's revocation exists:
  an object a space maps keeps its frames for its life, so ROOT's pages stay
  non-reclaimable and ROOT is not served through a pager until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test read its own peak memory through `this_process().stats()`, which
the kernel refuses: a process's handle to itself carries `WRITE`, `DUP` and
`TRANSFER`, and stats need `READ`. The AArch64 job case ran it first and
ended at that call (`PermissionDenied`, guest run 43062438931); the x86-64
shared boots run the same binary and would have too.

Three runs of the program now read nothing, the read-only table and the
writable table, and the spawner reads each one's peak off its end through
the handle its spawn answers. The claims are unchanged: the read-only table
costs less than an eighth of its bytes over the run that reads nothing, the
writable one at least three quarters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both benches are down, and metal is no longer a landing condition (the
orchestrator's ruling). The memory track records what step 1 owes there, for
one batched boot of main when a bench returns: the whole testcases boot with
`vm_map_shares`'s x86-64 verdict, shared-debug and proctreecase on both
benches, every job exiting 0; and osbench's spawn rows, main before against
main after, no slower.

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

`peak_of` built a `Spawn` by hand with no descriptors, so a reader's
assertion messages went nowhere and a failure read only as an exit of 101.
A reader is now spawned with `Command`, as the storer already is, inheriting
stdio, and its peak is read off its end through its `Child`'s handle, which
carries the spawner's `READ` (as `job_fairness` reads a child's accounting).
The measurement and its claims are unchanged. The comment on what is
measured names the reader.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main 3b93f37; head 04b1235. The one conflict was tests/toyos.rs against #1254's registry: virt_vm_map_shares moves into SCREEN_TESTS' registry form (("virt_vm_map_shares", qemu::Profile::Virt, |profile, _| virt_job(profile, &format!("test_rs_{VIRT_VM_MAP}")))), and its arm in the removed run_screen_test goes. git range-diff 06fcb81ea..8a55a5fcf origin/main..04b1235a2 shows that move and nothing else; the other five commits are =. Checks at 04b1235: cargo test -p kernel --lib --features sched-check 464 passed, cargo test --test toyos-checks suite_split passed, cargo run -- --ci host lint 5 of 5 green. Run 43110347183 (at 1f8a0e4, green) stands for the kernel and the guest binaries.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: rebased onto main 3b93f37 at 04b1235. git range-diff shows commits 2–6 unchanged and commit 1 differing only by moving virt_vm_map_shares into #1254's SCREEN_TESTS registry form (verified). 83b9f64's message no longer claims #1245's fault-around. Review LAND; guest run 43110347183 (tree-identical kernel and guest binaries) green. Queued.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 23:24
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 75bdec3 Nov 11, 2026
10 checks passed
@Japabu
Japabu deleted the map7b branch November 11, 2026 23:41
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
…cator's lock

Main's populate already fills an object range's whole run (#1245), so a
region written whole after it is made, such as a 32 MiB spawn image, drew
a frame a page, one hold of the frame allocator's lock each: 8,192 where
drawing the region whole took 16. The orchestrator decided to build the
batch here rather than wait for #1259.

`SharedMemObject::pages()` answers a run of pages at once, at most a
fault's run, and `page()` goes through it. For a region, `slot::frames`
takes every frame installed with one Acquire load each; if any page it was
asked for is a hole it takes the region's first-touch lock, looks again,
and has the holes still left drawn by one call,
`pmm::alloc_promised_zeroed_into`: one hold of the allocator's lock for the
draw, zeroing outside it, and one Release store a page to install. The
object arm of `populate` asks for its run this way, and `create_whole`
draws an inbox's rings a run at a time.

The loom model gains two runs sharing a page: each page draws once, the
shared page's one frame is handed to both, and each run draws in one call
at most; `slot-install-relaxed` still reds the hand-over models. The
first-touch record says it is built and carries what it owes on metal; the
window record's two bullets go, the batch being built and the reading
moved to the record of the change it measures. The slot's comments are
trimmed as review r2 suggested.

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
…cator's lock

Main's populate already fills an object range's whole run (#1245), so a
region written whole after it is made, such as a 32 MiB spawn image, drew
a frame a page, one hold of the frame allocator's lock each: 8,192 where
drawing the region whole took 16. The orchestrator decided to build the
batch here rather than wait for #1259.

`SharedMemObject::pages()` answers a run of pages at once, at most a
fault's run, and `page()` goes through it. For a region, `slot::frames`
takes every frame installed with one Acquire load each; if any page it was
asked for is a hole it takes the region's first-touch lock, looks again,
and has the holes still left drawn by one call,
`pmm::alloc_promised_zeroed_into`: one hold of the allocator's lock for the
draw, zeroing outside it, and one Release store a page to install. The
object arm of `populate` asks for its run this way, and `create_whole`
draws an inbox's rings a run at a time.

The loom model gains two runs sharing a page: each page draws once, the
shared page's one frame is handed to both, and each run draws in one call
at most; `slot-install-relaxed` still reds the hand-over models. The
first-touch record says it is built and carries what it owes on metal; the
window record's two bullets go, the batch being built and the reading
moved to the record of the change it measures. The slot's comments are
trimmed as review r2 suggested.

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
…cator's lock

Main's populate already fills an object range's whole run (#1245), so a
region written whole after it is made, such as a 32 MiB spawn image, drew
a frame a page, one hold of the frame allocator's lock each: 8,192 where
drawing the region whole took 16. The orchestrator decided to build the
batch here rather than wait for #1259.

`SharedMemObject::pages()` answers a run of pages at once, at most a
fault's run, and `page()` goes through it. For a region, `slot::frames`
takes every frame installed with one Acquire load each; if any page it was
asked for is a hole it takes the region's first-touch lock, looks again,
and has the holes still left drawn by one call,
`pmm::alloc_promised_zeroed_into`: one hold of the allocator's lock for the
draw, zeroing outside it, and one Release store a page to install. The
object arm of `populate` asks for its run this way, and `create_whole`
draws an inbox's rings a run at a time.

The loom model gains two runs sharing a page: each page draws once, the
shared page's one frame is handed to both, and each run draws in one call
at most; `slot-install-relaxed` still reds the hand-over models. The
first-touch record says it is built and carries what it owes on metal; the
window record's two bullets go, the batch being built and the reading
moved to the record of the change it measures. The slot's comments are
trimmed as review r2 suggested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
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>
Japabu added a commit that referenced this pull request Nov 12, 2026
…cator's lock

Main's populate already fills an object range's whole run (#1245), so a
region written whole after it is made, such as a 32 MiB spawn image, drew
a frame a page, one hold of the frame allocator's lock each: 8,192 where
drawing the region whole took 16. The orchestrator decided to build the
batch here rather than wait for #1259.

`SharedMemObject::pages()` answers a run of pages at once, at most a
fault's run, and `page()` goes through it. For a region, `slot::frames`
takes every frame installed with one Acquire load each; if any page it was
asked for is a hole it takes the region's first-touch lock, looks again,
and has the holes still left drawn by one call,
`pmm::alloc_promised_zeroed_into`: one hold of the allocator's lock for the
draw, zeroing outside it, and one Release store a page to install. The
object arm of `populate` asks for its run this way, and `create_whole`
draws an inbox's rings a run at a time.

The loom model gains two runs sharing a page: each page draws once, the
shared page's one frame is handed to both, and each run draws in one call
at most; `slot-install-relaxed` still reds the hand-over models. The
first-touch record says it is built and carries what it owes on metal; the
window record's two bullets go, the batch being built and the reading
moved to the record of the change it measures. The slot's comments are
trimmed as review r2 suggested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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