nimiq-primitives: Panic DoS in trie chunk processing via ROOT-keyed item
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) andput_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< 1.5.0+ 1 more
- (no CPE)range: < 1.5.0
- (no CPE)
Patches
10fb8766adea9Trie: 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- github.com/advisories/GHSA-mw3q-r9wh-h2ffghsaADVISORY
- github.com/nimiq/core-rs-albatross/commit/0fb8766adea91e038af00e635a6eb92756e50172ghsa
- github.com/nimiq/core-rs-albatross/pull/3762ghsa
- github.com/nimiq/core-rs-albatross/releases/tag/v1.5.0ghsa
- github.com/nimiq/core-rs-albatross/security/advisories/GHSA-mw3q-r9wh-h2ffghsa
News mentions
0No linked articles in our index yet.