fix(link): the allocator shim's marker is called, not read, so it points at a ret - #49
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-B05increment 6's gate: a component allocates for the first time, andHEAP_GAPis 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:
Sixteen bytes of stack per cycle, about a thousand cycles, then
SPAWN_GUARD:user/init/link.ldaliasedalloc's shim marker to the image's first byte, and said why: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 intocomponent::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
retat 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_PAGESin 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, andSPAWN_STACK_PAGESstays at four. The paragraph I left beside that constant blaming a large frame is corrected in this diff.Blast radius
allocstoresupervisorExactly the two components that declare a
heapneed.storewould have faulted identically the day it was scheduled, soE2-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/initis a flat image on another path, and a runtime comes fromprepare_runtime. Scheduling an occupant is what made a two-epoch-old link bug reachable.Paying
HEAP_GAPThe boot now reports:
HEAP_GAPdeclared that the peak was zero because no component with aheapneed 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'sheap_peakfield 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'sheap_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'sE2-B10andE1-B05lines 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 fromconst CHAOS_GAPby my own earlier insertion ofHEAP_GAPbetween them — so for two PRs it had silently been documenting the wrong constant. DeletingHEAP_GAPorphaned it and the compiler said so. It is back on its own constant.Evidence
cargo xtask verify—verify: all green, exit 0cargo xtask run—M0 ok,peak 64 byte(s),init process core 1, 4 call(s) answered, 0 refusedllvm-nm/llvm-objdumpon the linked images, before and after — the marker no longer aliases__image_start, andbox_new_uninitcalls0x400311 <__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