Skip to content

Potential Chain Stall via stale `parent_error_map`

Moderate
mpguerra published GHSA-8gxx-hc65-vv82 Jul 17, 2026

Package

cargo zebrad (Rust)

Affected versions

6.0.0

Patched versions

6.1.0

Description

Chain stall via stale parent_error_map entry after a poisoned-block rejection

Field Value
Severity Moderate
CVSS 3.1 AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H (5.9)
CWE CWE-459 (Incomplete Cleanup) / CWE-400 (Uncontrolled Resource Consumption)
Affected versions through v6.0.0
Patched versions 6.1.0
Reporter credit @deedim
Fix PR #10995

Am I affected?

You are affected if you run an affected version with default peer-to-peer networking. A remote, unauthenticated peer can stall your node at a specific block height for an extended period (on the order of 2,000 blocks, roughly 41 hours, per successful trigger), and can repeat the trigger on each new block to keep the node behind the network tip. The stall clears if you restart the node. There is no crash, no consensus divergence, and no state corruption; the node falls behind the chain tip rather than accepting anything invalid.

Summary

When a block fails contextual verification, Zebra records its hash in an in-memory map (parent_error_map) and propagates that failure to any child block whose parent is in the map. Entries are only removed when the map exceeds a fixed size (2,000 entries) or when the node restarts; an entry is never removed when the canonical block at that hash is later committed successfully. Using the same ZIP-244 coinbase-malleability primitive as GHSA-4m69-67m6-prqp, an attacker can craft a poisoned block whose header hash equals a canonical block N and get it rejected before the canonical N arrives. The poisoned hash then blocks the canonical N+1 from being committed, stalling the node at height N until the stale entry is evicted (after ~2,000 further rejected entries) or the node is restarted.

Details

Verified on v6.0.0.

The map and its eviction rule (zebra-state/src/service/write.rs):

  • PARENT_ERROR_MAP_LIMIT = MAX_BLOCK_REORG_HEIGHT * 2 (write.rs:39). MAX_BLOCK_REORG_HEIGHT is 1,000 (zebra-chain/src/parameters/constants.rs:30), so the limit is 2,000.
  • On a contextual-verification failure the hash is inserted (parent_error_map.insert(child_hash, error), write.rs:409). The only removal is FIFO, and only once the map exceeds the limit: if parent_error_map.len() > PARENT_ERROR_MAP_LIMIT { ... shift_remove_index(0) } (write.rs:412-414). There is no removal when the canonical block at that hash later commits.
  • If a block's parent hash is already in the map, the child is rejected with the cloned parent error without re-running contextual verification, and the child's own hash is inserted too (write.rs:385-400). So once a poisoned hash is present, the matching canonical block is rejected on arrival and the rejection propagates forward.

The rejection of the poisoned body itself occurs at the auth-data-root check (block_commitment_is_valid_for_chain_history, zebra-state/src/service/check.rs:137).

Attack sequence: the attacker observes a new block N, constructs a body with the same header hash by mutating the coinbase scriptSig (the ZIP-244 primitive from GHSA-4m69), and delivers it before the canonical N arrives. The poisoned N fails at the auth-data-root check and its hash (equal to the canonical N's hash) is recorded in parent_error_map. When the canonical N and N+1 arrive, N+1 is rejected against the stale entry without re-validation, and the node stalls at height N. The stall persists until 2,000 further entries evict the stale one or the node restarts. An attacker can repeat this on each new block to keep the node behind the tip. Triggering requires winning the propagation race against honest peers, the same precondition as GHSA-4m69.

This is a distinct finding from GHSA-4m69. It shares the ZIP-244 primitive and the propagation-race precondition, but it is a different code path (parent_error_map eviction, not the SentHashes lockout) with a different effect (a temporary chain stall). Code added around the GHSA-4m69 fix (write.rs:416-431) signals the state service to drop a rejected hash from the sent-hashes set so a re-delivery is not short-circuited; that addresses the sent-hashes lockout but not the parent_error_map eviction, so it does not resolve this finding.

Patches

6.1.0. The direct fix is to remove a hash from parent_error_map when the canonical block at that hash successfully commits, rather than relying only on the FIFO size limit, so that a contextually-valid block is not shadowed by a prior poisoned variant at the same hash. The poisoned block must still be rejected; only the retention of the rejection against a hash that is subsequently validly committed should change. This is state-write-task logic and must not weaken contextual verification itself.

Workarounds

Restarting the node clears the in-memory map and allows the canonical chain to advance, but does not prevent an attacker from re-triggering the stall. There is no configuration-only workaround. Upgrading once a patch is available is the durable fix.

Impact

Availability. A remote, unauthenticated peer that wins the propagation race can stall a targeted node at a block height for an extended period (about 2,000 blocks, roughly 41 hours, per successful trigger), and can sustain the effect by re-triggering on each new block, keeping the node behind the network tip. Downstream consumers relying on the node (for example, wallets or indexers) see a stalled chain. The stall clears on restart, so it is not a permanent halt. There is no crash, no consensus divergence, no acceptance of invalid data, and no state corruption. The final severity depends on whether a node recovers on the live network without operator intervention while the attack is sustained, or effectively wedges; this is pending confirmation.

Credit

Reported by @deedim, with a precise root-cause analysis of the parent_error_map eviction behaviour, a deterministic submitblock-based proof of concept on Regtest, and a clear description of the relationship to GHSA-4m69.

References

  • zebra-state/src/service/write.rs:39,385-400,409,412-414 (the map limit, error propagation to children, insertion, and FIFO-only eviction)
  • zebra-state/src/service/check.rs:137 (auth-data-root rejection of the poisoned body)
  • zebra-chain/src/parameters/constants.rs:30 (MAX_BLOCK_REORG_HEIGHT = 1000)
  • Related: GHSA-4m69-67m6-prqp (same ZIP-244 primitive, different code path and effect)
  • CWE-459, CWE-400

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

CVE ID

No known CVE

Weaknesses

No CWEs

Credits