ZEBRA is a Zcash node written entirely in Rust. Prior to 4.5.0, an unauthenticated P2P peer can cause the mempool download pipeline to…
GitHub_M·CWE-401·Published 2026-07-02
ZEBRA is a Zcash node written entirely in Rust. Prior to 4.5.0, an unauthenticated P2P peer can cause the mempool download pipeline to retain transactions after verification reaches the outer RATE_LIMIT_DELAY timeout. In zebrad/src/components/mempool/downloads.rs, Downloads::poll_next removed cancel_handles entries after success and ordinary verification errors, but tokio::time::error::Elapsed did not carry the UnminedTxId needed to remove the timed-out entry. Each retained cancel_handles entry could hold a full Gossip::Tx(UnminedTx), while normal mined-transaction cleanup could not match attacker transactions and no periodic garbage collection or count cap existed. Sustained traffic therefore caused monotonic memory growth until swap pressure degraded the node or the operating system terminated the zebrad process for exhausting memory. This issue is fixed in version 4.5.0.
ZEBRA is a Zcash node written entirely in Rust. Prior to 4.5.0, an unauthenticated P2P peer can cause the mempool download pipeline to retain transactions after verification reaches the outer RATE_LIMIT_DELAY timeout. In zebrad/src/components/mempool/downloads.rs, Downloads::poll_next removed cancel_handles entries after success and ordinary verification errors, but tokio::time::error::Elapsed did not carry the UnminedTxId needed to remove the timed-out entry. Each retained cancel_handles entry could hold a full Gossip::Tx(UnminedTx), while normal mined-transaction cleanup could not match attacker transactions and no periodic garbage collection or count cap existed. Sustained traffic therefore caused monotonic memory growth until swap pressure degraded the node or the operating system terminated the zebrad process for exhausting memory. This issue is fixed in version 4.5.0.
### Am I affected You are affected if: 1. You run `zebrad` up to and including `v4.4.1`. 2. Your node accepts inbound P2P connections (`network.listen_addr` is set, which is the default). 3. Your node's mempool is active (node is synced near the chain tip). All default configurations are affected. ### Summary The mempool download pipeline's `cancel_handles` map retains entries for transactions whose verification times out at the outer `RATE_LIMIT_DELAY` (73-second) boundary. The `tokio::time::error::Elapsed` error carries no payload, so the transaction ID is unrecoverable and the corresponding `cancel_handles` entry (including the full `Gossip::Tx(UnminedTx)`, up to ~2 MB) is never removed. Entries accumulate monotonically with no upper bound or garbage collection, leading to eventual out-of-memory process termination. ### Details `Downloads::poll_next()` at `zebrad/src/components/mempool/downloads.rs:215-228` handles three terminal states for a verification task: - `Ok(Ok(...))`: success. Calls `cancel_handles.remove(&tx.transaction.id)`. Correct. - `Ok(Err(...))`: verification error. Calls `cancel_handles.remove(&hash)`. Correct. - `Err(elapsed)`: outer timeout. Returns `Err(elapsed)` without removing anything. **Bug.** `tokio::time::error::Elapsed` has no payload, so the timed-out transaction's `UnminedTxId` is unrecoverable from the error. The consumer at `zebrad/src/components/mempool.rs:663-672` explicitly acknowledges this gap with a TODO comment. The only cleanup paths for `cancel_handles` are `cancel(mined_ids)` (removes entries matching mined transaction IDs; attacker transactions are never mined) and `cancel_all()` (clears everything on shutdown or chain reset). No periodic GC, no time-based eviction, and no count cap exists. For direct `tx` pushes (`Gossip::Tx`), the retained entry holds the full deserialized transaction, which can be up to ~9 MB in memory for a transaction near the transparent-output extreme. Per-connection leak rate at worst case: ~685 KB/s (~2.4 GB/hour). ### Patches The fix preserves the `UnminedTxId` through the timeout error path: wrap the timeout future so the spawned task's outer error carries the txid (e.g., `Err((txid, elapsed))`). In `Downloads::poll_next()`, on the timeout arm, call `cancel_handles.remove(&txid)`. ### Workarounds There is no configuration-level workaround. Restarting the node clears the accumulated entries. Operators running in memory-constrained environments (containers with cgroup limits) may see the process killed by the OOM killer before natural recovery. ### Impact Gradual, unbounded memory exhaustion of a Zebra node from unauthenticated P2P traffic. The leak is monotonic (entries are never freed under normal operation) but slow (~685 KB/s per connection worst case). An attacker must sustain traffic for hours to exhaust typical server memory. The node continues operating normally until memory pressure becomes critical, at which point the OS OOM killer terminates the process or the node degrades due to swap pressure. No consensus impact, no fund loss, no on-disk corruption. ### Credit Reported by `@AnticsDecoded` via a private GitHub Security Advisory submission. Working E2E reproduction on a live regtest node with staged parent/child transaction dependencies.
### Am I affected You are affected if: 1. You run `zebrad` up to and including `v4.4.1`. 2. Your node accepts inbound P2P connections (`network.listen_addr` is set, which is the default). 3. Your node's mempool is active (node is synced near the chain tip). All default configurations are affected. ### Summary The mempool download pipeline's `cancel_handles` map retains entries for transactions whose verification times out at the outer `RATE_LIMIT_DELAY` (73-second) boundary. The `tokio::time::error::Elapsed` error carries no payload, so the transaction ID is unrecoverable and the corresponding `cancel_handles` entry (including the full `Gossip::Tx(UnminedTx)`, up to ~2 MB) is never removed. Entries accumulate monotonically with no upper bound or garbage collection, leading to eventual out-of-memory process termination. ### Details `Downloads::poll_next()` at `zebrad/src/components/mempool/downloads.rs:215-228` handles three terminal states for a verification task: - `Ok(Ok(...))`: success. Calls `cancel_handles.remove(&tx.transaction.id)`. Correct. - `Ok(Err(...))`: verification error. Calls `cancel_handles.remove(&hash)`. Correct. - `Err(elapsed)`: outer timeout. Returns `Err(elapsed)` without removing anything. **Bug.** `tokio::time::error::Elapsed` has no payload, so the timed-out transaction's `UnminedTxId` is unrecoverable from the error. The consumer at `zebrad/src/components/mempool.rs:663-672` explicitly acknowledges this gap with a TODO comment. The only cleanup paths for `cancel_handles` are `cancel(mined_ids)` (removes entries matching mined transaction IDs; attacker transactions are never mined) and `cancel_all()` (clears everything on shutdown or chain reset). No periodic GC, no time-based eviction, and no count cap exists. For direct `tx` pushes (`Gossip::Tx`), the retained entry holds the full deserialized transaction, which can be up to ~9 MB in memory for a transaction near the transparent-output extreme. Per-connection leak rate at worst case: ~685 KB/s (~2.4 GB/hour). ### Patches The fix preserves the `UnminedTxId` through the timeout error path: wrap the timeout future so the spawned task's outer error carries the txid (e.g., `Err((txid, elapsed))`). In `Downloads::poll_next()`, on the timeout arm, call `cancel_handles.remove(&txid)`. ### Workarounds There is no configuration-level workaround. Restarting the node clears the accumulated entries. Operators running in memory-constrained environments (containers with cgroup limits) may see the process killed by the OOM killer before natural recovery. ### Impact Gradual, unbounded memory exhaustion of a Zebra node from unauthenticated P2P traffic. The leak is monotonic (entries are never freed under normal operation) but slow (~685 KB/s per connection worst case). An attacker must sustain traffic for hours to exhaust typical server memory. The node continues operating normally until memory pressure becomes critical, at which point the OS OOM killer terminates the process or the node degrades due to swap pressure. No consensus impact, no fund loss, no on-disk corruption. ### Credit Reported by `@AnticsDecoded` via a private GitHub Security Advisory submission. Working E2E reproduction on a live regtest node with staged parent/child transaction dependencies.
ZEBRA es un nodo Zcash escrito completamente en Rust. Antes de la versión 4.5.0, un par P2P no autenticado puede provocar que la tubería de descarga del mempool retenga transacciones después de que la verificación alcance el tiempo de espera externo RATE_LIMIT_DELAY. En zebrad/src/components/mempool/downloads.rs, Downloads::poll_next eliminaba las entradas de cancel_handles después de un éxito y de errores de verificación ordinarios, pero tokio::time::error::Elapsed no contenía el UnminedTxId necesario para eliminar la entrada que había expirado. Cada entrada de cancel_handles retenida podía contener un Gossip::Tx(UnminedTx) completo, mientras que la limpieza normal de transacciones minadas no podía coincidir con las transacciones del atacante y no existía una recolección de basura periódica ni un límite de conteo. El tráfico sostenido, por lo tanto, causaba un crecimiento monotónico de la memoria hasta que la presión del swap degradaba el nodo o el sistema operativo terminaba el proceso zebrad por agotar la memoria. Este problema está solucionado en la versión 4.5.0.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 3.1 | Primary | cve.org | 5.3 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
| 3.1 | Primary | cve.org | 5.3 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
| 3.1 | Secondary | GHSA | 5.3 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
| 3.1 | Secondary | ENISA EUVD | 5.3 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
| 3.1 | Secondary | NVD | 5.3 | 3.9 | 1.4 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |