Signal Mutation in Loop
Detects self-referential signal mutation inside loops, where prover-controlled iteration counts allow arbitrary final values.
Signal Mutation in Loop
Overview
Remediation Guide: How to Fix Signal Mutation
The signal mutation detector reports two related patterns. The first is self-referential mutation, in which a signal is assigned via <-- using an expression that references the same signal (s <-- f(s)). The second is state leakage through intermediate signals, where an intermediate assigned via <-- never feeds any constraint that ties it to an output.
Both patterns depend on the fact that <-- emits no R1CS constraints. The prover is trusted to run the witness generator, but the verifier has no way to check that the intermediate values followed the intended recurrence.
Why This Is an Issue
Loops in Circom execute during compilation: the parser unrolls the loop body, and the R1CS contains one copy of the body per iteration. When the loop body mutates a signal self-referentially under <--, the unrolled form contains several assignments to the same signal — each one overwriting the previous witness value without generating any constraint. The final value is whatever the last unrolled assignment computes, and there is nothing tying that value to the initial state of the signal.
A malicious prover can exploit this by computing the unrolled recurrence honestly for one input and substituting a different final value. The R1CS does not notice because the only constraint involving the mutated signal is whatever downstream code does with its final value.
State leakage through unconstrained intermediates is a milder form of the same problem. An intermediate holds a value the developer considered important enough to name, but because the value is never pinned by a constraint, the prover can substitute any other value that satisfies the downstream constraints.
How to Resolve
Replace per-iteration mutation with per-iteration fresh signals. The canonical Circom idiom is an accumulator array sized to the loop count plus one.
// Vulnerable: self-referential mutation in a loop
template Sum(N) {
signal input xs[N];
signal acc;
signal output total;
acc <-- 0;
for (var i = 0; i < N; i++) {
acc <-- acc + xs[i]; // overwrites each iteration
}
total <== acc;
}
// Fixed: accumulator array, each step constrained
template Sum(N) {
signal input xs[N];
signal acc[N + 1];
signal output total;
acc[0] <== 0;
for (var i = 0; i < N; i++) {
acc[i + 1] <== acc[i] + xs[i];
}
total <== acc[N];
}
For intermediates that must use <-- (typically non-quadratic hints), add an explicit === that binds the hint to a constrained expression.
Sample Sigvex Output
{
"detector": "signal-mutation-in-loop",
"severity": "high",
"confidence": 0.90,
"title": "Self-referential signal mutation in template `Sum`",
"template": "Sum",
"line": 8,
"signal": "acc",
"description": "Signal `acc` is assigned via `<--` with a right-hand expression that references `acc` itself. Combined with the loop, this allows the prover to control the final value because no constraint binds successive iterations.",
"recommendation": "Use an accumulator array indexed by the loop variable, with each element constrained via `<==`."
}
Detection Methodology
For each template, the detector first collects the set of signals that participate in any <== or === constraint — these are the “constrained” signals. It then scans unconstrained assignments (<--) and flags those whose right-hand expression names the same signal that appears on the left-hand side. Self-referential assignments to signals that are not also constrained elsewhere are reported as signal-mutation-in-loop at High severity.
A second pass walks all <-- assignments and reports intermediates whose left-hand signal never appears in any constraint connecting it to an output, classified as state-leakage-intermediate at Medium severity.
Limitations
- The parser unrolls loops before analysis, so the detector sees per-iteration assignments rather than loop structure. Loop bounds that depend on runtime inputs are handled conservatively.
- Self-reference through a temporary variable (
t <-- s + 1; s <-- t) is not detected unless the temporary itself has no constraints. - State leakage detection is conservative: an intermediate that reaches an output through a chain of sub-components may be reported when the chain crosses template boundaries.
Related Detectors
- Feedback Loop — cyclic signal dependencies
- Signal Reuse — multiple assignments to the same signal
- Prover-Controlled Loop Bound — loop counts influenced by witness values