Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions changelog.d/8021-7990-map-comparison-regression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
**Fixed: the zod dependency corpus no longer reaches the young-pin latch
through a stale comparison operand (#7990).**

The abort's `GC_TYPE_MAP | GC_FLAG_INTERNED` header was internally impossible:
the interned flag is only created on strings. A comparison operand had kept an
old SSA address across an allocating right-hand operand, then handed recycled
bytes to the copying collector. The comparison-rooting fix in #8011 removed
that window; a patch-only A/B completed 26 of 26 rate-1 corpus runs clean, with
about 6,400 copying minors and 855,000 moved objects per run.

An LLVM-dataflow regression now covers the reported typed-Map population and
proves that its value is re-read from a GC root below the allocating operand.
The collector still aborts before an unsafe relocation, but its internal
documentation no longer assumes an incomplete pin latch is the only possible
cause.
25 changes: 25 additions & 0 deletions crates/perry-codegen/src/expr/compare_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,31 @@ fn strict_eq_rereads_its_left_operand_below_an_allocating_right_operand() {
);
}

/// #7990 surfaced the same stale comparison-operand class through a different
/// consumer: the copier reached bytes reporting the impossible combination
/// `GC_TYPE_MAP | GC_FLAG_INTERNED`. Keep the reported typed-Map population in
/// the regression. The generic object case above would stay green if a future
/// type-analysis shortcut accidentally classified Maps as non-pointers.
#[test]
fn strict_eq_rereads_a_map_operand_below_an_allocating_right_operand() {
let ir = cmp_ir(
"streq_rooted_map_operand_7990",
CompareOp::Eq,
Expr::MapNew,
Expr::Object(vec![("right".to_string(), Expr::Number(2.0))]),
);
let left = call_operand_of(&ir, "js_eq", 0);
let right = call_operand_of(&ir, "js_eq", 1);
let left_producer = producer_line(&ir, &left);
let right_producer = producer_line(&ir, &right);
assert!(
left_producer > right_producer,
"js_eq's Map operand ({left}) is produced at line {left_producer}, below the right \
operand ({right}) at line {right_producer}. An intervening collection would leave \
the comparison holding a retired Map address (#7990).\n{ir}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}

/// The complementary cost assertion: even with an allocating right operand,
/// a proven-number left operand cannot be invalidated by relocation and must
/// stay in its original register. A blanket "root every comparison" fix would
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-runtime/src/gc/copying.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1033,8 +1033,11 @@ pub(super) fn scan_remembered_dirty_slots_copying(
}

/// The young-pin latch was clear, the preflight was skipped on that proof, and
/// the copier then met a pinned young object anyway — so the latch is
/// incomplete and a pin site exists that does not go through `gc::pin_object`.
/// the copier then met bytes that describe a pinned young object anyway.
/// This is the instant relocation would become unsafe, but it does not by
/// itself identify the violated invariant. In #7990 the header was internally
/// impossible (`GC_TYPE_MAP | GC_FLAG_INTERNED`), and the fault disappeared
/// when comparison operands were rooted; the pin latch itself was complete.
///
/// There is no recovery: leaving the object in from-space strands the
/// referring slot on memory `copying_reset_from_spaces_and_flip` is about to
Expand Down
Loading