Jul 31, 2026·6 min read·18 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.
CVE-2026-107718 is a medium-severity Open Redirect vulnerability in the core HTTP server package of the AdonisJS Node.js framework. Prior to versions 8.2.3 and 9.3.0, the framework built route paths by directly interpolating dynamic parameters and wildcard segments without URI encoding. If an application routes attacker-controlled input directly to a dynamic first path segment and uses the generated route URL as a redirect destination, a leading slash can produce a scheme-relative external URL. Modern web browsers process scheme-relative URLs by redirecting the client to the specified external domain, exposing users to credential harvesting, social engineering, and session hijacking. This vulnerability affects all applications running unpatched configurations where input validation is not explicitly implemented before generating paths.
CVE-2026-107725 is a critical security bypass in Hazelcast where missing authorization checks in the MapPermission class permit unprivileged clients to issue queries containing aggregators or projections. This architectural oversight allows attackers to run arbitrary code on the cluster servers under the privileges of the active Hazelcast process.
A stored Cross-Site Scripting (XSS) vulnerability was identified in Indico, an open-source event management system developed at CERN, prior to version 3.3.13. The vulnerability stems from weak URL validation in custom link fields and lack of HTML sanitization during Marshmallow serialization of event notes. This allows authenticated attackers with event modification privileges to inject malicious payloads that execute in the browser of users viewing the event pages or collaborating on notes.
A technical analysis of CVE-2026-107397, a stored Cross-Site Scripting (XSS) vulnerability in Indico's collaborative notes editor and custom link generation fields. Prior to version 3.3.13, Marshmallow serialization schemas omitted HTML sanitization during conflict resolution, and form validators failed to enforce strict URI schemes, enabling authenticated low-privilege attackers to execute arbitrary JavaScript.
An authorization bypass vulnerability exists in the legacy session export API of Indico, an open-source event management system developed at CERN. Due to a missing object-level access check, authenticated users can bypass configuration-level restrictions to extract private session metadata (including session titles, descriptions, and list of conveners) from events that they are otherwise authorized to view.
An incomplete Server-Side Request Forgery (SSRF) validation check in Indico prior to version 3.3.13 allows authenticated event organizers to bypass outbound network restrictions. By utilizing backslash characters within crafted URLs, attackers can exploit a parser differential between the application's validator and the downstream HTTP client library to access internal network resources.