Follow-up to the Critical CodeRabbit finding on #8251 (split out rather than forced into that merged PR).
The gap
#8251 tightened plausible_gc_header so a fixed-layout type's size must equal its known total (Map/Set total = 24). That closes the observed SIGSEGV vector, where a pointer-shaped word in a live object's payload was read as a header with size ≈ 1024–1040 and fabricated into a fake Map.
But a fabricated header with a correct size (24) is indistinguishable from a genuine one by header fields alone — obj_type=8, flags plausible, size=24 all match. plausible_gc_header can't reject it, because it validates the header's fields, not whether the address is actually an object start. classify_arena confirms the address and addr - 8 are in heap space, then trusts the pattern match; nothing checks the address is an object boundary.
The PR's own regression test documents this explicitly (crates/perry-runtime/src/gc/tests/copying/fabricated_map_rejection.rs:193-223): a correctly-sized fabricated Map header passes. The entries/elements tripwire backstops the read, but the fabrication itself is still possible.
Why it's not already fixed
The load-bearing claim for the size check being sound for genuine objects was verified: hot_arena_free_list / ARENA_FREE_LIST is never populated anywhere (every borrow_mut() is a .clear(); the nonempty flag is never set true), so size == total holds for every genuine arena Map/Set. That makes the size check correct for the observed vector — but it does not address the correct-size coincidence.
The real fix (heavier than a header tweak)
Give classify_arena a real object-start check — a per-page object-start bitmap, or validate the candidate address against the arena's object walk. This is the principled fix mature conservative collectors use. The cost must be designed, not guessed: the copying minor exists specifically to avoid O(all-objects) work, so a bitmap (or an incremental walk) is the likely shape rather than a full scan.
Note
A correct-size fabricated header requires a pointer-shaped word whose low byte is 0x08 (GC_TYPE_MAP) AND whose next byte has GC_FLAG_ARENA AND whose top 32 bits read as size=24 — i.e. a pointer into a very low address (≈0x18000000 = 24<<24). That is a much narrower coincidence than the ~1024-size vector, so this is lower-frequency than what #8251 closed, but it is a real soundness gap.
Related: #8251 (the size check), and the Finding-2 follow-up that unified the entries/elements tripwires on is_plausible_heap_addr.
Follow-up to the Critical CodeRabbit finding on #8251 (split out rather than forced into that merged PR).
The gap
#8251 tightened
plausible_gc_headerso a fixed-layout type'ssizemust equal its known total (Map/Set total = 24). That closes the observed SIGSEGV vector, where a pointer-shaped word in a live object's payload was read as a header withsize≈ 1024–1040 and fabricated into a fake Map.But a fabricated header with a correct size (24) is indistinguishable from a genuine one by header fields alone —
obj_type=8,flagsplausible,size=24all match.plausible_gc_headercan't reject it, because it validates the header's fields, not whether the address is actually an object start.classify_arenaconfirms the address andaddr - 8are in heap space, then trusts the pattern match; nothing checks the address is an object boundary.The PR's own regression test documents this explicitly (
crates/perry-runtime/src/gc/tests/copying/fabricated_map_rejection.rs:193-223): a correctly-sized fabricated Map header passes. The entries/elements tripwire backstops the read, but the fabrication itself is still possible.Why it's not already fixed
The load-bearing claim for the size check being sound for genuine objects was verified:
hot_arena_free_list/ARENA_FREE_LISTis never populated anywhere (everyborrow_mut()is a.clear(); the nonempty flag is never set true), sosize == totalholds for every genuine arena Map/Set. That makes the size check correct for the observed vector — but it does not address the correct-size coincidence.The real fix (heavier than a header tweak)
Give
classify_arenaa real object-start check — a per-page object-start bitmap, or validate the candidate address against the arena's object walk. This is the principled fix mature conservative collectors use. The cost must be designed, not guessed: the copying minor exists specifically to avoid O(all-objects) work, so a bitmap (or an incremental walk) is the likely shape rather than a full scan.Note
A correct-size fabricated header requires a pointer-shaped word whose low byte is
0x08(GC_TYPE_MAP) AND whose next byte has GC_FLAG_ARENA AND whose top 32 bits read assize=24— i.e. a pointer into a very low address (≈0x18000000 = 24<<24). That is a much narrower coincidence than the ~1024-size vector, so this is lower-frequency than what #8251 closed, but it is a real soundness gap.Related: #8251 (the size check), and the Finding-2 follow-up that unified the entries/elements tripwires on
is_plausible_heap_addr.