VYPR
High severity7.5GHSA Advisory· Published May 21, 2026· Updated May 21, 2026

nimiq-primitives: Panic DoS in trie chunk processing via ROOT-keyed item

CVE-2026-46545

Description

Impact

A remote, unauthenticated denial-of-service vulnerability in MerkleRadixTrie::put_chunk allows any state-sync peer to crash any node performing state synchronization (freshly joining nodes and recovering nodes).

A malicious peer can respond to a RequestChunk with a ResponseChunk::Chunk whose first TrieItem.key is the empty (ROOT) key. The chunk passes sorting, range, and Merkle-proof validation, but when put_raw tries to store a value at the root node, it calls TrieNode::put_value(...).unwrap(), which returns Err(RootCantHaveValue) and panics, aborting the node process. The panic fires on the first malicious chunk the victim commits; no rate limit or authentication gate caps the attack.

Impacted: any node running state sync against untrusted peers — this includes fresh nodes performing initial download and existing nodes recovering from data loss. Honest nodes never construct ROOT-keyed items, so non-syncing operation is unaffected.

Patches

See PR.

Workarounds

There is no safe in-process workaround: any peer serving state-sync data can trigger the crash and the code path is not guarded by a feature flag.

Resources

  • Fix commit: (link to the merged PR commit, once merged)
  • Affected code: `primitives/trie/src/trie.rs`put_chunk (around line 819) and put_raw (around line 351)

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A remote unauthenticated DoS in Nimiq's MerkleRadixTrie::put_chunk allows any state-sync peer to crash nodes by sending a chunk with a ROOT-keyed item.

Vulnerability

CVE-2026-46545 is a denial-of-service vulnerability in the MerkleRadixTrie::put_chunk function of the Nimiq core-rs-albatross implementation. The root cause is that the function does not validate that the first item in a received chunk has a non-empty key. When a chunk contains an item with the empty (ROOT) key, the subsequent call to put_raw attempts to store a value at the root node, which triggers a panic via TrieNode::put_value(...).unwrap() because the root node cannot hold a value (RootCantHaveValue). This panic aborts the node process [1][2][3].

Exploitation

An unauthenticated attacker acting as a state-sync peer can exploit this by responding to a legitimate RequestChunk with a crafted ResponseChunk::Chunk whose first TrieItem.key is the empty ROOT key. The chunk passes sorting, range, and Merkle-proof validation, so it is accepted by the victim node. No rate limiting or authentication is in place to prevent this attack; the panic occurs on the first malicious chunk committed [1][3].

Impact

Any node performing state synchronization—whether a freshly joining node or an existing node recovering from data loss—is vulnerable. The attack causes an immediate crash, effectively preventing the node from completing synchronization. Honest nodes never construct ROOT-keyed items, so normal operation is unaffected [1][3].

Mitigation

