Skip to content

bug: V6 transaction hash panic at zebra-chain hash.rs:77 β€” Remote DoS via crafted V6 transaction once tx_v6 is enabledΒ #10534

Description

@conradoplg

What happened?

Reported by @robustfengbin. Opened as issue because not currently exploitable.

Summary

A crafted V6 transaction passes Transaction::zcash_deserialize() but panics at zebra-chain/src/transaction/hash.rs:77 when computing the tx hash β€” TxIdBuilder::txid() returns None (because to_librustzcash() fails on V6 due to zcash_primitives not yet supporting the ZIP-233 zip233_amount field), and the code calls .expect(). This happens in the P2P codec during Transaction β†’ UnminedTx conversion, before any consensus checks. Any peer can crash any Zebra node compiled with --cfg zcash_unstable="nu7" + feature="tx_v6" by sending a single 33-byte tx message.

The V5 arm in serialize.rs already calls tx.to_librustzcash(network_upgrade)?; immediately after deserializing V5 transactions, which validates librustzcash compatibility at the deserialize boundary and prevents the panic for V5 inputs. The V6 arm in the same match does not include this check, so V6 inputs reach hash.rs:77 and panic.

Current attack surface is zero β€” production ZF release binaries do not enable tx_v6 + zcash_unstable="nu7", so the V6 match arm is preprocessed out. Once NU7 testnet/mainnet activation requires enabling those flags in production, every Zebra node becomes vulnerable to a single 33-byte broadcast tx. We are reporting before NU7 activation so the fix can land on a calm release schedule rather than as an emergency response.

Details

zebra-chain/src/transaction/hash.rs:72-79:

impl From<&Transaction> for Hash {
    fn from(transaction: &Transaction) -> Self {
        let hasher = TxIdBuilder::new(transaction);
        hasher
            .txid()
            .expect("zcash_primitives and Zebra transaction formats must be compatible")
            // ^^^^ panics when txid() returns None
    }
}

For V6 inputs, txid() delegates to txid_v5(), which calls to_librustzcash(nu).ok()?. Because zcash_primitives 0.26 does not recognize zip233_amount, this returns None, propagates up, and .expect() panics.

crash path

zebra-network/src/protocol/external/codec.rs read_tx()
  β†’ Transaction::zcash_deserialize(reader)              β€” V6 deserialize succeeds
  β†’ Message::Tx(result?.into())                         β€” Transaction β†’ UnminedTx
  β†’ UnminedTx::from(Transaction)
  β†’ UnminedTxId::from(&Transaction)                     β€” V6 arm Witnessed(transaction.into())
  β†’ WtxId::from(&Transaction)                           β€” id: transaction.into()
  β†’ Hash::from(&Transaction)
  β†’ TxIdBuilder::new(tx).txid().expect(...)             β€” panic at hash.rs:77

Suggested fix β€” 3 lines, mirroring the V5 arm

#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
(6, true) => {
    // ...existing deserialize logic up to building V6 fields...
    let tx = Transaction::V6 { ... };

    tx.to_librustzcash(network_upgrade)?;   // ← NEW (mirrors V5 arm)

    Ok(tx)
}

PoC

use std::io::Cursor;
use zebra_chain::serialization::ZcashDeserialize;
use zebra_chain::transaction::Transaction;

fn main() {
    let bytes: Vec<u8> = vec![
        0x06, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF,
        0x55, 0x10, 0xE7, 0xC8, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00,
    ];
    let tx = Transaction::zcash_deserialize(Cursor::new(&bytes))
        .expect("deserializes successfully");
    let _hash = tx.hash();  // panics at hash.rs:77
}


Expected output:
thread '' panicked at zebra-chain/src/transaction/hash.rs:77:14:
zcash_primitives and Zebra transaction formats must be compatible

Impact

Remote Denial of Service, network-wide once V6 transactions are accepted.

  • Every Zebra node that has P2P port 8233 open is reachable
  • A single attacker-crafted 33-byte V6 tx message panics the codec, before any consensus check
  • No authentication, signatures, or special access required
  • Same propagation surface as any normal P2P tx β€” one broadcast can hit the whole network

Current attack surface (today, pre-NU7): zero. ZF release binaries (zfnd/zebra:latest) do not set tx_v6 Cargo feature nor --cfg zcash_unstable="nu7", so rustc preprocesses the V6 match arm out of the binary. No production deployment is currently affected.

Future attack surface (NU7 activation): high. When NU7 ships and ZF release binaries enable tx_v6 + zcash_unstable="nu7", the V6 match arm becomes part of the shipped binary, the 33-byte PoC above immediately becomes a remote DoS primitive, and one peer-to-peer broadcast crashes all V6-enabled Zebra nodes simultaneously β€” at the worst possible moment, right at NU7 activation, potentially destabilizing the upgrade itself.

We are submitting this advisory well in advance of NU7 mainnet activation so the fix can be designed, reviewed, merged, released, and propagated through the deployment chain before V6 transactions become live. Pre-activation disclosure means a calm fix on the project's normal release cadence; post-activation disclosure of the same finding would be an emergency security release with downtime risk for the network.

What were you doing when the issue happened?

No response

Zebra logs

No response

Zebra Version

No response

Which operating systems does the issue happen on?

  • Linux
  • macOS
  • Windows
  • Other OS

OS details

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bugS-needs-triageStatus: A bug report needs triage

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions