Skip to content

fix(bb): don't corrupt the builder in DetectsUnconstrainedWitnesses (fixes nightly debug build)#24767

Draft
AztecBot wants to merge 1 commit into
nextfrom
cb/fix-acir-components-check-debug-nightly
Draft

fix(bb): don't corrupt the builder in DetectsUnconstrainedWitnesses (fixes nightly debug build)#24767
AztecBot wants to merge 1 commit into
nextfrom
cb/fix-acir-components-check-debug-nightly

Conversation

@AztecBot

Copy link
Copy Markdown
Collaborator

Problem

The Nightly Debug Build has failed every night since 2026-07-15 — runs 148, 149, 150. The GitHub Actions log only shows exit status 134; the real failure is a single aborting test:

FAILED: acir_components_check_tests AcirComponentsCheckTest.DetectsUnconstrainedWitnesses (code: 134)

/usr/include/c++/13/debug/vector:442:
  reference std::vector<unsigned int>::operator[](size_type)
Error: attempt to subscript container with out-of-bounds index 10, but container only holds 9 elements.

134 = SIGABRT: _GLIBCXX_DEBUG's checked operator[] catching a genuine out-of-bounds read.

Root cause

The test fabricated an invalid builder state:

auto builder = create_circuit<AcirComponentsCheckBuilder>(program);
// Corrupt the circuit
builder.real_variable_index.resize(9);

real_variable_index holds one entry per circuit variable. For this circuit create_circuit allocates 19 variables, and the arithmetic block's gates reference variables 8–18 (witnesses 8/9, the constant-zero variable 10, and the default pairing-point variables 11–18). Truncating the vector to 9 entries throws away the mapping for 10 variables that gates still point at.

ComponentsChecker::check() then runs cdg::StaticAnalyzer_ before it reaches the witness >= real_variable_index.size() guard the resize was aiming at, and the analyzer maps every gate wire through real_variable_index:

components_check.cpp:31  StaticAnalyzer_ analyzer(builder_)
graph.cpp:394            process_execution_trace()
graph.cpp:84             extract_gate_variables(index=0, kind=Arith)
graph.hpp:105            return circuit_builder.real_variable_index[variable_index];  // [10] on size 9

So the very first arithmetic gate reads out of bounds — hence index 10, not 9. This is plain UB: release builds read past the end and the test "passes"; the debug preset sets -D_GLIBCXX_DEBUG (CMakePresets.json), which turns it into an abort.

Why it started now: nothing regressed in bb. acir_components_check was developed in the private repo and landed on public next on 2026-07-14 via 3f999a4dc4c chore(merge): Private next to public (#24686). The last green nightly (e1d2ed64d81) predates it — the module isn't in that tree at all. Run 148 was simply the first debug run that ever executed this test, and it aborted immediately. The resize(9) was never valid here; it only ever worked where the variable layout happened to keep every gate below the truncation point.

Fix

Test-only. The production code is correct as written — the analyzer is entitled to assume real_variable_index has one entry per variable, and bounds-checking that hot path to accommodate a fabricated builder would be wrong.

Instead the test now models the bug it actually cares about — create_circuit dropping a constraint — without corrupting the builder: the ACIR handed to the checker links a witness pair that the built circuit never allocated a variable for, which is exactly the witness >= real_variable_index.size()UNCONSTRAINED path. The index is derived from builder.real_variable_index.size(), so it can't silently rot as the variable count changes — the failure mode that caused this bug.

Testing

Debug preset (the failing configuration), on the failing commit f13496502a1:

  • Red: reproduced the exact abort locally (index 10, container only holds 9) and confirmed the frame via lldb — graph.hpp:105 under the StaticAnalyzer_ constructor.
  • Green: all 10 tests in acir_components_check_tests pass.
  • Not vacuous: mutating the witness >= real_variable_index.size() guard in components_check.cpp to hand back a real CC id makes the rewritten test fail, so it genuinely covers that guard.
cmake --preset debug && cmake --build build-debug --target acir_components_check_tests
./build-debug/bin/acir_components_check_tests
[==========] 10 tests from 1 test suite ran. (6934 ms total)
[  PASSED  ] 10 tests.

Notes for the reviewer

  • The nightly's test runner fail-fasts, so it stopped at this abort. If further debug-only (_GLIBCXX_DEBUG) failures are hiding behind it, the next nightly will surface them; this change clears the first one.
  • The debug preset is not built by PR CI, so this PR's checks won't exercise the fix — it was verified locally as above.
  • Targeting next as requested for the nightly fix, rather than the usual merge-train/barretenberg base for barretenberg/**.

Created by claudebox · group: slackbot · Slack thread

@AztecBot AztecBot added ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claudebox Owned by claudebox. it can push to this PR. labels Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claudebox Owned by claudebox. it can push to this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant