mod support {
pub mod parser_hardening;
}
use proptest::prelude::*;
use reddb_server::storage::query::parser::{self, ParseError, ParseErrorKind, ParserLimits};
use support::parser_hardening::{
self as harness, assert_no_panic_on, corpus::subquery_adversarial_inputs, subquery_grammar,
HardenedParser,
};
pub struct SubqueryParser;
impl HardenedParser for SubqueryParser {
type Error = ParseError;
fn parse(input: &str) -> Result<(), Self::Error> {
parser::parse(input).map(|_| ())
}
fn parse_with_limits(input: &str, limits: ParserLimits) -> Result<(), Self::Error> {
let mut p = parser::Parser::with_limits(input, limits)?;
p.parse().map(|_| ())
}
}
#[test]
fn subquery_parser_does_not_panic_on_adversarial_corpus() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
for (name, input) in subquery_adversarial_inputs() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_no_panic_on::<SubqueryParser>(&input);
}));
if result.is_err() {
panic!("subquery adversarial corpus entry {} panicked", name);
}
}
})
.expect("spawn corpus thread");
handle.join().expect("corpus thread panic");
}
#[test]
fn dos_limit_deeply_nested_select_subquery_does_not_overflow_stack() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let input = subquery_grammar::nested_scalar_subquery(200);
let err = parser::parse(&input).err().expect("must error, not panic");
assert!(
matches!(
err.kind,
ParseErrorKind::DepthLimit {
limit_name: "max_depth",
..
} | ParseErrorKind::Syntax
),
"expected DepthLimit or Syntax (Phase A), got: {:?}",
err.kind
);
})
.expect("spawn deep-SELECT thread");
handle.join().expect("deep-SELECT thread must not panic");
}
#[test]
fn dos_limit_deeply_nested_from_subquery_returns_depth_limit() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let depth = 200usize;
let mut s = String::new();
for _ in 0..depth {
s.push_str("FROM (SELECT * ");
}
s.push_str("FROM t");
for i in 0..depth {
s.push_str(&format!(") AS a{}", i));
}
let err = parser::parse(&s).err().expect("must error, not panic");
assert!(
matches!(
err.kind,
ParseErrorKind::DepthLimit {
limit_name: "max_depth",
..
} | ParseErrorKind::Syntax
),
"expected DepthLimit or Syntax (Phase A), got: {:?}",
err.kind
);
})
.expect("spawn deep-FROM-subquery thread");
handle
.join()
.expect("deep-FROM-subquery thread must not panic");
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn proptest_where_in_subquery_no_panic(
s in subquery_grammar::where_in_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
#[test]
fn proptest_where_exists_subquery_no_panic(
s in subquery_grammar::where_exists_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
#[test]
fn proptest_scalar_subquery_no_panic(
s in subquery_grammar::scalar_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
#[test]
fn proptest_from_aliased_subquery_roundtrips(
s in subquery_grammar::from_aliased_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
prop_assert!(
SubqueryParser::parse(&s).is_ok(),
"FROM (SELECT …) AS sub did not parse: {}", s
);
}
#[test]
fn proptest_correlated_subquery_no_panic(
s in subquery_grammar::correlated_subquery_stmt(),
) {
harness::roundtrip_property::<SubqueryParser>(&s);
}
#[test]
fn proptest_subquery_arbitrary_suffix_no_panic(
prefix in prop_oneof![
Just("SELECT * FROM t WHERE x IN (".to_string()),
Just("SELECT * FROM t WHERE EXISTS (".to_string()),
Just("SELECT * FROM t WHERE x = (SELECT ".to_string()),
Just("FROM (SELECT ".to_string()),
Just("FROM (SELECT id FROM t) AS ".to_string()),
],
suffix in ".{0,256}",
) {
let s = format!("{}{}", prefix, suffix);
harness::roundtrip_property::<SubqueryParser>(&s);
}
#[test]
fn proptest_subquery_depth_limit_enforced(
depth in 40usize..120,
) {
let limits = ParserLimits {
max_depth: 16,
..ParserLimits::default()
};
let input = subquery_grammar::nested_in_subquery(depth);
let r = SubqueryParser::parse_with_limits(&input, limits);
prop_assert!(r.is_err(), "deeply nested subquery must error under tight depth");
}
}