# AVMIsa Manual Math Audit — Vulnerability Report

This report walks through every arithmetic path in the AVM ISA step-by-step,
identifies how each can go wrong, and proposes concrete mitigations.

---

## Source Files Under Audit

| File | Role |
|------|------|
| [FixedPoint.lean](file:///home/allaun/SilverSight/Core/SilverSight/FixedPoint.lean) | Q16_16 type definition + arithmetic kernels |
| [Step.lean](file:///home/allaun/SilverSight/formal/SilverSight/AVMIsa/Step.lean) | AVM instruction semantics |
| [State.lean](file:///home/allaun/SilverSight/formal/SilverSight/AVMIsa/State.lean) | Machine state (stack, locals, pc) |
| [Run.lean](file:///home/allaun/SilverSight/formal/SilverSight/AVMIsa/Run.lean) | Fuel-bounded execution loop |

---

## §1 — Arithmetic Vulnerability Surfaces

### V1. Multiplication Intermediate Overflow ⚠️ CRITICAL

**Definition:**
```lean
def mul (a b : Q16_16) : Q16_16 := ofRawInt ((a.toInt * b.toInt) / q16Scale)
```

**The problem:** `a.toInt` and `b.toInt` are both in $[-2^{31}, 2^{31}-1]$.
Their product can reach $2^{62}$, which is fine in Lean (arbitrary-precision `Int`),
but **any backend using 32-bit integers will silently overflow**.

**Worked example:**
- $a = 32767.0$ (raw $2147418112$), $b = 2.0$ (raw $131072$)
- Intermediate: $2147418112 \times 131072 = 2.81 \times 10^{17}$ → overflows `int32`
- Even `int64` handles this, but a 32-bit C/Verilog backend will produce garbage

**Mitigation:**
1. **Backend spec rule:** All AVM backends MUST use ≥ 64-bit intermediate storage for `mul` and `div`
2. **Lean-side:** Add a `#eval` witness confirming the worst-case product fits in `Int64`:
   ```lean
   #eval (q16MaxRaw * q16MaxRaw) -- should be ≤ 2^62, fits Int64
   ```

---

### V2. Division Intermediate Overflow ⚠️ CRITICAL

**Definition:**
```lean
def div (a b : Q16_16) : Q16_16 :=
  if b.toInt = 0 then infinity else ofRawInt ((a.toInt * q16Scale) / b.toInt)
```

**The problem:** `a.toInt * 65536` can reach $2^{31} \times 2^{16} = 2^{47}$,
which overflows `int32` but fits `int64`.

**Worked example:**
- $a = \text{maxVal}$ (raw $2147483647$), $b = 1.0$ (raw $65536$)
- Intermediate: $2147483647 \times 65536 = 1.407 \times 10^{14}$ → overflows `int32`

**Mitigation:** Same as V1 — mandate 64-bit intermediates for all backends.

---

### V3. Division by Zero Sentinel ⚠️ MEDIUM

**Definition:** `div(a, 0)` returns `infinity` (which is `maxVal = 2147483647`).

**The problem:** This is a **silent sentinel** — the caller cannot distinguish
"the result was legitimately very large" from "division by zero occurred."
In a bidirectional encoding pipeline, this could silently corrupt a DNA sequence
if a zero denominator arises from a degenerate AST.

**Mitigation options (pick one):**
1. **Add a `StepError.divisionByZero`** error variant and make `divSatQ16` return
   `Outcome.err StepError.divisionByZero` instead of a sentinel value
2. **Add a `Prim.isDivSafe`** instruction that pushes `true` if TOS ≠ 0, allowing
   programs to guard division explicitly
3. **Accept the sentinel** but document it as a backend invariant and add a
   `#eval` witness confirming `div maxVal zero = maxVal`

> [!IMPORTANT]
> Option 1 is the most rigid but changes the `Outcome` contract.
> Option 2 preserves totality while giving programs the ability to guard.

---

### V4. Rounding Direction Mismatch ⚠️ CRITICAL (Cross-Platform)

**Lean's integer division:** Truncation toward zero (same as C99 `/`).
- $-7 / 2 = -3$ (truncates toward zero)

**Python's `//` operator:** Floor division (rounds toward $-\infty$).
- $-7 // 2 = -4$ (floors)

**Impact on `mul`:**
- $a = -1$ (raw $-65536$), $b = 0.5$ (raw $32768$)
- Lean: $(-65536 \times 32768) / 65536 = -2147483648 / 65536 = -32768$ (truncation)
- Python: $(-65536 \times 32768) // 65536 = -32768$ (same here, but...)

- $a$ raw $= -3$, $b$ raw $= 2$:
- Lean: $(-3 \times 2) / 65536 = -6 / 65536 = 0$ (truncates toward zero)
- Python: $(-6) // 65536 = -1$ (floors toward $-\infty$)
- **Result differs by 1 LSB** → DNA sequence divergence

**Mitigation:**
1. **BioSight Python shim MUST use `int(x / y)` (truncation) instead of `x // y` (floor)**
   for all Q16_16 operations
2. Add a cross-platform conformance test: compute `mul(ofRawInt(-3), ofRawInt(2))`
   in both Lean and Python and assert identical raw values

---

### V5. Saturation Asymmetry ⚠️ LOW

**Range:** $[-2147483648, 2147483647]$ — the negative range has 1 more value than
the positive range. This means:
- `neg(minVal)` = `ofRawInt(2147483648)` → saturates to `maxVal` ($2147483647$)
- So `neg(neg(minVal)) ≠ minVal` — negation is not an involution at the boundary

**Impact:** For bidirectional encoding, if a DNA base encodes `minVal` and the
decoder negates twice, it will get `maxVal - 1` instead. This is a 1-LSB error
at the extreme boundary.

**Mitigation:**
1. Document this as a known boundary: the extreme negative value $-2^{31}$ is
   outside the invertible range
2. Restrict the encoding domain to $[-2^{31}+1, 2^{31}-1]$ so negation is always invertible

---

### V6. Comparison Signedness ⚠️ LOW

**`ltQ16` in Step.lean:**
```lean
let res := y.toInt < x.toInt
```

This uses `Int` comparison which is correct for signed Q16_16. However, if a
backend implements this using unsigned comparison on the raw bits, large negative
values would appear positive and comparisons would be inverted.

**Mitigation:** Backend spec must state: "All Q16_16 comparisons use **signed**
integer comparison on the raw representation."

---

## §2 — Execution & Memory Vulnerability Surfaces

### V7. Unbounded Stack Growth ⚠️ MEDIUM

**State definition:**
```lean
structure State where
  pc : Nat
  stack : List AnyVal
  locals : List (Option AnyVal)
  halted : Bool
```

The stack is an unbounded `List`. A malicious or buggy program could push
indefinitely (e.g., `push; jump 0` loop), consuming unbounded memory before
fuel runs out.

**Worst case:** With fuel $= N$, the stack can grow to depth $N$ values
($N \times 12$ bytes for the tag+payload). At fuel $= 2^{20}$, that's ~12 MB.

**Mitigation:**
1. Add a `maxStackDepth : Nat` field to `State` and check it on every `push`
2. Add a new `StepError.stackOverflow` variant
3. Or: accept fuel as the implicit bound and document the relationship
   $\text{max\_stack} \leq \text{fuel}$

---

### V8. Local Frame Silent No-Op on Out-of-Bounds Store ⚠️ MEDIUM

**`setLocal` definition:**
```lean
def setLocal (s : State) (i : Nat) (v : AnyVal) : State :=
  if i < s.locals.length then
    { s with locals := s.locals.set i (some v) }
  else
    s  -- silent no-op!
```

If `i ≥ locals.length`, the store is silently dropped. The program continues
with stale data, which could lead to incorrect DNA encodings without any error signal.

**Mitigation:**
1. Return `Outcome.err StepError.missingLocal` on out-of-bounds store (matching
   the behavior of `load`, which already returns `missingLocal`)
2. Or: pre-allocate the local frame to a fixed size at program start

---

## §3 — Summary Matrix

| ID | Surface | Severity | Status |
|----|---------|----------|--------|
| V1 | mul intermediate overflow (32-bit backends) | 🔴 Critical | Open |
| V2 | div intermediate overflow (32-bit backends) | 🔴 Critical | Open |
| V3 | div-by-zero returns silent sentinel | 🟡 Medium | Open |
| V4 | Rounding direction mismatch (Lean vs Python) | 🔴 Critical | Open |
| V5 | Negation non-involution at minVal boundary | 🟢 Low | Documented |
| V6 | Comparison signedness in backends | 🟢 Low | Documented |
| V7 | Unbounded stack growth | 🟡 Medium | Open |
| V8 | Silent no-op on out-of-bounds store | 🟡 Medium | Open |

## §4 — Recommended Priority Order

1. **V4 (Rounding)** — Fix the Python shim first. This is the most likely source
   of real DNA sequence divergence between Lean and Python today.
2. **V1/V2 (Overflow)** — Add 64-bit mandate to backend spec + `#eval` witnesses.
3. **V8 (Silent store)** — Change `setLocal` to error on out-of-bounds.
4. **V3 (Div-by-zero)** — Add `isDivSafe` guard primitive or error variant.
5. **V7 (Stack growth)** — Add stack depth limit or accept fuel as bound.
6. **V5/V6** — Document as known boundaries.
