New files:
- docs/reviews/avmisa_audit_report.md: Full AVMIsa rigidity & vulnerability audit
(8 surfaces found, 5 mitigated, 2 open)
- formal/BindingSite/BindingSiteTypes.lean: Binding site type definitions
- formal/CoreFormalism/GoormaghtighEnumeration.lean: Goormaghtigh conjecture enumeration
- formal/SilverSight/HachimojiCharClass.lean: Hachimoji character classification
- formal/SilverSight/HachimojiN8.lean: N=8 minimal alphabet theorem
- formal/SilverSight/HachimojiN8Bridge.lean: Bridge between HachimojiCharClass and N8
- formal/SilverSight/PhiConsistency.lean: Phi pipeline consistency checks
- formal/SilverSight/PhiDNALayout.lean: DNA layout for Phi-encoded equations
- formal/SilverSight/PhiPipelineReceipt.lean: Receipt format for Phi pipeline
8.1 KiB
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 | Q16_16 type definition + arithmetic kernels |
| Step.lean | AVM instruction semantics |
| State.lean | Machine state (stack, locals, pc) |
| Run.lean | Fuel-bounded execution loop |
§1 — Arithmetic Vulnerability Surfaces
V1. Multiplication Intermediate Overflow ⚠️ CRITICAL
Definition:
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(raw2147418112),b = 2.0(raw131072)- Intermediate:
2147418112 \times 131072 = 2.81 \times 10^{17}→ overflowsint32 - Even
int64handles this, but a 32-bit C/Verilog backend will produce garbage
Mitigation:
- Backend spec rule: All AVM backends MUST use ≥ 64-bit intermediate storage for
mulanddiv - Lean-side: Add a
#evalwitness confirming the worst-case product fits inInt64:#eval (q16MaxRaw * q16MaxRaw) -- should be ≤ 2^62, fits Int64
V2. Division Intermediate Overflow ⚠️ CRITICAL
Definition:
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}(raw2147483647),b = 1.0(raw65536)- Intermediate:
2147483647 \times 65536 = 1.407 \times 10^{14}→ overflowsint32
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):
- Add a
StepError.divisionByZeroerror variant and makedivSatQ16returnOutcome.err StepError.divisionByZeroinstead of a sentinel value - Add a
Prim.isDivSafeinstruction that pushestrueif TOS ≠ 0, allowing programs to guard division explicitly - Accept the sentinel but document it as a backend invariant and add a
#evalwitness confirmingdiv maxVal zero = maxVal
Important
Option 1 is the most rigid but changes the
Outcomecontract. 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(raw32768) -
Lean:
(-65536 \times 32768) / 65536 = -2147483648 / 65536 = -32768(truncation) -
Python:
(-65536 \times 32768) // 65536 = -32768(same here, but...) -
araw= -3,braw= 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:
- BioSight Python shim MUST use
int(x / y)(truncation) instead ofx // y(floor) for all Q16_16 operations - 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 tomaxVal(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:
- Document this as a known boundary: the extreme negative value
-2^{31}is outside the invertible range - Restrict the encoding domain to
[-2^{31}+1, 2^{31}-1]so negation is always invertible
V6. Comparison Signedness ⚠️ LOW
ltQ16 in Step.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:
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:
- Add a
maxStackDepth : Natfield toStateand check it on everypush - Add a new
StepError.stackOverflowvariant - 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:
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:
- Return
Outcome.err StepError.missingLocalon out-of-bounds store (matching the behavior ofload, which already returnsmissingLocal) - 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
- V4 (Rounding) — Fix the Python shim first. This is the most likely source of real DNA sequence divergence between Lean and Python today.
- V1/V2 (Overflow) — Add 64-bit mandate to backend spec +
#evalwitnesses. - V8 (Silent store) — Change
setLocalto error on out-of-bounds. - V3 (Div-by-zero) — Add
isDivSafeguard primitive or error variant. - V7 (Stack growth) — Add stack depth limit or accept fuel as bound.
- V5/V6 — Document as known boundaries.