VDB
KO
CRITICAL

GHSA-gf9r-m956-97qx

zebrad has consensus divergence via P2SH sigop undercount in pure-Rust disabled-opcode parser

Details

### Am I affected

You are affected if:

1. You run any version of `zebrad` up to and including `v4.4.1`. 2. Your node validates blocks on mainnet, testnet, or any network where both Zebra and zcashd nodes participate.

All default configurations are affected. No feature flags, non-default settings, or special build options are required.

### Summary

Zebra's P2SH sigop counter uses a pure-Rust code path that short-circuits on disabled opcodes (such as `OP_CODESEPARATOR`), returning a partial count of zero for any sigops following the disabled opcode. The reference implementation (zcashd) correctly counts through disabled opcodes in its static sigop analysis. This produces a consensus divergence: Zebra accepts blocks that zcashd rejects when the block-wide `MAX_BLOCK_SIGOPS = 20,000` threshold is crossed on one side but not the other.

An attacker can exploit this without mining capability. Broadcasting transactions that spend P2SH outputs with malicious redeem scripts is sufficient; any Zebra miner who includes those transactions in a block triggers a chain split between Zebra and zcashd validators.

### Details

The P2SH sigop counter at `zebra-script/src/lib.rs:399` calls `script::Code(redeemed_bytes).sig_op_count(true)`, which is a pure-Rust path through `zcash_script-0.4.4`. The legacy (non-P2SH) sigop counter at `lib.rs:282-289` correctly uses the C++ FFI via `interpreter.legacy_sigop_count_script()`. Only the P2SH path bypasses the FFI.

The Rust parser in `zcash_script-0.4.4/src/opcode/mod.rs:1247-1260` treats 16 disabled opcodes (0x7e through 0xab, including `OP_CAT`, `OP_SUBSTR`, `OP_AND`, `OP_OR`, `OP_XOR`, `OP_2MUL`, `OP_2DIV`, `OP_MUL`, `OP_DIV`, `OP_MOD`, `OP_LSHIFT`, `OP_RSHIFT`, and `OP_CODESEPARATOR`) as `Err(Error::Disabled(...))`. The `sig_op_count` function at `iter.rs:104-115` uses `try_fold`, which terminates on the first `Err` and returns the partial sum accumulated so far.

zcashd's `GetOp2` (`script.h:514-562`) returns `true` for all non-push opcodes including the disabled range. Its `GetSigOpCount(true)` (`script.cpp:152-174`) continues counting through disabled opcodes. zcashd rejects disabled opcodes at execution time in the interpreter, not during static sigop analysis.

A redeem script of `[0xab, OP_CHECKMULTISIG x 50]` produces: Zebra = 0 sigops, zcashd = 1,000 sigops. Across 21 inputs in a block, Zebra computes 0 while zcashd computes 21,000, crossing the `MAX_BLOCK_SIGOPS = 20,000` threshold on one side only.

### Patches

Patched in Zebra 4.4.2. The fix routes the P2SH sigop counter through the same C++ FFI already used by the legacy sigop counter.

### Workarounds

There is no configuration-level workaround. All Zebra nodes validating blocks on a network shared with zcashd are affected. Upgrade as soon as the patched version is available.

### Impact

A chain split between Zebra and zcashd validators. The attacker broadcasts spending transactions referencing P2SH outputs whose redeem scripts contain a disabled opcode followed by `OP_CHECKSIG` or `OP_CHECKMULTISIG` opcodes. When a Zebra miner (estimated ~30% of current network hashrate) includes these transactions in a block, Zebra validators accept the block while zcashd validators reject it with `bad-blk-sigops`. The two halves of the network diverge and every subsequent block extending the Zebra-side tip inherits the divergence.

The attacker does not need mining capability, RPC access, or any special privileges. The cost is the transaction fees for the funding and spending transactions.

### Credit

Reported by `@samsulselfut` via a private GitHub Security Advisory submission.

Are you affected?

Enter the version of the package you're using.

Affected packages

crates.io / zebra-script
Introduced in: 0 Fixed in: 7.0.0

Upgrade zebra-script to 7.0.0 or newer (ecosystem crates.io).

crates.io / zebrad
Introduced in: 0 Fixed in: 4.5.0

Upgrade zebrad to 4.5.0 or newer (ecosystem crates.io).

References