Skip to content

fix: return Err on malformed proofs and out-of-range params instead of panicking#14

Open
mwaddip wants to merge 1 commit into
ergoplatform:mainfrom
mwaddip:fix/no-panic-malformed-input
Open

fix: return Err on malformed proofs and out-of-range params instead of panicking#14
mwaddip wants to merge 1 commit into
ergoplatform:mainfrom
mwaddip:fix/no-panic-malformed-input

Conversation

@mwaddip

@mwaddip mwaddip commented Jun 7, 2026

Copy link
Copy Markdown

Problem

BatchAVLVerifier panics on several classes of malformed input instead of returning an error:

  • empty / truncated / garbage proof bytes → slice-bounds or stack.pop().unwrap() panic during tree reconstruction
  • an out-of-range (e.g. wrapped-negative) key length → slice-bounds or arithmetic-overflow panic
  • an operation whose value length ≠ the tree's fixed value length → assert! panic in the modify path

The reference Scala/scorex BatchAVLVerifier wraps reconstruction in a Try and treats any failure as "no reconstructed tree", so its consumers (sigma's CErgoTreeEvaluator / CAvlTreeVerifier) never observe a panic — a failed construction yields a verifier whose operations fail and whose digest is None. Because this crate panics instead, a consumer that feeds it crafted / attacker-supplied proof bytes (e.g. sigma-rust evaluating an AvlTree operation) crashes rather than failing the operation cleanly — a crash-on-deserialize / DoS risk.

Fix

Honor the crate's existing Result / ensure! contract at the panic sites so the same inputs return Err:

  • reconstruct_tree: bounds-checked proof reads via a small read_proof_slice helper (with checked_add against length overflow), a guarded loop bound, and ok_or on the stack pops.
  • modify_helper: assert!ensure! on the two value-length checks.

No behavior change for valid proofs — the new checks only fire where the code would previously have panicked. Works under no_std and is unaffected by panic = "abort".

Tests

tests/malformed_proof.rs — empty, single-garbage-byte, truncated-label, and truncated-real proofs; an oversized key length; and a wrong-length operation value. Each asserts Err rather than a panic.

Found via SANTA conformance testing of the sigma-rust port against the JVM oracle.

…f panicking

`BatchAVLVerifier` tree reconstruction and the value-length checks in the modify
path used raw slice indexing, `stack.pop().unwrap()`, and `assert!`, so a
malformed / truncated / empty proof, an out-of-range key length, or an operation
whose value length does not match the tree's fixed value length panicked instead
of failing gracefully.

The reference (scorex) `BatchAVLVerifier` wraps reconstruction in a `Try` and
treats any failure as "no reconstructed tree", so downstream consumers (e.g.
sigma's `CErgoTreeEvaluator` / `CAvlTreeVerifier`) never observe a panic — they
see a verifier whose operations fail and whose digest is `None`. Honor the
crate's existing `Result` / `ensure!` contract at the panic sites so the same
inputs return `Err` here too:

- `reconstruct_tree`: bounds-checked proof reads via a `read_proof_slice` helper
  (with `checked_add` to avoid length overflow), a guarded loop bound, and
  `ok_or` on the stack pops.
- `modify_helper`: `assert!` -> `ensure!` on the value-length checks.

Adds `tests/malformed_proof.rs` covering empty / garbage / truncated proofs, an
oversized key length, and a wrong-length operation value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mwaddip

mwaddip commented Jun 7, 2026

Copy link
Copy Markdown
Author

Consumer side: ergoplatform/sigma-rust#892 (eval-layer regression tests, Draft). These should merge together — that PR's tests exercise this fix through sigma-rust's AVL evaluator and are #[ignore]d until this releases + sigma-rust bumps the version.

mwaddip added a commit to mwaddip/sigma-rust that referenced this pull request Jun 7, 2026
…annot start

Scala's CAvlTreeVerifier.treeHeight reads BatchAVLVerifier.rootNodeHeight,
which is assigned from the digest's trailing byte only after reconstruction's
up-front requires (keyLength > 0, digest length) pass. A non-positive
keyLength (signed Int on the JVM; wire values with the high bit set) fails
before the assignment, so the JVM charges the degenerate tree a zero-height
walk. We read the digest byte unconditionally, overcharging the height-scaled
Lookup/Insert/Update/Remove ops on those shapes. Failures during proof
parsing (malformed proof bytes, wrong value length) happen after the
assignment and correctly keep the digest-derived height.

The degenerate path is observable end-to-end only with
ergoplatform/ergo_avltree_rust#14 (the crates.io verifier panics on these
inputs before any per-op charge is reached); pinned here at the unit level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mwaddip added a commit to mwaddip/sigma-rust that referenced this pull request Jun 7, 2026
…annot start

Scala's CAvlTreeVerifier.treeHeight reads BatchAVLVerifier.rootNodeHeight,
which is assigned from the digest's trailing byte only after reconstruction's
up-front requires (keyLength > 0, digest length) pass. A non-positive
keyLength (signed Int on the JVM; wire values with the high bit set) fails
before the assignment, so the JVM charges the degenerate tree a zero-height
walk. We read the digest byte unconditionally, overcharging the height-scaled
Lookup/Insert/Update/Remove ops on those shapes. Failures during proof
parsing (malformed proof bytes, wrong value length) happen after the
assignment and correctly keep the digest-derived height.

The degenerate path is observable end-to-end only with
ergoplatform/ergo_avltree_rust#14 (the crates.io verifier panics on these
inputs before any per-op charge is reached); pinned here at the unit level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant