Skip to content

fix(link): the allocator shim's marker is called, not read, so it points at a ret - #49

Merged
dimmus merged 1 commit into
mainfrom
iritur/what-wants-four-pages-of-stack
Sep 13, 2026
Merged

dimmus merged 1 commit into
mainfrom
iritur/what-wants-four-pages-of-stack

Conversation

@dimmus

@dimmus dimmus commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

A component that allocated re-entered its own entry point until the stack met its guard page. This is why, and it was in the linker script.

This closes E1-B05 increment 6's gate: a component allocates for the first time, and HEAP_GAP is paid and deleted on its own terms.

The defect

#48 scheduled the first occupant of a place and it died on its first instruction. The disassembly:

0000000000400000 <__image_start>:          ← component::start
  400001:  callq 0x40006d <box_new_uninit>
000000000040006d <box_new_uninit>:
  40006e:  callq 0x400000 <__image_start>  ← straight back in

Sixteen bytes of stack per cycle, about a thousand cycles, then SPAWN_GUARD:

occupant  killed: exception 14 at 0x0000000000410ff8, error 0x6, rip 0x0000000000400000

user/init/link.ld aliased alloc's shim marker to the image's first byte, and said why:

When alloc grows a vector it emits a volatile one-byte read of __rust_no_alloc_shim_is_unstable_v2 … the script provides it, pointed at the image's first byte: the read is one byte from a page mapped read-only and executable.

That was true when it was written. This toolchain emits a callq, not a read — so the alias turned the allocator's first act into a recursive call into component::start.

The script's own reversal condition anticipated the hash changing on a toolchain bump, and said the link would then fail loudly. What actually arrived was different: the hash was unchanged and the marker's use moved from a read to a call. The marker now points at one ret at the end of .text, which satisfies both uses, so the next such move does not matter.

Neither the stack nor the frame size was the cause

#48 left this open with two candidate fixes — raise SPAWN_STACK_PAGES in lockstep across three crates, or find what wanted 16 KiB and shrink it. Looking at the frame size first was the right order and the frame size was not the finding: there is no oversized frame. The four-page stack is what caught the recursion, exactly as a guard page should, and SPAWN_STACK_PAGES stays at four. The paragraph I left beside that constant blaming a large frame is corrected in this diff.

Blast radius

component links alloc aliased
store yes yes
supervisor yes yes
virtio-blk / net / gpu no no

Exactly the two components that declare a heap need. store would have faulted identically the day it was scheduled, so E2-B10's heap has never worked inside a component and could not have.

It was unobservable until now because no component image had ever executed: user/init is a flat image on another path, and a runtime comes from prepare_runtime. Scheduling an occupant is what made a two-epoch-old link bug reachable.

Paying HEAP_GAP

The boot now reports:

scheduled  place supervisor on core 1 — it announced itself after 19 tick(s) in ring 3
occupant   ended itself with status 0
supervisor ok — … heap 8192 B described, peak 64 byte(s), starved false

HEAP_GAP declared that the peak was zero because no component with a heap need had ever run, and its refusal said in as many words that a non-zero peak would be the good ending and the row should then be deleted. It went red, said that, and has been deleted.

The check is inverted rather than dropped. Zero is still the finding kernel/src/component.rs's heap_peak field calls it — a finding rather than a default — and a check that stopped looking would let the allocation this whole task exists to demonstrate quietly stop happening.

The gap's fourth field named five documents. Three carried the claim and are updated in this diff: kernel/src/runtime.rs's needle sentence, now half false and saying which half; component.rs's heap_peak; and the check itself. The two boot documents it named never mentioned the heap at all — recorded rather than silently skipped, because a fourth field naming a document that does not carry the claim is the same decay the field exists to prevent.

TODO.md's E2-B10 and E1-B05 lines are the originator's; agents do not edit that file.

One defect of my own, found by removing the constant

CHAOS_GAP's doc comment had been separated from const CHAOS_GAP by my own earlier insertion of HEAP_GAP between them — so for two PRs it had silently been documenting the wrong constant. Deleting HEAP_GAP orphaned it and the compiler said so. It is back on its own constant.

Evidence

  • cargo xtask verifyverify: all green, exit 0
  • cargo xtask runM0 ok, peak 64 byte(s), init process core 1, 4 call(s) answered, 0 refused
  • llvm-nm / llvm-objdump on the linked images, before and after — the marker no longer aliases __image_start, and box_new_uninit calls 0x400311 <__rust_alloc_shim_marker>

The test that asserted HEAP_GAP's needle was still in the tree is replaced rather than removed, by one asserting the shape this now guarantees — a peak of zero is the failure, whatever the figure is when it is not. Deleting it without a replacement would have left the strongest claim on this path resting on a boot nobody checks.

🤖 Generated with Claude Code

…nts at a ret

A component that allocated re-entered its own entry point until the stack met
its guard page. `E1-B05` scheduled the first occupant of a place and it died on
its first instruction; this is why, and it was in the linker script.

    0000000000400000 <__image_start>:      ← component::start
      400001:  callq 0x40006d <box_new_uninit>
    000000000040006d <box_new_uninit>:
      40006e:  callq 0x400000 <__image_start>

`user/init/link.ld` aliased `alloc`'s shim marker to the image's first byte, and
said why: *`alloc` emits a volatile one-byte **read** of
`__rust_no_alloc_shim_is_unstable_v2`*, so any readable address would do. That
was true when it was written. This toolchain emits a **call**, so the alias made
the allocator's first act a recursive call into `component::start` — sixteen
bytes of stack per cycle, about a thousand cycles, then `SPAWN_GUARD`.

The marker now points at one `ret` at the end of `.text`, which satisfies both
uses rather than the one the comment assumed: a read gets `0xC3` from a mapped
read-only page, and a call returns immediately.

**Neither the stack nor the frame size was the cause.** The four-page stack is
what *caught* it, exactly as a guard page should. `SPAWN_STACK_PAGES` is back to
four and the paragraph beside it that blamed a large frame is corrected: looking
at the frame size first was right, and the frame size was not the finding.

**It could not have been observed before now.** No component image had ever
executed: `user/init` is a flat image on another path and a runtime comes from
`prepare_runtime`. Both components that declare a heap — `store` and
`supervisor` — carried it, and `store` would have faulted identically the day it
was scheduled. So `E2-B10`'s heap has never worked inside a component.

**`HEAP_GAP` is paid and deleted on its own terms.** It declared that the peak
was zero because no component with a `heap` need had ever run, and its refusal
said a non-zero peak would be the good ending and the row should then go. The
boot now reports `peak 64 byte(s)`, so it has gone. The check is **inverted
rather than dropped** — zero is still the finding `heap_peak` calls it, and a
check that stopped looking would let this quietly stop working again.

The gap's fourth field named five documents. Three carried the claim and are
updated: `kernel/src/runtime.rs`'s needle sentence, which is now half false and
says which half; `kernel/src/component.rs`'s `heap_peak`; and the check. The two
boot documents it named never mentioned the heap at all — recorded here rather
than silently skipped, because a fourth field that names a document which does
not carry the claim is the same decay the field exists to prevent. `TODO.md`'s
`E2-B10` and `E1-B05` lines are the originator's.

Also fixed: `CHAOS_GAP`'s doc comment had been separated from `const CHAOS_GAP`
by my own earlier insertion of `HEAP_GAP` between them, so it had silently been
documenting the wrong constant. Removing `HEAP_GAP` orphaned it and the compiler
said so. It is back on its own constant.

    scheduled  place supervisor on core 1 — it announced itself after 19 tick(s)
    occupant   ended itself with status 0
    supervisor ok — … heap 8192 B described, peak 64 byte(s), starved false

`cargo xtask verify` is 0 — `verify: all green`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dimmus
dimmus merged commit 8964f8b into main Sep 13, 2026
26 checks passed
@dimmus
dimmus deleted the iritur/what-wants-four-pages-of-stack branch September 13, 2026 14:50
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