The fix is implemented in commit 0fb8766, which adds a check in put_chunk to reject any chunk whose first item has an empty key, returning an InvalidChunk error instead of proceeding to the panic-prone code path [2]. The fix is included in release v1.5.0 [1]. There is no safe in-process workaround; users must update to the patched version [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
0fb8766adea9

Trie: reject ROOT-keyed items in `put_chunk`

1 file changed · +45 1
  • primitives/trie/src/trie.rs+45 1 modified
    @@ -355,6 +355,11 @@ impl<T: TrieTable> MerkleRadixTrie<T> {
             value: Vec<u8>,
             missing_range: &Option<ops::RangeFrom<KeyNibbles>>,
         ) {
    +        debug_assert!(
    +            !key.is_empty(),
    +            "empty (ROOT) key must be rejected at the put_chunk boundary",
    +        );
    +
             // Start by getting the root node.
             let mut cur_node = self
                 .get_root(txn)
    @@ -413,7 +418,8 @@ impl<T: TrieTable> MerkleRadixTrie<T> {
                 // with the given key. Update the value.
                 if cur_node.key == *key {
                     // Update the node and store it.
    -                // TODO This unwrap() will fail when attempting to store a value at the root node
    +                // `put_raw` rejects empty keys at its entry, so `cur_node` here never has
    +                // `root_data.is_some()`; `put_value` cannot return `RootCantHaveValue`.
                     let prev_kind = cur_node.kind();
                     let old_value = cur_node.put_value(value).unwrap();
                     self.put_node(txn, &cur_node, old_value.into());
    @@ -840,6 +846,15 @@ impl<T: TrieTable> MerkleRadixTrie<T> {
                         "first key is inconsistent with key range",
                     ));
                 }
    +
    +            // The root node holds no value, so a ROOT-keyed item would panic in `put_raw`.
    +            // ROOT is the minimum key, so checking `items[0]` suffices given the sort check below.
    +            if chunk.items[0].key.is_empty() {
    +                return Err(MerkleRadixTrieError::InvalidChunk(
    +                    "item key must not be the root key",
    +                ));
    +            }
    +
                 let last_item_key = chunk.items.last().unwrap().key.clone();
                 if let Some(end_key) = &chunk.end_key
                     && last_item_key >= *end_key
    @@ -2214,6 +2229,35 @@ mod tests {
             );
         }
     
    +    #[test]
    +    fn put_chunk_rejects_root_keyed_item() {
    +        // A ROOT-keyed item used to reach `put_raw` and panic on
    +        // `put_value(...).unwrap()`; `put_chunk` must reject it as `InvalidChunk`.
    +        let env = MdbxDatabase::new_volatile(Default::default()).unwrap();
    +        let trie = MerkleRadixTrie::new_incomplete(&env, TestTrie);
    +        let mut raw_txn = env.write_transaction();
    +        let mut txn: WriteTransactionProxy = (&mut raw_txn).into();
    +        assert!(!trie.is_complete(&txn));
    +
    +        let proof_root = TrieNode::new_root();
    +        let expected_hash = proof_root.hash_assert();
    +        let proof = TrieProof::new(vec![proof_root.into()], Default::default());
    +
    +        let malicious_chunk = TrieChunk::new(
    +            None,
    +            vec![TrieItem::new(
    +                KeyNibbles::ROOT,
    +                vec![0xab, 0xbc, 0xcd, 0xde],
    +            )],
    +            proof,
    +        );
    +
    +        match trie.put_chunk(&mut txn, KeyNibbles::ROOT, malicious_chunk, expected_hash) {
    +            Err(MerkleRadixTrieError::InvalidChunk(_)) => {}
    +            other => panic!("expected InvalidChunk(_), got {:?}", other),
    +        }
    +    }
    +
         #[test]
         fn partial_tree_put_chunks_manual() {
             let key_1 = "413f22".parse().unwrap();
    

Vulnerability mechanics

Root cause

"Missing validation that the first TrieItem key is not the empty (ROOT) key in MerkleRadixTrie::put_chunk, causing an unwrap on TrieNode::put_value which returns Err(RootCantHaveValue) and panics."

Attack vector

An unauthenticated remote attacker acting as a state-sync peer responds to a victim's RequestChunk with a ResponseChunk::Chunk whose first TrieItem.key is the empty (ROOT) key. The chunk passes sorting, range, and Merkle-proof validation because those checks do not reject a ROOT key. When the victim node calls put_raw to store the value at the root node, it invokes TrieNode::put_value(...).unwrap(), which returns Err(RootCantHaveValue) and panics, aborting the process. No authentication, rate limit, or feature flag protects this code path, so any peer serving state-sync data can crash any node performing state synchronization.

Affected code

The vulnerability resides in `primitives/trie/src/trie.rs` in the `MerkleRadixTrie::put_chunk` function (around line 819) and the downstream `put_raw` function (around line 351). The `put_chunk` function validates sorting, range, and Merkle proofs but does not check whether the first `TrieItem.key` is the empty ROOT key. When `put_raw` subsequently calls `TrieNode::put_value(...).unwrap()`, the unwrap panics on `Err(RootCantHaveValue)`.

What the fix does

The patch [patch_id=1262096] adds a check in put_chunk that rejects any chunk whose first TrieItem key is the empty (ROOT) key before it reaches put_raw. This prevents the malicious chunk from ever triggering TrieNode::put_value on the root node. By validating the key early in the chunk-processing pipeline, the fix ensures that only chunks with valid non-root keys proceed to Merkle-proof verification and storage, closing the panic path.

Preconditions

  • networkAttacker must be able to respond to a RequestChunk message as a state-sync peer
  • inputAttacker sends a ResponseChunk::Chunk whose first TrieItem.key is the empty (ROOT) key

Generated on May 21, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.