test: property-based fuzzing (proptest) for every untrusted-input parser — message line, PROXY header, WebSocket frame, regex engine, ban-mask, duration; asserts no-panic + round-trip/idempotence/bounds invariants
This commit is contained in:
parent
cf2b157842
commit
621f06448d
7 changed files with 85 additions and 0 deletions
|
|
@ -37,6 +37,8 @@ rustls-pemfile = "2"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# integration tests spawn the built binary and act as a TLS client against it
|
# integration tests spawn the built binary and act as a TLS client against it
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
|
# property-based fuzzing (parsers) + deterministic S2S simulation (convergence).
|
||||||
|
proptest = "1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
|
|
@ -1469,6 +1469,18 @@ pub fn normalize_ban_mask(m: &str) -> String {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
// Fuzz ban-mask normalisation: no input panics, and it's idempotent
|
||||||
|
// (normalising an already-normalised mask changes nothing).
|
||||||
|
#[test]
|
||||||
|
fn normalize_ban_mask_no_panic_and_idempotent(m in ".*") {
|
||||||
|
let once = normalize_ban_mask(&m);
|
||||||
|
let twice = normalize_ban_mask(&once);
|
||||||
|
prop_assert_eq!(once, twice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn account_extban_mask_is_left_verbatim() {
|
fn account_extban_mask_is_left_verbatim() {
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,27 @@ pub fn parse(line: &str) -> Option<Message> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
// Fuzz: no arbitrary line may panic the parser.
|
||||||
|
#[test]
|
||||||
|
fn parse_never_panics(line in ".*") {
|
||||||
|
let _ = parse(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-trip: a parsed line, re-serialised and re-parsed, yields the same
|
||||||
|
// source/command/params (to_wire drops tags by design, so we don't compare those).
|
||||||
|
#[test]
|
||||||
|
fn parse_roundtrips_core_fields(line in ".*") {
|
||||||
|
if let Some(m) = parse(&line) {
|
||||||
|
let again = parse(&m.to_wire());
|
||||||
|
prop_assert_eq!(again.as_ref().map(|x| x.source.clone()), Some(m.source.clone()));
|
||||||
|
prop_assert_eq!(again.as_ref().map(|x| x.command.clone()), Some(m.command.clone()));
|
||||||
|
prop_assert_eq!(again.as_ref().map(|x| x.params.clone()), Some(m.params.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_command() {
|
fn simple_command() {
|
||||||
|
|
|
||||||
11
src/proxy.rs
11
src/proxy.rs
|
|
@ -202,6 +202,17 @@ fn parse_v2_tlvs(mut tlv: &[u8]) -> (bool, Option<String>) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
// Fuzz the PROXY-header parser: no byte prefix may panic, and it must never
|
||||||
|
// claim to have consumed more than it was given.
|
||||||
|
#[test]
|
||||||
|
fn proxy_parse_never_panics_and_bounds_consumed(buf in prop::collection::vec(any::<u8>(), 0..400)) {
|
||||||
|
let (_p, n) = parse(&buf);
|
||||||
|
prop_assert!(n <= buf.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn src(p: &Parsed) -> Option<SocketAddr> {
|
fn src(p: &Parsed) -> Option<SocketAddr> {
|
||||||
match p {
|
match p {
|
||||||
|
|
|
||||||
13
src/regex.rs
13
src/regex.rs
|
|
@ -545,6 +545,19 @@ impl Parser {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
// Fuzz the regex engine: no pattern may panic the compiler, and no
|
||||||
|
// (pattern, text) pair may panic the matcher. The NFA is linear-time, so a
|
||||||
|
// pathological pattern can't hang it either.
|
||||||
|
#[test]
|
||||||
|
fn regex_new_and_match_never_panic(pat in ".*", text in ".*") {
|
||||||
|
if let Ok(re) = Regex::new(&pat) {
|
||||||
|
let _ = re.is_match(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn m(pat: &str, text: &str) -> bool {
|
fn m(pat: &str, text: &str) -> bool {
|
||||||
Regex::new(pat).unwrap().is_match(text)
|
Regex::new(pat).unwrap().is_match(text)
|
||||||
|
|
|
||||||
|
|
@ -640,6 +640,18 @@ fn header(head: &str, name: &str) -> Option<String> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
// Fuzz the WebSocket frame parser: arbitrary bytes must not panic, and a
|
||||||
|
// decoded frame must never report consuming past the buffer.
|
||||||
|
#[test]
|
||||||
|
fn ws_parse_frame_never_panics_and_bounds(buf in prop::collection::vec(any::<u8>(), 0..600)) {
|
||||||
|
if let Ok(Some((_f, n))) = parse_frame(&buf) {
|
||||||
|
prop_assert!(n <= buf.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accept_key_matches_rfc_example() {
|
fn accept_key_matches_rfc_example() {
|
||||||
|
|
|
||||||
14
src/xline.rs
14
src/xline.rs
|
|
@ -371,3 +371,17 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
// Fuzz the duration parser: no arbitrary string may panic it.
|
||||||
|
#[test]
|
||||||
|
fn parse_duration_never_panics(s in ".*") {
|
||||||
|
let _ = parse_duration(&s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue