# Poseidon2 verify batch: top-level rows can come right after an inside_row but before in- side_row end

Poseidon2 verify-batch AIR lacked a constraint forcing inside_row blocks to stay contiguous, letting incorporate_sibling rows bypass top-level initialization checks and forge row hashing.

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

**Target:** Poseidon2 verify batch AIR constraints (poseidon2/air.rs)
**Root cause:** State-transition constraints between consecutive inside_row rows only fire when the current row is inside_row and not end_inside_row, but nothing forces the next row to also be inside_row; an incorporate_sibling row can be inserted mid-block, skipping the top-level initialization checks that only run after an end row.
**Steps:**
1. Read poseidon2/air.rs#L155-L172: constraints gated by `when(inside_row - end_inside_row)` only assert equality of next.start_timestamp, opened_base_pointer, opened_element_size_inv, initial_opened_index, very_first_timestamp.
2. Note no constraint asserts `next.inside_row == inside_row` for non-terminal inside_row rows.
3. Construct a verify-batch trace where an inside_row (not end_inside_row) is followed directly by an incorporate_sibling row.
4. Because the previous row was not an end row, top-level initialization checks are skipped for the sibling row, allowing an unconstrained/forged row hash.
5. Fix: add `builder.when(inside_row).when(ONE - end_inside_row).assert_one(next.inside_row)` (commit 2fe9ad79).
**How found:** Code review of the AIR state-transition constraints: noticed the guard `inside_row - end_inside_row` constrains next-row fields but never constrains the next row's type, so the block's contiguity is unenforced.
**Exploitation:** A malicious prover crafts a Poseidon2 verify-batch witness where inside_row blocks are interrupted by incorporate_sibling rows, bypassing top-level phase initialization constraints and producing an accepted but incorrect row hash, breaking soundness of the batch verification.
**Insight:** In AIR/zkVM constraint systems, when a transition guard is `current_state AND NOT terminal`, always also constrain the next row's state/type selector; otherwise an attacker can exit the block early and skip initialization checks that only run after a terminal row.
