Skip to content

Commit 70d3710

Browse files
spherelclaude
andcommitted
fix(soundness): bound untrusted num_instances in recursion verifier
Companion to the native-verifier bound. The native verifier rejects any num_instances entry (and their sum) exceeding 2^MAX_NUM_VARIABLES and derives log2_num_instances from those bounded counts. The recursion verifier reads sum_num_instances and log2_num_instances as untrusted hints without bounding them, so a crafted proof could drive pow_2(log2_num_instances) past the field size and wrap the next_pow2 - sum offset, verifying a different statement than the native verifier. verify_chip_proof_pre_main now bounds log2_num_instances <= MAX_NUM_VARIABLES and sum_num_instances / each num_instances[i] <= 2^MAX_NUM_VARIABLES before they are used. Valid proofs are always within this bound, so only malformed proofs are rejected; native and recursion stay in lockstep. Validated: cargo check / clippy clean; aggregation e2e over multi-shard keccak_syscall verifies (leaf proofs run the asserts for every chip, valid proofs are not rejected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ab2f36d commit 70d3710

1 file changed

Lines changed: 26 additions & 1 deletion

File tree

ceno_recursion/src/zkvm_verifier/verifier.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ use crate::{
2929
SepticExtensionVariable, SepticPointVariable, SumcheckLayerProofVariable,
3030
},
3131
};
32-
use ceno_zkvm::structs::{ComposedConstrainSystem, VerifyingKey, ZKVMVerifyingKey};
32+
use ceno_zkvm::{
33+
scheme::constants::MAX_NUM_VARIABLES,
34+
structs::{ComposedConstrainSystem, VerifyingKey, ZKVMVerifyingKey},
35+
};
3336
use ff_ext::BabyBearExt4;
3437

3538
use crate::transcript::{challenger_add_forked_index, clone_challenger_state};
@@ -613,6 +616,28 @@ pub fn verify_chip_proof_pre_main<C: Config>(
613616
let lk_counts_per_instance: Usize<C::N> = Usize::from(lk_len);
614617
let num_batched: Usize<C::N> = Usize::from(num_batched);
615618

619+
// Bound the untrusted instance-count hints before they feed
620+
// `pow_2(log2_num_instances)` and the `next_pow2 - sum` offset, so a crafted
621+
// proof cannot overflow the field. Valid proofs are always within
622+
// `2^MAX_NUM_VARIABLES`.
623+
builder.assert_less_than_slow_small_rhs(
624+
chip_proof.log2_num_instances.get_var(),
625+
RVar::from(MAX_NUM_VARIABLES + 1),
626+
);
627+
let max_instances_plus_one: Var<C::N> = builder.eval(C::N::from_canonical_usize(
628+
(1usize << MAX_NUM_VARIABLES) + 1,
629+
));
630+
builder.assert_less_than_slow_bit_decomp(
631+
chip_proof.sum_num_instances.get_var(),
632+
max_instances_plus_one,
633+
);
634+
builder
635+
.range(0, chip_proof.num_instances.len())
636+
.for_each(|idx_vec, builder| {
637+
let n = builder.get(&chip_proof.num_instances, idx_vec[0]);
638+
builder.assert_less_than_slow_bit_decomp(n, max_instances_plus_one);
639+
});
640+
616641
let log2_num_instances = chip_proof.log2_num_instances.clone();
617642
if composed_cs.has_ecc_ops() {
618643
builder.assign(

0 commit comments

Comments
 (0)