GHSA-3whf-vgf2-9w6g zaino-state has a Non-Finalized State Reorg — No Cycle Detection or Depth Limit
Summary`NonFinalizedState::handle_reorg` is a recursive, unbounded async function that traverses parent blocks until it finds a common ancestor on the main chain. It has **no recursion depth limit** and **no cycle detection**. A malicious or buggy validator can serve a block whose `previous_block_hash` points back to itself (or forms a cycle with other blocks), causing `handle_reorg` to infinite-loop, consuming 100% CPU and never making sync progress. Additionally, `update()` contains an `.expect("empty snapshot impossible")` that panics if the non-finalized snapshot becomes empty after trimming finalized blocks.Details**Location:** `packages/zaino-state/src/chain_index/non_finalised_state.rs:443-489`**Infinite loop via self-referencing block:**A compromised validator serves a block `B` where `B.prev_hash == B.hash`.`handle_reorg` is called with `B`.`get_block_by_hash_bytes_in_serialized_order(B.prev_hash)` finds `B` itself in `working_snapshot.blocks`.Check: is `B.hash` in `working_snapshot.heights_to_hashes`? If `B` is a new chaintip not yet on the main chain, **no**.Recurse with `prev_block` = `B` (the exact same block).This repeats forever. The async recursion builds a new `Box::pin` future each iteration, consuming heap memory and CPU.**Stack exhaustion via deep reorg:**
A deep reorg of >1000 blocks would recurse >1000 times. Each async recursion creates a new `Box::pin` future on the heap. While this won't exhaust the native stack immediately, it will allocate unbounded heap memory and CPU time, effectively DoS-ing the sync task.**`.expect("empty snapshot impossible")` panic:****Location:** `packages/zaino-state/src/chain_index/non_finalised_state.rs:543-548`If `finalized_height` is greater than or equal to all blocks in `new_snapshot.blocks`, `remove_finalized_blocks` retains only blocks at or above that height. If none exist, `new_snapshot.blocks` becomes empty. The `.expect()` then panics. While the comment claims this is "impossible," defensive programming dictates it is reachable under corruption or edge-case sync conditions.PoCRun a regtest.Serve a block where `header.previous_block_hash == block.hash()`.Zaino's `NonFinalizedState::sync` enters `handle_reorg` and infinite-loops.Sync never completes. CPU usage pegs to 100%. No new blocks are served to clients.Fix**Add an explicit recursion depth limit** (e.g., max 1000 iterations) and return `SyncError::ReorgFailure` if exceeded:**Track visited hashes** in a `HashSet<BlockHash>` during traversal to detect cycles and abort with an error.**Replace `.expect("empty snapshot impossible")`** with a proper `Err(UpdateError::DatabaseHole)` or similar error return.Additional Attack Vectors**Deep reorg DoS:** A miner with significant hash power (or a compromised validator) triggers a deep reorg. Zaino spends excessive CPU and memory in `handle_reorg`, starving the async runtime and stalling response serving.**Fork-choice manipulation:** By serving cyclic or very deep sidechains, an attacker can keep Zaino stuck in reorg handling indefinitely, preventing it from ever serving the real best chain.
CVSS v4.0 score: 6.9