# 🔒 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