Problem:
eval_circuit::load_ops switched ops.bin to a plaintext header plus zstd-compressed fixed-width records. The loader intends to reject trailing data and bound untrusted input, but two audit findings remain:
- A file with a valid header and two concatenated empty zstd frames is accepted by
load_ops; it then fails later at register validation instead of failing as non-canonical trailing data.
- The loader allocates
Vec::with_capacity(n) directly from the declared untrusted op count after only checking n <= MAX_OPS, where MAX_OPS = 4_000_000_000. That cap is far above a predictable memory budget for Vec<Op> and happens before the body proves it contains that many records.
Evidence:
Checked on origin/main / da51a4807f92e4dd6df60262a9258aa751adbb58 in a clean detached worktree.
Relevant code:
src/bin/eval_circuit.rs:96 fn load_ops(path: &str) -> Result<Vec<Op>, String>
src/bin/eval_circuit.rs:108 if n > MAX_OPS { ... }
src/bin/eval_circuit.rs:121 let mut ops = Vec::with_capacity(n);
src/bin/eval_circuit.rs:160 // Reject trailing data: exactly n records must decompress, no more.
src/bin/eval_circuit.rs:162 match dec.read(&mut extra) { ... }
Reproducer for concatenated empty frames:
cargo build --release --locked --bin eval_circuit
rm -f ops.bin score.json results.tsv
python3 - <<'PY'
from pathlib import Path
Path('ops.bin').write_bytes(b'QECCOPSZ' + (0).to_bytes(8, 'little'))
PY
printf '' | zstd -q -c >> ops.bin
printf '' | zstd -q -c >> ops.bin
./target/release/eval_circuit --note zero-twoframes
Observed output:
=== quantum_ecc: eval_circuit (trusted stage) ===
loaded ops : 0
!! eval FAILED: expected 4 registers, got 0
For comparison, appending raw bytes after the empty frame is rejected by the loader:
!! could not load ops.bin: ops.bin: error checking for trailing data: Unknown frame descriptor
Effect:
The trusted stage does not enforce a canonical single-frame compressed body. Separately, a forged header can steer allocation shape before the compressed body is validated. Neither issue changes the current accepted score, but both weaken the loader's fail-closed story and make malformed input handling harder to reason about.
Smallest useful fix:
Use a single-frame zstd decode mode or explicitly verify EOF on the underlying reader after the frame finishes, so concatenated empty frames fail in load_ops. Replace Vec::with_capacity(n) with a checked reservation path and a memory-derived MAX_OPS cap, or stream records through validation/counting before committing to a full vector-sized allocation.
Gate:
Add loader tests that cover:
- valid zero-record frame (then downstream register validation can fail as today),
- raw trailing bytes rejected in
load_ops,
- concatenated empty zstd frames rejected in
load_ops,
- declared counts that exceed the memory-derived cap rejected before allocation.
Problem:
eval_circuit::load_opsswitchedops.binto a plaintext header plus zstd-compressed fixed-width records. The loader intends to reject trailing data and bound untrusted input, but two audit findings remain:load_ops; it then fails later at register validation instead of failing as non-canonical trailing data.Vec::with_capacity(n)directly from the declared untrusted op count after only checkingn <= MAX_OPS, whereMAX_OPS = 4_000_000_000. That cap is far above a predictable memory budget forVec<Op>and happens before the body proves it contains that many records.Evidence:
Checked on
origin/main/da51a4807f92e4dd6df60262a9258aa751adbb58in a clean detached worktree.Relevant code:
Reproducer for concatenated empty frames:
Observed output:
For comparison, appending raw bytes after the empty frame is rejected by the loader:
Effect:
The trusted stage does not enforce a canonical single-frame compressed body. Separately, a forged header can steer allocation shape before the compressed body is validated. Neither issue changes the current accepted score, but both weaken the loader's fail-closed story and make malformed input handling harder to reason about.
Smallest useful fix:
Use a single-frame zstd decode mode or explicitly verify EOF on the underlying reader after the frame finishes, so concatenated empty frames fail in
load_ops. ReplaceVec::with_capacity(n)with a checked reservation path and a memory-derivedMAX_OPScap, or stream records through validation/counting before committing to a full vector-sized allocation.Gate:
Add loader tests that cover:
load_ops,load_ops,