GitHub Advisoryadvisory · crates.io

GHSA-3whf-vgf2-9w6g zaino-state has a Non-Finalized State Reorg — No Cycle Detection or Depth Limit

MEDIUM Published 2026-07-31 Modified 2026-07-31 Official page ↗
Ecosystem
crates.io
Packages
zaino-state
Severity
Moderate
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Weaknesses
CWE-770
Reviewed
2026-07-31

Affected packages

• crates.io zaino-state: < 0.4.1 → fixed in 0.4.1

Description

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` ```rust async fn handle_reorg( &self, working_snapshot: &mut NonfinalizedBlockCacheSnapshot, block: &impl Block, ) -> Result { let prev_block = match working_snapshot .get_block_by_hash_bytes_in_serialized_order(block.prev_hash_bytes_serialized_order()) .cloned() { Some(prev_block) => { if !working_snapshot .heights_to_hashes .values() .any(|hash| hash == prev_block.hash()) { Box::pin(self.handle_reorg(working_snapshot, &prev_block)).await? // { let prev_block = self .source .get_block(HashOrHeight::Hash( zebra_chain::block::Hash::from_bytes_in_serialized_order( block.prev_hash_bytes_serialized_order(), ), )) .await .map_err(|e| { ... })? .ok_or(SyncError::ValidatorConnectionError(...))?; Box::pin(self.handle_reorg(working_snapshot, &*prev_block)).await? // 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` ```rust new_snapshot.remove_finalized_blocks(finalized_height); let best_block = &new_snapshot .blocks .values() .max_by_key(|block| block.chainwork()) .cloned() .expect("empty snapshot impossible"); // ` during traversal to detect cycles and abort with an error. 3. 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.

Related

Weakness · CWE (1)

  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References