diff --git a/crypto/stark/src/tests/small_trace_tests.rs b/crypto/stark/src/tests/small_trace_tests.rs index 96e04858d..ea8d3bc4a 100644 --- a/crypto/stark/src/tests/small_trace_tests.rs +++ b/crypto/stark/src/tests/small_trace_tests.rs @@ -116,6 +116,34 @@ fn test_verify_rejects_truncated_composition_poly_parts_ood() { ); } +/// A malformed proof whose `deep_poly_openings` Vec is shorter than the FRI +/// query count. `reconstruct_deep_composition_poly_evaluations_for_all_queries` +/// indexes `deep_poly_openings[i]` for every query index, and this Vec's length +/// is not otherwise bound (the `query_list.len()` guard checks a different +/// field), so a truncated `deep_poly_openings` must make the verifier return +/// `false` instead of panicking with an out-of-bounds index in release builds. +#[test_log::test] +fn test_verify_rejects_truncated_deep_poly_openings() { + let (air, mut proof) = make_valid_simple_proof(); + + assert!( + proof.deep_poly_openings.len() >= 2, + "test precondition: a valid proof has one deep-poly opening per FRI query", + ); + // Drop the last opening so the Vec is shorter than `fri_number_of_queries`; + // the query loop would then index past the end. + proof.deep_poly_openings.pop(); + + assert!( + !Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]) + ), + "Verifier must reject when deep_poly_openings is shorter than the query count" + ); +} + /// A malformed proof whose deep-poly opening `evaluations` slice has the /// wrong number of columns. The runtime width-mismatch guard added in this /// PR must cause the verifier to return `false` instead of indexing past diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 616732e22..a9dc8f381 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -539,6 +539,18 @@ pub trait IsStarkVerifier< proof: &StarkProof, ) -> Option> { let num_queries = challenges.iotas.len(); + + // `deep_poly_openings` comes straight from the untrusted proof and its + // length is not otherwise pinned (the `query_list.len()` guard checks a + // different field). The loop below indexes `deep_poly_openings[i]` for + // every `i` in `0..num_queries`, so a truncated Vec would panic the + // verifier with an out-of-bounds index on a malicious proof. Reject + // instead. (Extra entries are harmless — they are never indexed — + // matching the `<` convention of the `query_list` guard.) + if proof.deep_poly_openings.len() < num_queries { + return None; + } + let mut deep_poly_evaluations = Vec::with_capacity(num_queries); let mut deep_poly_evaluations_sym = Vec::with_capacity(num_queries);