Summary
Parser::next (src/parser/parser_impl.rs:235) computes self.offset + n > self.buffer.len() with unchecked usize addition. When n is decoded from a CompactSize length prefix (e.g. via parse_string), an attacker-supplied 0xff prefix followed by 8 bytes of 0xff yields n = usize::MAX. The addition wraps modulo 2^64, the bounds check evaluates false, and the function falls through to a slice access that panics the thread.
Found via cargo-fuzz / libfuzzer within a few seconds of running fresh harnesses against DBKey::parse_data and the full BDBDump → ZcashdDump → ZcashdParser::parse_dump pipeline. Any tool that ingests an untrusted wallet.dat (migration tooling, wallet recovery utilities, light-wallet importers) will panic on a single-record attack input as small as 10 bytes after db_dump extraction.
A PR with the fix and the fuzz harnesses is incoming and will reference this issue.
Vulnerable code
src/parser/parser_impl.rs:234-243:
pub fn next(&mut self, n: usize) -> Result<&'a [u8]> {
if self.offset + n > self.buffer.len() { // <-- unchecked addition
return Err(Error::BufferUnderflow { ... });
}
let bytes = &self.buffer[self.offset..self.offset + n];
self.offset += n;
// ...
}
When parse_string (src/zcashd_wallet/parseable_types.rs:27-38) is called, n is the result of decoding a CompactSize. Per the Bitcoin/Zcash convention, prefix byte 0xff indicates "next 8 bytes are little-endian u64". Eight bytes of 0xff decode to 0xffffffffffffffff = usize::MAX on 64-bit platforms. With self.offset >= 1, self.offset + usize::MAX wraps and the bounds check is bypassed.
Under -Cdebug-assertions (libfuzzer's instrumented build) the overflow is caught one line earlier with panic_const_add_overflow at parser_impl.rs:235:12. In standard release builds it panics on the slice index in the next statement. Either way the result is an unrecoverable panic on attacker-controlled input.
Reach
DBKey::parse_data (src/zcashd_dump.rs:40) — called for every record key in a wallet.dat dump.
ZcashdDump::from_bdb_dump (src/zcashd_dump.rs:95) — invokes DBKey::parse_data over all records emitted by db_dump.
ZcashdParser::parse_dump (src/zcashd_parser.rs:36) — the full typed parser, called by the public migrate_to_zewif API.
Sibling audit: I also reviewed the other byte-access methods on Parser for the same shape. peek (line 270), rest (line 275), and the std::io::Read impl (line 296) each saturate n to min(n, self.remaining()) before any slice access, so the addition cannot wrap. next is the only exploitable site.
Reproducer
10-byte input that triggers the panic via DBKey::parse_data:
ff ff ff ff ff ff ff ff ff 04
The first nine bytes form a CompactSize decoding to usize::MAX; the trailing 0x04 is a single data byte. In Rust:
use zewif::Data;
use zewif_zcashd::DBKey;
fn main() {
let bytes: &[u8] = &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x04];
let _ = DBKey::parse_data(&Data::from(bytes.to_vec()));
// thread 'main' panicked at src/parser/parser_impl.rs:235:12
}
A 47-byte input that exercises the same fault through the full ZcashdParser::parse_dump pipeline is included in the fuzz corpus on the incoming PR.
Suggested fix
Use checked_add so the overflow path returns Err(BufferUnderflow) cleanly:
pub fn next(&mut self, n: usize) -> Result<&'a [u8]> {
let end = match self.offset.checked_add(n) {
Some(end) => end,
None => {
return Err(Error::BufferUnderflow {
offset: self.offset,
needed: n,
remaining: self.remaining(),
});
}
};
if end > self.buffer.len() {
return Err(Error::BufferUnderflow {
offset: self.offset,
needed: n,
remaining: self.remaining(),
});
}
let bytes = &self.buffer[self.offset..end];
self.offset = end;
// ... existing trace block
Ok(bytes)
}
Both reproducers return Err(BufferUnderflow) with this patch and parsing continues normally on legitimate inputs. The incoming PR also adds the two cargo-fuzz harnesses (fuzz_dbkey_parse_data, fuzz_zcashd_full_parse) so this regression class is caught in CI if you'd like to adopt them.
Severity
Low. Single-process panic on attacker-supplied file in a tool with no production deployment yet (crate is at 0.0.0). Not memory corruption — Rust's slice-bounds check catches the post-wraparound access. The exposure surface is the user-facing wallet recovery use case the crate is built for, so the fix is still worth landing.
AI disclosure
This finding was identified, the patch was drafted, and the fuzz harnesses were built with assistance from Claude (Anthropic). I reviewed each step and am the responsible author.
Summary
Parser::next(src/parser/parser_impl.rs:235) computesself.offset + n > self.buffer.len()with uncheckedusizeaddition. Whennis decoded from aCompactSizelength prefix (e.g. viaparse_string), an attacker-supplied0xffprefix followed by 8 bytes of0xffyieldsn = usize::MAX. The addition wraps modulo2^64, the bounds check evaluatesfalse, and the function falls through to a slice access that panics the thread.Found via cargo-fuzz / libfuzzer within a few seconds of running fresh harnesses against
DBKey::parse_dataand the fullBDBDump → ZcashdDump → ZcashdParser::parse_dumppipeline. Any tool that ingests an untrustedwallet.dat(migration tooling, wallet recovery utilities, light-wallet importers) will panic on a single-record attack input as small as 10 bytes afterdb_dumpextraction.A PR with the fix and the fuzz harnesses is incoming and will reference this issue.
Vulnerable code
src/parser/parser_impl.rs:234-243:When
parse_string(src/zcashd_wallet/parseable_types.rs:27-38) is called,nis the result of decoding aCompactSize. Per the Bitcoin/Zcash convention, prefix byte0xffindicates "next 8 bytes are little-endian u64". Eight bytes of0xffdecode to0xffffffffffffffff = usize::MAXon 64-bit platforms. Withself.offset >= 1,self.offset + usize::MAXwraps and the bounds check is bypassed.Under
-Cdebug-assertions(libfuzzer's instrumented build) the overflow is caught one line earlier withpanic_const_add_overflowatparser_impl.rs:235:12. In standard release builds it panics on the slice index in the next statement. Either way the result is an unrecoverable panic on attacker-controlled input.Reach
DBKey::parse_data(src/zcashd_dump.rs:40) — called for every record key in awallet.datdump.ZcashdDump::from_bdb_dump(src/zcashd_dump.rs:95) — invokesDBKey::parse_dataover all records emitted bydb_dump.ZcashdParser::parse_dump(src/zcashd_parser.rs:36) — the full typed parser, called by the publicmigrate_to_zewifAPI.Sibling audit: I also reviewed the other byte-access methods on
Parserfor the same shape.peek(line 270),rest(line 275), and thestd::io::Readimpl (line 296) each saturatentomin(n, self.remaining())before any slice access, so the addition cannot wrap.nextis the only exploitable site.Reproducer
10-byte input that triggers the panic via
DBKey::parse_data:The first nine bytes form a
CompactSizedecoding tousize::MAX; the trailing0x04is a single data byte. In Rust:A 47-byte input that exercises the same fault through the full
ZcashdParser::parse_dumppipeline is included in the fuzz corpus on the incoming PR.Suggested fix
Use
checked_addso the overflow path returnsErr(BufferUnderflow)cleanly:Both reproducers return
Err(BufferUnderflow)with this patch and parsing continues normally on legitimate inputs. The incoming PR also adds the two cargo-fuzz harnesses (fuzz_dbkey_parse_data,fuzz_zcashd_full_parse) so this regression class is caught in CI if you'd like to adopt them.Severity
Low. Single-process panic on attacker-supplied file in a tool with no production deployment yet (crate is at 0.0.0). Not memory corruption — Rust's slice-bounds check catches the post-wraparound access. The exposure surface is the user-facing wallet recovery use case the crate is built for, so the fix is still worth landing.
AI disclosure
This finding was identified, the patch was drafted, and the fuzz harnesses were built with assistance from Claude (Anthropic). I reviewed each step and am the responsible author.