diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..b966c40 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +artifacts +coverage +corpus diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..6266efb --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rustguac-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.rustguac] +path = ".." + +[[bin]] +name = "protocol_parse" +path = "fuzz_targets/protocol_parse.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "protocol_stream" +path = "fuzz_targets/protocol_stream.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/protocol_parse.rs b/fuzz/fuzz_targets/protocol_parse.rs new file mode 100644 index 0000000..fb43721 --- /dev/null +++ b/fuzz/fuzz_targets/protocol_parse.rs @@ -0,0 +1,10 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use rustguac::protocol::Instruction; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + // Fuzz single-instruction parsing + let _ = Instruction::parse(s); + } +}); diff --git a/fuzz/fuzz_targets/protocol_stream.rs b/fuzz/fuzz_targets/protocol_stream.rs new file mode 100644 index 0000000..34d3bcb --- /dev/null +++ b/fuzz/fuzz_targets/protocol_stream.rs @@ -0,0 +1,28 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use rustguac::protocol::InstructionParser; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + // Fuzz the streaming parser: split input into random-sized chunks + // to exercise buffer accumulation and boundary handling + let mut parser = InstructionParser::new(); + let bytes = s.as_bytes(); + let mut pos = 0; + let mut chunk_size = 1; + while pos < bytes.len() { + let end = (pos + chunk_size).min(bytes.len()); + if let Ok(chunk) = std::str::from_utf8(&bytes[pos..end]) { + for result in parser.receive(chunk) { + // Exercise encode on successfully parsed instructions + if let Ok(inst) = result { + let _ = inst.encode(); + } + } + } + pos = end; + // Vary chunk sizes: 1, 2, 4, 8, ... then wrap back + chunk_size = if chunk_size >= 64 { 1 } else { chunk_size * 2 }; + } + } +}); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ab8e71f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +/// Re-export modules for fuzz targets and testing. +pub mod protocol; diff --git a/src/protocol.rs b/src/protocol.rs index 60c210a..7a69107 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -49,10 +49,13 @@ impl Instruction { .map_err(|_| ParseError::InvalidLength)?; remaining = &remaining[dot_pos + 1..]; - // Extract element value + // Extract element value (length is in bytes per Guacamole spec) if remaining.len() < len { return Err(ParseError::Truncated); } + if !remaining.is_char_boundary(len) { + return Err(ParseError::Truncated); + } elements.push(remaining[..len].to_string()); remaining = &remaining[len..];