From 97cbe389491b2d6d617196eda3eb397b8acd0c79 Mon Sep 17 00:00:00 2001 From: "ilblackdragon@gmail.com" Date: Tue, 10 Mar 2026 11:15:56 -0700 Subject: [PATCH] =?UTF-8?q?fix(fuzz):=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20LazyLock=20for=20expensive=20constructors,=20fix=20assertion?= =?UTF-8?q?s=20and=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use std::sync::LazyLock to construct Sanitizer, Validator, and LeakDetector once instead of on every fuzz iteration (they compile regex/Aho-Corasick) - Remove fuzz_config_env assertion that panics on null-byte-only input - Remove no-op length check with misleading comment in fuzz_config_env - Update fuzz_config_env description in README to match actual behavior Co-Authored-By: Claude Opus 4.6 --- fuzz/README.md | 2 +- fuzz/fuzz_targets/fuzz_config_env.rs | 29 +++++++--------------- fuzz/fuzz_targets/fuzz_leak_detector.rs | 10 +++++--- fuzz/fuzz_targets/fuzz_safety_sanitizer.rs | 8 +++--- fuzz/fuzz_targets/fuzz_safety_validator.rs | 10 +++++--- fuzz/fuzz_targets/fuzz_tool_params.rs | 7 ++++-- 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/fuzz/README.md b/fuzz/README.md index c4c27c69..1743b18b 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -10,7 +10,7 @@ Fuzz testing for security-critical input parsing paths using [cargo-fuzz](https: | `fuzz_safety_validator` | Input validation (length, encoding, forbidden patterns) | | `fuzz_leak_detector` | Secret leak detection (API keys, tokens, credentials) | | `fuzz_tool_params` | Tool parameter and schema JSON validation | -| `fuzz_config_env` | SafetyLayer end-to-end (sanitize, validate, policy check) | +| `fuzz_config_env` | Combined safety primitives (sanitize, validate, leak detect) | ## Setup diff --git a/fuzz/fuzz_targets/fuzz_config_env.rs b/fuzz/fuzz_targets/fuzz_config_env.rs index 265a85e9..1241d97f 100644 --- a/fuzz/fuzz_targets/fuzz_config_env.rs +++ b/fuzz/fuzz_targets/fuzz_config_env.rs @@ -1,29 +1,24 @@ #![no_main] use libfuzzer_sys::fuzz_target; +use std::sync::LazyLock; use ironclaw::safety::{LeakDetector, Sanitizer, Validator}; +static SANITIZER: LazyLock = LazyLock::new(Sanitizer::new); +static VALIDATOR: LazyLock = LazyLock::new(Validator::new); +static LEAK_DETECTOR: LazyLock = LazyLock::new(LeakDetector::new); + fuzz_target!(|data: &[u8]| { if let Ok(input) = std::str::from_utf8(data) { // Exercise Sanitizer: detect and neutralize prompt injection attempts. - let sanitizer = Sanitizer::new(); - let sanitized = sanitizer.sanitize(input); - // The sanitized content must never be empty when input is non-empty, - // because sanitization wraps/escapes rather than deleting. - if !input.is_empty() { - assert!( - !sanitized.content.is_empty(), - "sanitize() produced empty content for non-empty input" - ); - } + let sanitized = SANITIZER.sanitize(input); // If no modification occurred, content must equal input. if !sanitized.was_modified { assert_eq!(sanitized.content, input); } // Exercise Validator: input validation (length, encoding, patterns). - let validator = Validator::new(); - let result = validator.validate(input); + let result = VALIDATOR.validate(input); // ValidationResult must always be well-formed: if valid, no errors. if result.is_valid { assert!( @@ -33,15 +28,9 @@ fuzz_target!(|data: &[u8]| { } // Exercise LeakDetector: secret detection (API keys, tokens, etc.). - let detector = LeakDetector::new(); - let scan = detector.scan(input); + let scan = LEAK_DETECTOR.scan(input); // scan_and_clean must not panic and must return valid UTF-8. - let cleaned = detector.scan_and_clean(input); - if let Ok(ref clean_str) = cleaned { - // Cleaned output must never be longer than original + redaction markers. - // At minimum it should be valid UTF-8 (guaranteed by String type). - let _ = clean_str.len(); - } + let cleaned = LEAK_DETECTOR.scan_and_clean(input); // If scan found no matches, scan_and_clean should return the input unchanged. if scan.matches.is_empty() { if let Ok(ref clean_str) = cleaned { diff --git a/fuzz/fuzz_targets/fuzz_leak_detector.rs b/fuzz/fuzz_targets/fuzz_leak_detector.rs index f1e6e09c..c8928b67 100644 --- a/fuzz/fuzz_targets/fuzz_leak_detector.rs +++ b/fuzz/fuzz_targets/fuzz_leak_detector.rs @@ -1,13 +1,15 @@ #![no_main] use libfuzzer_sys::fuzz_target; +use std::sync::LazyLock; + use ironclaw::safety::LeakDetector; +static DETECTOR: LazyLock = LazyLock::new(LeakDetector::new); + fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { - let detector = LeakDetector::new(); - // Exercise scan path - let result = detector.scan(s); + let result = DETECTOR.scan(s); // Invariant: if should_block, there must be matches if result.should_block { assert!(!result.matches.is_empty()); @@ -18,6 +20,6 @@ fuzz_target!(|data: &[u8]| { } // Exercise scan_and_clean path - let _ = detector.scan_and_clean(s); + let _ = DETECTOR.scan_and_clean(s); } }); diff --git a/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs b/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs index 32db887d..8b6c9011 100644 --- a/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs +++ b/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs @@ -1,13 +1,15 @@ #![no_main] use libfuzzer_sys::fuzz_target; +use std::sync::LazyLock; + use ironclaw::safety::Sanitizer; +static SANITIZER: LazyLock = LazyLock::new(Sanitizer::new); + fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { - let sanitizer = Sanitizer::new(); - // Exercise the main sanitization path - let result = sanitizer.sanitize(s); + let result = SANITIZER.sanitize(s); // Verify invariant: warnings should have valid ranges for w in &result.warnings { assert!(w.location.end <= s.len()); diff --git a/fuzz/fuzz_targets/fuzz_safety_validator.rs b/fuzz/fuzz_targets/fuzz_safety_validator.rs index 065bc86d..cbf3bca6 100644 --- a/fuzz/fuzz_targets/fuzz_safety_validator.rs +++ b/fuzz/fuzz_targets/fuzz_safety_validator.rs @@ -1,13 +1,15 @@ #![no_main] use libfuzzer_sys::fuzz_target; +use std::sync::LazyLock; + use ironclaw::safety::Validator; +static VALIDATOR: LazyLock = LazyLock::new(Validator::new); + fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { - let validator = Validator::new(); - // Exercise input validation - let result = validator.validate(s); + let result = VALIDATOR.validate(s); // Invariant: empty input is always invalid if s.is_empty() { assert!(!result.is_valid); @@ -15,7 +17,7 @@ fuzz_target!(|data: &[u8]| { // Exercise tool parameter validation with arbitrary JSON if let Ok(value) = serde_json::from_str::(s) { - let _ = validator.validate_tool_params(&value); + let _ = VALIDATOR.validate_tool_params(&value); } } }); diff --git a/fuzz/fuzz_targets/fuzz_tool_params.rs b/fuzz/fuzz_targets/fuzz_tool_params.rs index 52e39867..0c840eda 100644 --- a/fuzz/fuzz_targets/fuzz_tool_params.rs +++ b/fuzz/fuzz_targets/fuzz_tool_params.rs @@ -1,15 +1,18 @@ #![no_main] use libfuzzer_sys::fuzz_target; +use std::sync::LazyLock; + use ironclaw::safety::Validator; use ironclaw::tools::validate_tool_schema; +static VALIDATOR: LazyLock = LazyLock::new(Validator::new); + fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { // Try parsing as JSON and validating as tool parameters if let Ok(value) = serde_json::from_str::(s) { // Exercise Validator::validate_tool_params with arbitrary JSON - let validator = Validator::new(); - let result = validator.validate_tool_params(&value); + let result = VALIDATOR.validate_tool_params(&value); // Invariant: result should always be well-formed if !result.is_valid { assert!(!result.errors.is_empty());