Skip to content

Integer overflow in Parser::next bounds check causes panic on crafted input #8

Description

@dingledropper

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions