# sha256-air doesn't constrain final_hash at the last block, but this value is read by sha256_- chip

OpenVM sha256-air left final_hash unconstrained on the last block, letting a malicious prover forge any sha256(x)=y digest accepted by the sha256 chip.

- Payout: Contest reward
- Severity: high
- Program: OpenVM
- Credited: ed255
- Disclosed: 2025-03-24
- Write-up quality: 8/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_openvm_january2025.pdf#3.1.4
- Library page: https://bugbountylibrary.com/r/cantina%3Aopenvm_january2025%3A3.1.4
- Tags: domain:web3 impact:integrity kind:contest platform:cantina sev:high tech:rust technique:incomplete-fix technique:source-review vuln:misconfiguration

**Target:** zkVM SHA-256 AIR constraint / sha256 chip extension
**Root cause:** The AIR constrains final_hash on every block except the last, where witness generation assigns it freely; the sha256 chip reads this unconstrained column as the hash result.
**Steps:**
1. Patch trace.rs to set `final_hash[0]=0` when `is_last_block`.
2. Patch sha256_chip mod.rs to zero the first 4 bytes of `hasher.finalize()` output.
3. Comment out the assert in tests.rs comparing sha256_solve output to memory read.
4. Run `sha256_chip::tests::rand_sha256_test`; verification passes despite mismatched digest.
**How found:** Code review of the AIR constraints: noticed final_hash is constrained per-block except on final_block, while the chip extension consumes that column as the result, creating a soundness gap.
**Exploitation:** A malicious prover sets the unconstrained final_hash column to any desired digest on the final block; the sha256 chip extension reads that column as the hash result, so the proof attests sha256(x)=y for arbitrary x,y.
**Insight:** In AIR/zkVM circuits, audit boundary rows (first/last block, digest rows) for constraints that are conditionally skipped; any column read by a chip extension must be constrained on every row it is consumed from.
