Jul 31, 2026·6 min read·4 visits
zaino-state is vulnerable to unconstrained recursive block reorganization and an unhandled panic during state cache pruning, causing denial of service.
The zaino-state crate contains two critical flaws in its block reorganization and state synchronization logic. An unbounded recursive async function handling block reorganization fails to validate cyclic relationships, enabling network peers to cause infinite loops that exhaust CPU and memory resources. Furthermore, a logical pruning error during non-finalized block cache trimming can purge all cached blocks, triggering an immediate panic and crash of the daemon.
The zaino-state crate is a core state-tracking component in the Zaino indexer, which indexes and monitors state transitions for the Zcash blockchain. The indexer relies on maintaining a non-finalized state representing potential chain reorganizations (reorgs) prior to transaction finality.
To manage state updates, the component processes incoming blocks and traces changes backward along parent hash pointers to align with the active main chain. This model introduces an attack surface where processed chain metadata influences recursive state traversal. Specifically, malicious or incorrectly formatted inputs can exploit logic paths within the state tracking mechanism.
This advisory tracks two operational vulnerabilities: an unconstrained recursive async loop in handle_reorg and an unhandled panic state within the snapshot cache trimming process. Both vulnerabilities allow an unauthenticated actor to disrupt the availability of the indexing node, leading to resource exhaustion or immediate process crashes.
The primary vulnerability arises from the implementation of NonFinalizedState::handle_reorg. When the indexer detects a block reorganization, it must verify parent relationships across blocks that are not yet finalized. It achieves this by recursively traversing backward along parent hashes using the prev_hash_bytes_serialized_order method to find a common ancestor.
Because Rust async functions cannot have an unconstrained size at compile time, recursive async functions require heap allocation via pin boxing (Box::pin). However, the implementation of handle_reorg lacked cycle detection and an explicit recursion depth limit. If a node processes a block that references itself as its parent, or a group of blocks that form a cyclic reference graph, the loop executes indefinitely.
The second vulnerability resides in NonfinalizedBlockCacheSnapshot::remove_finalized_blocks. This function trims finalized blocks from the active cache using a simple retain logic (block.height() >= finalized_height). Under extreme network forks or rapid finality shifting, if the finalized_height exceeds the height of all blocks in the cache, the pruning logic retains zero blocks. The downstream code then executes .expect("empty snapshot impossible") on an empty map, triggering an immediate process panic.
The original vulnerable implementation of the recursive async function structure lacks bounds checks or loop state. The recursive call pattern within handle_reorg was defined as follows:
// Line 459 and 483 in non_finalised_state.rs (Original)
Some(prev_block) => {
if !working_snapshot
.heights_to_hashes
.values()
.any(|hash| hash == prev_block.hash())
{
// Unbounded recursive call allocating a new heap future on every iteration
Box::pin(self.handle_reorg(working_snapshot, &prev_block)).await?
} else {
prev_block
}
}The corresponding patch introduces a recursion_count: u8 parameter initialized to 0 at the entry point of the reorganization handler. The function is modified to validate the parameter on each recursive step:
// Patched version showing recursive depth validation
async fn handle_reorg(
&self,
working_snapshot: &mut NonfinalizedBlockCacheSnapshot,
block: &impl Block,
recursion_count: u8, // Introduced recursion counter
) -> Result<IndexedBlock, SyncError> {
// Limit recursion to 110 steps to account for complete reorg margins
if recursion_count > 110 {
return Err(SyncError::ReorgFailure(
"reorg handling recursed beyond reason".to_string(),
));
}
// ...
// Recursive calls increment the counter
Box::pin(self.handle_reorg(working_snapshot, &prev_block, recursion_count + 1)).await?
}For the snapshot pruning panic, the previous logic unconditionally pruned blocks based on height without ensuring the structural integrity of the cache. The patched logic obtains the highest block hash first, ensuring that at least one block is preserved:
// Patched implementation ensuring the top block is retained
fn remove_finalized_blocks(&mut self, finalized_height: Height) {
let top_block_hash = match self
.heights_to_hashes
.iter()
.max_by_key(|(height, _hash)| *height)
{
Some((_height, hash)) => *hash,
None => return,
};
// Retain blocks that meet the height requirement OR match the top block hash
self.blocks.retain(|_hash, block| {
block.height() >= finalized_height || block.hash() == &top_block_hash
});
}To exploit the unbounded recursion vulnerability, an attacker must have the capability to feed custom block headers into the indexing pipeline. This occurs if the indexing daemon connects to a malicious validator node, or if the network sync interface accepts block data from untrusted peers.
An attacker constructs a block header where the parent block hash field matches the block's own cryptographic hash:
$$\text{header.previous_block_hash} = \text{block.hash()}$$
When this crafted header is processed, the daemon attempts to trace the history. It extracts the parent hash, retrieves the same block, and recursively calls handle_reorg. Because the recursion relies on Box::pin, every single call allocates memory on the heap while keeping the CPU thread fully saturated.
Alternatively, an attacker can create a multi-block loop (such as A to B to A) to achieve the same infinite recursive state. The thread will spin continuously at 100% CPU usage, rapidly leaking heap space until the platform kernel terminates the process due to out-of-memory constraints.
The impact of these two vulnerabilities is a complete Denial of Service (DoS) of the zaino indexer node. Because block synchronization runs within critical application execution contexts, blocking the state reorganization thread stalls all downstream operations.
While the CVSS v4 score is calculated as 5.1 (Medium), the real-world operational impact on indexer infrastructures is significant. A node impacted by the infinite recursion bug remains unresponsive and consumes system-wide computational resources, potentially affecting adjacent applications co-located on the same server instance.
The snapshot pruning panic is deterministic and results in a clean termination of the binary via Rust's panic infrastructure. While a panic does not lead to a resource leak, it terminates the service immediately. If automatic process recovery is not configured, the service remains offline indefinitely.
The primary remediation strategy is upgrading the zaino-state dependency to version 0.4.1 or higher. This version contains the mitigations merged in Pull Request #1172, enforcing a maximum recursion depth of 110 frames and ensuring snapshot stability during pruning.
While the recursion limit successfully prevents the infinite loop, it is a defensive bound rather than a formal cycle detection mechanism. The code continues to process up to 110 recursive heap allocations per malicious block. For comprehensive resilience, operators should restrict validator connections to trusted network peers.
For systems where immediate code updates are not feasible, process supervision is highly recommended. Utilizing monitoring tools like systemd with automatic restart properties ensures that nodes crashing due to the snapshot panic can recover automatically. To handle CPU spikes from recursive loops, resource limits (such as systemd CPUQuota or container-based limits) should be applied to prevent the indexer from degrading overall host performance.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
zaino-state zingolabs | >= 0.0.0, < 0.4.1 | 0.4.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-770 |
| Attack Vector | Network |
| CVSS v4 Score | 5.1 |
| Impact | Denial of Service (CPU & Memory Exhaustion, Daemon Crash) |
| Exploit Status | PoC Mechanics Documented |
| CISA KEV Status | Not Listed |
The software allocates search or recursion resources without restricting the quantity, allowing consumption of CPU and memory leading to a denial of service.
Improper input validation of the supiOrSuci field in free5GC Authentication Server Function (AUSF) allows unauthenticated remote attackers to trigger an unhandled parsing exception, resulting in a Denial of Service (DoS) and internal stack trace exposure.
A critical Regular Expression Denial of Service (ReDoS) vulnerability exists in Thumbor prior to version 7.8.0. The vulnerability resides within the dynamic filter-parsing engine, specifically inside the 'convolution' filter parameter processing logic. Due to overlapping and nested quantifiers in the regular expression used to parse matrix values, a remote, unauthenticated attacker can supply a specially crafted, malformed filter payload inside a request URL. This causes Python's standard NFA-based regular expression engine to undergo exponential backtracking, exhausting CPU resources and leading to a complete Denial of Service.
CVE-2026-54737 is a high-severity Prototype Pollution vulnerability in the @phun-ky/defaults-deep npm library prior to version 2.0.5. Due to unsafe recursive object merging, unauthenticated attackers can supply structured payloads that modify the properties of Object.prototype, compromising the runtime process state.
CVE-2026-54729 is a critical Server-Side Request Forgery (SSRF) bypass vulnerability in the dssrf-js Node.js library prior to version 1.0.5. The flaw occurs because the library's DNS validation mechanism incorrectly treats domains like 'localhost' as safe when the configured upstream DNS resolver returns NXDOMAIN. Since the system's HTTP client later falls back to OS-level resolution (resolving 'localhost' to '127.0.0.1'), attackers can bypass validation and access internal loopback addresses.
A template injection vulnerability in @dynatrace-oss/dynatrace-mcp-server allows untrusted input to be interpolated directly into Dynatrace Workflows using Jinja2 syntax, leading to persistent data exposure and exfiltration.
An uncontrolled resource consumption vulnerability (CWE-400) in OliveTin allows unauthenticated remote attackers to exhaust server memory and trigger a denial of service (DoS). By repeatedly initiating the OAuth2 login flow without completing it, attackers can force the server to allocate state variables in an unbounded in-memory map. This heap-based resource exhaustion eventually causes the host operating system to terminate the OliveTin process via the Out-Of-Memory (OOM) killer.