Skip to content

gc(copying): fabricated Map/Set with a correct-size header still passes plausible_gc_header — needs object-start validation #8256

Description

@jdalton

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions