Skip to content

fix(ast_fuzzer): bound literal vector indices, and diagnose leftover ACIR memory operations - #13478

Draft
AztecBot wants to merge 8 commits into
masterfrom
cb/fix-mem2reg-dead-reference-arrays
Draft

fix(ast_fuzzer): bound literal vector indices, and diagnose leftover ACIR memory operations#13478
AztecBot wants to merge 8 commits into
masterfrom
cb/fix-mem2reg-dead-reference-arrays

Conversation

@AztecBot

@AztecBot AztecBot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Background

The nightly AST fuzzer run found seed 0xbc347a7e00100000 failing valid_after_pass with Load or Store instruction found.

A dead MakeArray holding a reference makes the allocation inside it look first-class to mem2reg, so the allocation is never promoted and the store reaches ACIR generation. Original RCA: https://gist.github.com/AztecBot/fc70ad4799ca0e246832bf77d00b5c13

Reducing the seed's program showed the shape is not reachable from Noir: keeping the reference-holding aggregate alive that far requires a dynamic index into it, which verify_no_dynamic_indices_to_references rejects with a clean error. So rather than add handling in mem2reg for a shape that cannot occur, this PR fixes the generator that produced it and makes the situation diagnosable if it ever recurs. mem2reg is unchanged.

1. The generator hole

gen_vector_access picks the index one of two ways. The gen_expr branch sets in_no_dynamic (banning dynamic indices into reference-holding items, per #8888) and applies avoid_index_out_of_bounds. The other branch — a plain literal, taken whenever max_depth == 0 or a coin flip — did neither: it emitted an arbitrary u32 with no bound and no regard for the item type.

That is exactly what the seed did: c[1168058454_u32] on a constrained [([Field; 2], …, &[&str<1>; 2])]. A constant out-of-bounds index into a vector is not rejected at compile time (unlike an array, the length isn't statically known), so it survives to ACIR generation as an index that could not be simplified — precisely the case avoid_index_out_of_bounds already documents as leaving reference allocations behind.

The fix hoists the in_no_dynamic computation above both branches and routes the literal branch through avoid_index_out_of_bounds too, matching what gen_index already does for arrays. Two consequences:

  • For a constrained vector whose item type contains a reference, in_no_dynamic is set, so avoid_index_out_of_bounds returns true unconditionally and the index is always % len. The shape can no longer be generated.
  • On ordinary vectors the literal branch now honours Config::avoid_index_out_of_bounds at all, which it previously ignored — so setting that config actually takes effect there. Out-of-bounds literal indices are still generated at the usual 10% rate when it is off.

2. A useful message on the assertion

assert_not_load_or_store said only Load or Store instruction found. It now reports which function, which address, and what the address is a reference to:

Store instruction found in ACIR function 'main': the address v0 of type &mut Field was not
promoted by mem2reg, so a memory operation reached ACIR generation

Its doc comment records why an address can survive that far and points at verify_no_dynamic_indices_to_references as the user-facing check anything reaching the assertion has already passed. load_or_store_assertion_names_the_function_and_address pins the message.

3. Coverage for the validation that keeps this out of SSA

The validator already had unit tests (dynamic_array_of_2_mut_bools, error_on_index_overflow, the vector-intrinsic cases) and compile_failure programs for vector.remove / vector.insert. Two gaps:

  • No end-to-end test for a plain dynamic array index, the seed's shape. Added test_programs/compile_failure/dynamic_array_index_references: *r[i] on a [&mut Field; 2]. The harness snapshots stderr and asserts it contains no panic, pinning that this stays a clean rejection.
  • No test for the ArraySet arm of the validator, only ArrayGet. Added dynamic_array_set_of_references.

Evidence

  • The identical hole is present at 2702a1e82e6, the revision the seed was found on — the generated AST there is c[1168058454_u32], a bare literal from that branch.
  • Applying this fix at that revision makes seed 0xbc347a7e00100000 pass. (Taken alone this is weak: the extra avoid_index_out_of_bounds call shifts entropy consumption, so the seed generates a different program. The structural argument above is what carries it.)
  • The seed's reduced program still trips the assertion when fed to the pipeline directly — as it should, since mem2reg is deliberately unchanged. What changes is that the fuzzer no longer builds it, and the panic now names the function and address.

Tests

  • cargo fmt --all -- --check
  • cargo clippy -p noir_ast_fuzzer -p noirc_evaluator --all-targets -- -D warnings
  • cargo test -p noir_ast_fuzzer — 25 + 4 passed
  • cargo test -p noirc_evaluator — 1948 passed, 15 ignored
  • cargo test -p nargo_cli --test execute — 9280 passed
  • Fuzz targets with the generator change: valid_after_pass 420s, acir_vs_brillig 360s, pass_vs_prev 360s — all clean

Created by claudebox · group: slackbot · requested by Tom (@TomAFrench) · Slack thread

@AztecBot AztecBot added AST Fuzzer claude-review Adversarial ClaudeBox review pending claudebox labels Aug 6, 2026
@AztecBot AztecBot changed the title fix(ssa): remove dead reference arrays between mem2reg iterations chore(ssa): diagnose leftover ACIR memory operations, pin the dynamic reference-index rejection Aug 12, 2026
@AztecBot AztecBot changed the title chore(ssa): diagnose leftover ACIR memory operations, pin the dynamic reference-index rejection fix(ast_fuzzer): bound literal vector indices, and diagnose leftover ACIR memory operations Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AST Fuzzer claude-review Adversarial ClaudeBox review pending claudebox

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants