Skip to content

security: P0/P1 audit fixes (C-2, H-2)#1

Open
LucienSong wants to merge 1 commit into
ShellDAO:mainfrom
LucienSong:security/audit-fixes-r1
Open

security: P0/P1 audit fixes (C-2, H-2)#1
LucienSong wants to merge 1 commit into
ShellDAO:mainfrom
LucienSong:security/audit-fixes-r1

Conversation

@LucienSong
Copy link
Copy Markdown
Contributor

Summary

Fixes two P0/P1 security findings from the 2026-05-24 consolidated audit report.


C-2 — Batch precompile charges gas after work (free DoS)

File: crates/pqvm-precompiles/src/lib.rs

Root cause: verify_ml_dsa_65_batch executed the full verification loop before gas was charged, allowing an attacker to force unbounded CPU work at zero effective cost.

Fix:

  • Added MAX_BATCH_SIGNATURES: u32 = 256; inputs with a larger count are rejected immediately with PrecompileError::BatchTooLarge before any work begins.
  • Header is parsed upfront to extract n_sigs; total_cost is computed via checked_mul and charged before the verification loop.
  • New error variants: BatchTooLarge { n_sigs, max }, GasOverflow.

New tests:

  • batch_verify_oog_before_verification: n_sigs=100, gas_limit=10 → OOG error, no verification performed.
  • batch_verify_rejects_too_many_sigs: n_sigs=257 → BatchTooLarge error.

H-2 — Interpreter panics on attacker-controlled operands

File: crates/pqvm-interpreter/src/lib.rs

Root cause: Two U256::to::<T>() calls on stack values would panic on overflow; PQVERIFY also silently accepted unknown algo_id values.

Fix:

  • BLOCKHASH (0x40): .to::<u64>().saturating_to::<u64>(). Values > u64::MAX saturate to u64::MAX, which exceeds any plausible block height; the DB naturally returns B256::ZERO, matching EVM semantics.
  • PQVERIFY (0xB0): .to::<u8>().saturating_to::<u8>(). Unknown algo_id after the saturating cast now returns InterpreterError::InvalidPqAlgo(id) instead of silently pushing 0.
  • No other .to::<T>() calls on user-controlled stack data exist (verified with rg).

New tests:

  • blockhash_with_oversized_block_number_returns_zero: stack-top = U256::MAX → returns B256::ZERO without panic.
  • pqverify_with_unknown_algo_returns_invalid_pq_algo: algo_id=255InvalidPqAlgo(0xff) without panic.

Test results

All 55 workspace tests pass. cargo clippy --workspace --all-targets -- -D warnings clean.

Copilot AI review requested due to automatic review settings May 23, 2026 21:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Addresses two high-severity security audit findings by (1) preventing free CPU DoS in the ML-DSA-65 batch verification precompile via upfront gas charging + batch-size limiting, and (2) removing overflow panics from attacker-controlled stack operands in the interpreter and hard-failing unknown PQVERIFY algorithm IDs.

Changes:

  • Precompile: parse batch header up front, cap n_sigs (MAX_BATCH_SIGNATURES), compute/charge total gas before doing any batch verification work, and add BatchTooLarge/GasOverflow errors + regression tests.
  • Interpreter: replace panicking U256::to::<T>() conversions in BLOCKHASH and PQVERIFY with saturating_to::<T>(), add InvalidPqAlgo(u8) error, and add regression tests for both cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
crates/pqvm-precompiles/src/lib.rs Enforces batch limits and charges batch-verify gas before verification to prevent free CPU work; adds new error variants and regression tests.
crates/pqvm-interpreter/src/lib.rs Prevents overflow panics in BLOCKHASH/PQVERIFY operand decoding and rejects unknown PQ algorithm IDs; adds targeted regression tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +614 to +617
// saturating_to: a U256 > u64::MAX saturates to u64::MAX which
// exceeds any plausible block number; the DB returns B256::ZERO
// for unknown heights, matching EVM BLOCKHASH semantics.
let n = self.pop()?.saturating_to::<u64>();
C-2: charge batch precompile gas before verification loop; cap n_sigs
- Add MAX_BATCH_SIGNATURES = 256; reject larger headers before any work
- Compute total_cost via checked_mul; charge BEFORE the verification loop
- Add PrecompileError::BatchTooLarge and ::GasOverflow variants
- Tests: OOG-before-work with n_sigs=100 and gas=10; BatchTooLarge for n_sigs=257

H-2: replace U256::to::<T>() with saturating_to + explicit domain check
- BLOCKHASH (0x40): .to::<u64>() -> .saturating_to::<u64>(); u64::MAX > any
  plausible block height so the DB naturally returns B256::ZERO
- PQVERIFY (0xB0): .to::<u8>() -> .saturating_to::<u8>(); unknown algo_id
  now returns InterpreterError::InvalidPqAlgo(id) instead of silently
  pushing 0 (no other .to::<T>() calls on user-controlled stack data)
- Tests: BLOCKHASH with U256::MAX returns ZERO without panic;
  PQVERIFY with algo_id=255 returns InvalidPqAlgo without panic

Audit ref: 2026-05-24 consolidated report

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@LucienSong LucienSong force-pushed the security/audit-fixes-r1 branch from bfac0c1 to 6e56d5f Compare May 27, 2026 07:15
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.

2 participants