GHSA-3whf-vgf2-9w6g
GHSA-3whf-vgf2-9w6g is a security vulnerability in zaino-state. O3 Security confirms whether GHSA-3whf-vgf2-9w6g is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
zaino-state has a Non-Finalized State Reorg — No Cycle Detection or Depth Limit
Blast Radius
zaino-stateReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.
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
async fn handle_reorg(
&self,
working_snapshot: &mut NonfinalizedBlockCacheSnapshot,
block: &impl Block,
) -> Result<IndexedBlock, SyncError> {
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? // <-- LINE 459
} else {
prev_block
}
}
None => {
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? // <-- LINE 483
}
};
let indexed_block = block.to_indexed_block(&prev_block, self).await?;
working_snapshot.add_block_new_chaintip(indexed_block.clone());
Ok(indexed_block)
}
Infinite loop via self-referencing block:
- A compromised validator serves a block
BwhereB.prev_hash == B.hash. handle_reorgis called withB.get_block_by_hash_bytes_in_serialized_order(B.prev_hash)findsBitself inworking_snapshot.blocks.- Check: is
B.hashinworking_snapshot.heights_to_hashes? IfBis 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::pinfuture 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
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"); // <-- LINE 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.
PoC
- Run a regtest.
- Serve a block where
header.previous_block_hash == block.hash(). - Zaino's
NonFinalizedState::syncentershandle_reorgand 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::ReorgFailureif exceeded:const MAX_REORG_DEPTH: usize = 1000; - Track visited hashes in a
HashSet<BlockHash>during traversal to detect cycles and abort with an error. - Replace
.expect("empty snapshot impossible")with a properErr(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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🦀crates.io | zaino-state | all versions | 0.4.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for zaino-state. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update zaino-state to 0.4.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3whf-vgf2-9w6g is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-3whf-vgf2-9w6g is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-3whf-vgf2-9w6g. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-3whf-vgf2-9w6g in your dependencies?
O3 detects GHSA-3whf-vgf2-9w6g across crates.io dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.