# 🔒 NOIR COMPILER SSA OPTIMIZATION BUGS
## Security Audit Report - 3 Critical Issues
**Date:** October 13, 2025
**Scope:** SSA optimization passes in `compiler/noirc_evaluator/src/ssa/opt/`
---
## BUG #1: Array Set Multi-Block Unsoundness ⚠️ HIGH
**File:** `array_set.rs` (Lines 66-72, 133-135, 202-211)
**Issue:** Optimization allows entry-point functions with multiple blocks but uses single-block analysis, causing incorrect mutable markings.
**Root Cause:**
```rust
// Precondition allows multi-block entry points
if !func.runtime().is_entry_point() {
assert_eq!(reachable_blocks.len(), 1, /*...*/);
}
// But analysis is per-block without inter-block dataflow
for block in self.reachable_blocks() {
context.analyze_last_uses(block); // ← Independent analysis
}
```
**Counter-Example:**
```rust
acir(inline) entry_point fn main f0 {
b0():
v1 = make_array [Field 0]
v2 = array_set v1, index u32 0, value Field 1 // OUTPUT: v2
jmp b1
b1():
v3 = array_get v2, index u32 0 // USES v2 from b0
return v3
}
// Result: ArraySet incorrectly marked mutable (checks INPUT v1, not OUTPUT v2)
```
**Impact:** Array aliasing violations, potential memory corruption
**Fix:** Either assert entry points have 1 block, or skip multi-block entry points
---
## BUG #2: Constrain Not-Equal Operand Order 🟡 LOW
**File:** `make_constrain_not_equal.rs` (Lines 79-81)
**Issue:** Only handles `constrain v2 == u1 0`, not reversed `constrain u1 0 == v2`
**Code:**
```rust
if !context.dfg.get_numeric_constant(*rhs).is_some_and(|c| c.is_zero()) {
return; // ← Only checks RHS is zero
}
```
**Impact:** LOW - Comments (line 67) indicate frontend guarantees order, but makes pass brittle
**Fix:** Handle both operand orders:
```rust
let (eq_value, _) = if dfg.get_numeric_constant(*rhs).is_some_and(|c| c.is_zero()) {
(*lhs, *rhs)
} else if dfg.get_numeric_constant(*lhs).is_some_and(|c| c.is_zero()) {
(*rhs, *lhs)
} else {
return;
};
```
---
## BUG #3: Bit Shift Overflow Check Wrong Type 🔴 CRITICAL
**File:** `remove_bit_shifts.rs` (Lines 372-386, call site 119)
**Issue:** Checks `rhs < bit_size_of(rhs_type)` instead of `rhs < bit_size_of(lhs_type)`
**Code:**
```rust
fn enforce_bitshift_rhs_lt_bit_size(&mut self, rhs: ValueId) {
let rhs_type = self.context.dfg.type_of_value(rhs); // ← WRONG
let bit_size = rhs_type.bit_size(); // Should use LHS bit size!
// ...
let overflow = self.insert_binary(rhs, BinaryOp::Lt, max);
}
```
**Counter-Examples:**
```rust
// Case 1: FALSE NEGATIVE (accepts invalid code)
let x: u8 = 1;
let shift: u32 = 10;
x << shift;
// Current: Checks 10 < 32 ✓ PASSES (WRONG!)
// Should: Check 10 < 8 ✗ FAILS (correct - shift too large for u8)
// Case 2: FALSE POSITIVE (rejects valid code)
let x: u64 = 1;
let shift: u8 = 40;
x << shift;
// Current: Checks 40 < 8 ✗ FAILS (WRONG!)
// Should: Check 40 < 64 ✓ PASSES (correct - valid shift for u64)
```
**Why Tests Pass:** All tests use matching types (`u32 << u32`, `i32 << i32`)
**Impact:** CRITICAL - Accepts overflowing shifts OR rejects valid shifts depending on type mismatch
**Fix:**
```rust
fn enforce_bitshift_rhs_lt_bit_size(&mut self, lhs: ValueId, rhs: ValueId) {
let lhs_type = self.context.dfg.type_of_value(lhs); // ← Use LHS
let bit_size = lhs_type.bit_size();
// ... rest unchanged
}
// Call site (line 119):
bitshift_context.enforce_bitshift_rhs_lt_bit_size(lhs, rhs);
```
---
## SUMMARY
| Bug | Severity | Exploitable | Test Gap |
|-----|----------|-------------|----------|
| Array Set Multi-Block | HIGH | Depends on flattening guarantees | Missing multi-block test |
| Constrain Operand Order | LOW | Likely prevented by frontend | Missing reversed operand test |
| Bit Shift Type Check | **CRITICAL** | **YES - immediate** | Missing mismatched type tests |
**Recommended Actions:**
1. **URGENT:** Fix bit shift bug - wrong type used for overflow check
2. Add test cases with mismatched operand types
3. Clarify array set preconditions or add inter-block analysis
4. Document constrain operand order assumption