Add fuzz testing infrastructure and fix UTF-8 boundary panic

Add cargo-fuzz targets for the Guacamole protocol parser:
- protocol_parse: single instruction parsing
- protocol_stream: streaming parser with chunked input

Fix panic in Instruction::parse when a length prefix splits a multi-byte
UTF-8 character (found by fuzzer within seconds). Now returns
ParseError::Truncated instead of panicking on invalid char boundary.

Run with: cargo +nightly fuzz run protocol_parse
          cargo +nightly fuzz run protocol_stream

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave Kempe
2026-02-07 13:39:16 +11:00
parent eab04ba2bd
commit 1922bd9987
6 changed files with 76 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
target
artifacts
coverage
corpus
+28
View File
@@ -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
+10
View File
@@ -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);
}
});
+28
View File
@@ -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 };
}
}
});
+2
View File
@@ -0,0 +1,2 @@
/// Re-export modules for fuzz targets and testing.
pub mod protocol;
+4 -1
View File
@@ -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..];