Split out of #7493, which worked around this with a pin. The workaround is correct for today's default but the underlying assertion is wrong, and it will misfire again.
The assertion
crates/perry-codegen/tests/native_proof_regressions.rs:
fn assert_buffer_store_uses_dynamic_fallback(ir: &str) {
assert!(ir.contains("call void @js_buffer_set"), ...);
assert!(!ir.contains("getelementptr inbounds i8"), ...); // <— this one
}
Same shape at native_proof_regressions.rs:846 and twice in native_proof_buffer_views.rs (:606, :682).
The intent is "no native buffer GEP was emitted for this store". What it actually says is "this module contains no getelementptr inbounds i8 anywhere".
Why that breaks
The shadow-stack lowering's inline slot addressing (#7088) computes the slot address with getelementptr inbounds i8, for reasons that have nothing to do with a buffer. So under PERRY_RS4GC=0 — the sweep a GC engineer runs, and the one #7493's own diagnosis was built on — sixteen tests report a stale proof that was never emitted:
native_proof_regressions/invalidation.rs (15): localset_invalidates_native_i32_alias_facts, update_invalidates_native_i32_alias_facts, localset_invalidates_min_length_facts, localset_invalidates_active_bounded_buffer_index_facts, inner_loop_bounded_buffer_fact_is_removed_after_outer_fact_invalidation, localset_invalidates_buffer_view_local_length_sources, update_invalidates_buffer_view_local_length_sources, negative_loop_counter_does_not_emit_inbounds_buffer_gep, decrementing_loop_update_does_not_emit_inbounds_buffer_gep, body_counter_mutation_does_not_emit_inbounds_buffer_gep, inclusive_length_loop_does_not_emit_inbounds_buffer_gep, inclusive_local_length_bound_does_not_use_local_length_bound_fact, negative_loop_counter_does_not_use_local_length_bound_fact, body_mutation_of_local_bound_does_not_use_local_length_bound_fact, negative_loop_counter_does_not_use_min_length_bound_fact
native_proof_buffer_views.rs (1): loop_length_bound_does_not_prove_multibyte_buffer_read_inbounds
#7493 pinned all sixteen to NativeRootsPin::native(). That is honest — the assertion is only valid under native roots today — and it stops the false alarm. It does not make the assertion right.
Why the pin is not the fix
The proxy is unsound in the other direction too. It cannot distinguish:
- a native buffer GEP for the store under test (what it means to catch), from
- a native buffer GEP for an unrelated proven access elsewhere in the same module (a false positive it would also report), from
- any other
inbounds i8 the backend happens to emit (what actually bit us).
Any future pass that emits an inbounds i8 — a string-data address, an inline-cache slot, a native field offset — turns all sixteen red for a reason unrelated to their subject, exactly as the shadow frame did.
What to build
Scope the search to the store/read site:
- cut the function body under test (the file already has
enclosing_function / init_ir style helpers), then
- find the
js_buffer_set / typed-array call the fixture drives, and assert that the operand feeding it was not produced by an inbounds GEP off the buffer's data pointer — i.e. follow the data-flow rather than grepping the text. temp_root_operand_temporaries.rs already walks defs backwards this way (def_of / reaches_alloc), so the technique is in-tree.
Then remove the sixteen NativeRootsPin::native() pins: the fact-invalidation contract is lowering-independent and should be asserted that way.
Acceptance
- The sixteen pass under BOTH lowerings, unpinned.
- Sabotage-checked: restoring the stale proof (letting the invalidated fact through) must fail each of them.
grep -rn 'contains("getelementptr inbounds i8")' crates/perry-codegen/tests/ returns nothing module-scoped.
Split out of #7493, which worked around this with a pin. The workaround is correct for today's default but the underlying assertion is wrong, and it will misfire again.
The assertion
crates/perry-codegen/tests/native_proof_regressions.rs:Same shape at
native_proof_regressions.rs:846and twice innative_proof_buffer_views.rs(:606,:682).The intent is "no native buffer GEP was emitted for this store". What it actually says is "this module contains no
getelementptr inbounds i8anywhere".Why that breaks
The shadow-stack lowering's inline slot addressing (#7088) computes the slot address with
getelementptr inbounds i8, for reasons that have nothing to do with a buffer. So underPERRY_RS4GC=0— the sweep a GC engineer runs, and the one #7493's own diagnosis was built on — sixteen tests report a stale proof that was never emitted:native_proof_regressions/invalidation.rs(15):localset_invalidates_native_i32_alias_facts,update_invalidates_native_i32_alias_facts,localset_invalidates_min_length_facts,localset_invalidates_active_bounded_buffer_index_facts,inner_loop_bounded_buffer_fact_is_removed_after_outer_fact_invalidation,localset_invalidates_buffer_view_local_length_sources,update_invalidates_buffer_view_local_length_sources,negative_loop_counter_does_not_emit_inbounds_buffer_gep,decrementing_loop_update_does_not_emit_inbounds_buffer_gep,body_counter_mutation_does_not_emit_inbounds_buffer_gep,inclusive_length_loop_does_not_emit_inbounds_buffer_gep,inclusive_local_length_bound_does_not_use_local_length_bound_fact,negative_loop_counter_does_not_use_local_length_bound_fact,body_mutation_of_local_bound_does_not_use_local_length_bound_fact,negative_loop_counter_does_not_use_min_length_bound_factnative_proof_buffer_views.rs(1):loop_length_bound_does_not_prove_multibyte_buffer_read_inbounds#7493 pinned all sixteen to
NativeRootsPin::native(). That is honest — the assertion is only valid under native roots today — and it stops the false alarm. It does not make the assertion right.Why the pin is not the fix
The proxy is unsound in the other direction too. It cannot distinguish:
inbounds i8the backend happens to emit (what actually bit us).Any future pass that emits an
inbounds i8— a string-data address, an inline-cache slot, a native field offset — turns all sixteen red for a reason unrelated to their subject, exactly as the shadow frame did.What to build
Scope the search to the store/read site:
enclosing_function/init_irstyle helpers), thenjs_buffer_set/ typed-array call the fixture drives, and assert that the operand feeding it was not produced by aninboundsGEP off the buffer's data pointer — i.e. follow the data-flow rather than grepping the text.temp_root_operand_temporaries.rsalready walks defs backwards this way (def_of/reaches_alloc), so the technique is in-tree.Then remove the sixteen
NativeRootsPin::native()pins: the fact-invalidation contract is lowering-independent and should be asserted that way.Acceptance
grep -rn 'contains("getelementptr inbounds i8")' crates/perry-codegen/tests/returns nothing module-scoped.