security: apply 2026-06 source-review fixes (consensus, memory, crypto, FFI, P2P, wallet) - #3
Merged
Merged
Conversation
…o, FFI, P2P, wallet)
From the full multi-domain security review; each fix traced to verified source
lines and functionally exercised on mainnet at tip ~3.14M. Not a consensus
change vs the network.
Verification:
- Full block-index load (MEM-02 VARINT path) and clean block verify across
~3.14M blocks (CON-01 value-pool rebuild) with no errors.
- Shielded z_sendmany round-trip succeeded: note encrypted (CRY-01), spend +
binding signatures created (RUST-01 error branch did not fire), broadcast,
mined, and decrypted on receipt.
- BLK-01 confirmed by a full -reindex on a datadir copy: block at height 6587
(hash 000000077ff04de591789d4e11b26e4387e14753cc14a997168acf6cea788f70,
226001 bytes -- the first block over the old 200000-byte MAX_BLOCK_SIZE)
was read and connected; the chain advanced past it instead of stalling at
6586 as the unpatched LoadExternalBlockFile would have.
Consensus / validation:
- CON-01 LoadBlockIndexDB value-pool rebuild uses overflow-safe CheckedAdd
(was raw signed + on CAmount, undefined behaviour on overflow)
- CON-05 drop the no-op tx-count-vs-byte-constant conjunct in CheckBlock
- BLK-01 shared GENEROUS_BLOCK_SIZE_LIMIT; widen LoadExternalBlockFile buffer
and nSize gate so -reindex/-loadblock import the 1,272 canonical
mainnet blocks between 200 KB and 2 MB (were silently skipped)
Memory safety:
- MEM-01 size_t bounds + pre-add overflow guard in CBaseDataStream read/ignore
- MEM-02 overflow guard in ReadVarInt before the shift and the increment
(upstream form; VARINT is used on signed fields too)
- MEM-03 bound the addr count before vector allocation in bootstrap discovery
Cryptography:
- CRY-01 RAII zeroization of symmetric key K and dhsecret in NoteEncryption
(cleared on every scope exit, incl. KDF/DH-failure throw)
- CRY-03 explicit G1 prime-order subgroup check (mirrors the G2 check)
Rust / FFI:
- RUST-01 check librustzcash_sapling_spend_sig / _binding_sig returns; return an
error instead of building a silently zero-signed transaction
- RUST-02 ferror guard in check_file_hash read loop (no infinite spin on I/O error)
P2P / performance / stability:
- NET-01 uintptr_t for the addr-relay pointer hash (full 64-bit mixes in)
- NET-02 prune expired setBanned entries on Ban()
- PERF-01 drop redundant LOCK(mempool.cs) in ReacceptWalletTransactions
- PERF-03 cap mapRelay growth (evict oldest)
- PERF-04 reduce bootstrap-validator cs_main batch budget 80 -> 20 ms
Wallet / RPC:
- WAL-01 z_sendmany help warns that -debug=zrpcunsafe leaks shielded details
- WAL-02 warn when -rpcallowip is set without -rpcbind (binds all interfaces)
- WAL-03 raise wallet KDF iteration floor 25000 -> 100000
- WAL-05 dumpwallet writes the export file 0600 (contains keys + HD seed)
- WAL-06 nWalletUnlockTime -> std::atomic (no torn read vs the relock timer)
CI:
- SUP-04 re-enable the shielded regression tests
Documentation only (no behaviour change): CON-02/03 (turnstile check-skipped
invariant on unknown chain value), CON-04 (restoring the tight post-Sapling tx
size limit is a future soft fork, must be staged at an activation height).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Applies the fixes from a full multi-domain security review of the ZClassic source (crypto, C/C++ memory safety, Rust/FFI, consensus, P2P/DoS, wallet/RPC, performance/stability, supply chain). Each fix is traced to verified source lines and was functionally exercised on a mainnet node at tip ~3.14M.
18 files changed, +297 / −65. This is not a consensus change versus the network — every fix is either non-consensus, node-local, or (for
BLK-01) brings an import code path in line with the block-size ruleCheckBlockalready enforces. No hard fork; nothing here changes block/transaction acceptance for peers.Fixes
Consensus / validation
consensus/consensus.h,main.cppGENEROUS_BLOCK_SIZE_LIMIT; widenLoadExternalBlockFilebuffer and thenSizegate so-reindex/-loadblockimport the 1,272 canonical mainnet blocks between 200 KB and 2 MB that the oldMAX_BLOCK_SIZEcap silently skipped (which would otherwise produce a divergent/incomplete chainstate on rebuild).main.cppLoadBlockIndexDBvalue-pool rebuild now uses overflow-safeCheckedAdd(was raw signed+onCAmount— undefined behaviour on overflow, feeding the ZIP-209 turnstile).main.cppvtx.size() > GENEROUS_BLOCK_SIZE_LIMITconjunct (tx count vs a byte constant) inCheckBlock; the serialized-size check is the real guard.Memory safety
streams.hCBaseDataStream::read/ignorecompute bounds insize_twith a pre-addition overflow guard (wasunsigned intnarrowing of asize_tlength → latent OOBmemcpy).serialize.hReadVarIntguards against overflow ofIbefore the shift and beforen++(upstream Bitcoin Core form) — prevents value wrap / signed UB (VARINT is used on signed fields likenFile/nPos) and unbounded spin.bootstrap.cppaddrelement count before allocating the vector in bootstrap discovery (was deserialize-then-check).Cryptography
zcash/NoteEncryption.cppKanddhsecretvia a RAIIMemoryCleanserdeclared right after each buffer — cleared on every scope exit (normal/early return, AEAD-failure, and the KDF/DH-failure throw).zcash/Proof.cppto_libsnark_g1(), mirroring the existing G2 check (defense-in-depth; rejects nothing today as G1 cofactor = 1).Rust / FFI
transaction_builder.cppboolreturns oflibrustzcash_sapling_spend_sig/_binding_sig; free the proving ctx and return an error instead of building a silently zero-signed transaction.init.cppferrorguard in thecheck_file_hashread loop — no infinite 100%-CPU spin on a real I/O error during startup.P2P / performance / stability
main.cppuintptr_t(notunsigned int) for the addr-relay pointer hash so the full 64-bit pointer mixes in.net.cppsetBannedentries on eachBan()(bounds map growth / the per-connection linear scan).wallet/wallet.cppLOCK(mempool.cs)inReacceptWalletTransactions(ATMP takes it internally) — removes the flaggedcs_wallet → mempool.cslock-order edge.net.cppmapRelaygrowth (evict oldest) so a unique-txid flood can't grow it unbounded before the 15-min timer.bootstrapvalidation.cppcs_mainbatch budget 80 → 20 ms (shorter stalls to live message processing).Wallet / RPC
wallet/rpcwallet.cppz_sendmanyhelp text warns that-debug=zrpcunsafe/=allwrites shielded sender/recipients/amounts/memo todebug.log.httpserver.cpp-rpcallowipis set without-rpcbind(RPC then binds all interfaces).wallet/wallet.cppwallet/rpcdump.cppdumpwalletsets the export file to0600immediately (it contains all private keys + the HD seed).rpc/server.h,wallet/rpcwallet.cpp,rpc/misc.cppnWalletUnlockTime→std::atomic<int64_t>; reads use.load()(no torn read vs the relock timer). Writes still under the lock.CI
qa/pull-tester/rpc-tests.shzcjoinsplitdoublespend.py, shieldcoinbase, nullifiers, treestate, anchorfork, zkey import/export, NU-activation).Documentation only (no behaviour change)
nChainSaplingValuebecomesnoneand propagates, so the turnstile is skipped (not enforced) across the unknown window — corrupted data can never cause a wrong turnstile pass, but it is also not enforced there (acceptable for corruption recovery).ContextualCheckTransaction; left unchanged.Out of scope / deferred (tracked, not in this PR)
0x930b540d(upgrades.cpp:38,43). Immutable (active since height 707000; baked into the sighashes of all v4 transactions since) — must not be changed retroactively; the next network upgrade must use a fresh, unique branch ID.sha256hashes +cargo audit.-maxmempooleviction), WAL-04 (passphrase mlock), SUP-05 (orphanedcrate_*.mk).Verification
z_sendmanyround-trip succeeded — note encrypted, spend + binding signatures created (the RUST-01 error branch did not fire), broadcast, mined, and decrypted on receipt at the destination. Sapling note decryption (balance read) also confirmed.-reindexon a datadir copy: the block at height 6587 (hash000000077ff04de591789d4e11b26e4387e14753cc14a997168acf6cea788f70, 226 001 bytes — the first block over the old 200 000-byteMAX_BLOCK_SIZE) was read and connected, and the chain advanced past it (6588, 6589, …). The unpatchedLoadExternalBlockFilewould have skipped it and stalled at 6586. Hash matches theaudit-full.jsonoversized-block record.git diff --checkis clean.Reviewer checklist
qa/pull-tester/rpc-tests.sh— the re-enabled shielded tests are not yet run in CI here; triage any failure rather than re-commenting it.-reindexon a copy of a full-history datadir to confirmBLK-01imports the large historical blocks — done (height 6587 / 226 KB connected, hash-matched; see Verification). Reviewers may re-run to a full matching tip if desired.main.cpp(LoadBlockIndexDB,CheckBlock).Notes
SUP-04) may surface pre-existing failures; those should be triaged, not hidden.🤖 Generated with Claude Code