blob: cf58348915336921ee00842a06906f5c3d7c65a9 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::domains::GenericCorpusValue;
use super::domains::GenericDomain;
use std::collections::HashMap;
use std::fmt::Write;
use std::marker::PhantomData;
use std::mem;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
const REPORT_SEPARATOR: &str = "=================================================================";
const VALUE_MAX_PRINT_LENGTH: usize = 2048;
/// A trait implemented by types used to Fuzz a given property function.
///
/// When a test is annotated with the `fuzztest` macro attribute, that test will be wrapped in a
/// FuzzTestObject that implements this trait. The FuzzTestObject is created by the macro and holds,
/// among others, the property function and the domains of its arguments.
/// The FuzzTestObject is then manipulated by Fuzzing Test Engine through the APIs of this trait.
pub trait FuzzTest {
// TODO(tinmar): refine arguments as we get a clearer picture of
// what the dispatcher interface looks like
fn name(&self) -> &'static str;
fn activate(&mut self);
fn mutate(&mut self);
/// Executes the property function with the given arguments
/// (will attempt to downcast to actual user values).
///
/// Returns `true` if the property function holds, `false` if it crashes.
fn execute(&self, args: &GenericCorpusValue) -> bool;
/// Formats and prints the counterexample finding report for `args` to `stderr`.
fn print_finding_report(&self, args: &GenericCorpusValue);
/// Returns the static metadata identifying this fuzz test.
fn info(&self) -> &'static FuzzTestInfo;
fn domains(&self) -> Arc<Mutex<dyn GenericDomain>>;
}
/// Identifies the property function of a fuzz test.
///
/// `FuzzTestInfo` is instantiated by the `fuzztest` macro which populates the fields with the
/// appropriate values.
pub struct FuzzTestInfo {
pub name: &'static str,
pub module_path: &'static str,
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl FuzzTestInfo {
/// Returns the formatted test name (e.g.,
/// `__fuzztest_mod__unescaping_never_panics::unescaping_never_panics`).
pub fn full_test_name(&self) -> String {
let full = format!("{}::{}", self.module_path, self.name);
full.split_once("::").map_or(full.as_str(), |(_, rest)| rest).to_string()
}
}
/// Truncates a `Debug`-formatted argument string if it exceeds `VALUE_MAX_PRINT_LENGTH` bytes.
pub fn format_debug_arg(mut formatted: String) -> String {
if formatted.len() > VALUE_MAX_PRINT_LENGTH {
let mut end = VALUE_MAX_PRINT_LENGTH;
while !formatted.is_char_boundary(end) {
end -= 1;
}
formatted.truncate(end);
formatted.push_str(" ...<value too long>");
}
formatted
}
/// Formats the `=== BUG FOUND!` finding report block for the given fuzz test and formatted
/// arguments.
pub fn format_finding_report(info: &FuzzTestInfo, formatted_args: &[String]) -> String {
let mut out = String::new();
let _ = writeln!(out, "\n{REPORT_SEPARATOR}");
let _ = writeln!(out, "=== BUG FOUND!\n");
let _ = writeln!(
out,
"{}:{}: Counterexample found for {}.",
info.file,
info.line,
info.full_test_name()
);
let _ = writeln!(out, "The test fails with input:");
for (idx, arg) in formatted_args.iter().enumerate() {
let _ = writeln!(out, "argument {idx}: {arg}");
}
let _ = writeln!(out, "\n{REPORT_SEPARATOR}");
out
}
/// Prints the `=== BUG FOUND!` finding report block to `stderr`.
pub fn print_finding_report(info: &FuzzTestInfo, formatted_args: &[String]) {
let _ = std::io::Write::write_all(
&mut std::io::stderr(),
format_finding_report(info, formatted_args).as_bytes(),
);
}
struct FindingReportContext {
fuzz_test: *const dyn FuzzTest,
input: *const GenericCorpusValue,
// Tracks whether the finding report has already been printed for the current execution so we
// don't print duplicate `=== BUG FOUND!` blocks if the sanitizer error summary hook is invoked
// multiple times for a single crash.
report_printed: bool,
}
thread_local! {
/// A thread-local context holding the currently executing fuzz test and input for printing
/// the `=== BUG FOUND!` counterexample report from sanitizer crash callbacks.
///
/// Thread-local storage is necessary because C callbacks (e.g. sanitizer hooks in
/// `crash_handler.rs`) do not have access to the fuzz test or input, and in smoke-test mode
/// `libtest` runs multiple fuzz tests concurrently on separate threads. Wrapping in `Mutex`
/// allows same-thread sanitizer callbacks to use `try_lock()` safely without risking a
/// reentrancy panic.
static FINDING_REPORT_CONTEXT: spin::Mutex<Option<FindingReportContext>> =
const { spin::Mutex::new(None) };
}
/// A guard that manages the thread-local `FINDING_REPORT_CONTEXT` state, setting it to `Some` on
/// creation, and resetting to `None` on drop.
#[must_use = "the finding report guard must be held for the duration of the test execution to print counterexample reports on crashes"]
struct FindingReportGuard {
// Ensures `FindingReportGuard` is `!Send` so it is always dropped on the same thread that
// entered it, clearing that thread's `FINDING_REPORT_CONTEXT`.
_not_send: PhantomData<*const ()>,
}
impl FindingReportGuard {
/// Enters the finding report context by setting `FINDING_REPORT_CONTEXT` on the current thread.
///
/// # Safety
///
/// * `fuzz_test` and `input` must remain valid for shared borrows for the entire lifetime of
/// the returned `FindingReportGuard`.
/// * The returned `FindingReportGuard` must be dropped (not forgotten) before `fuzz_test` or
/// `input` is invalidated.
unsafe fn enter(fuzz_test: &dyn FuzzTest, input: &GenericCorpusValue) -> Self {
// SAFETY: `*const (dyn FuzzTest + '_)` and `*const (dyn FuzzTest + 'static)` have the
// exact same fat-pointer layout; erasing the lifetime on the raw pointer is sound because
// the caller guarantees `fuzz_test` remains valid until this guard is dropped.
let fuzz_test_ptr: *const (dyn FuzzTest + 'static) =
unsafe { mem::transmute(fuzz_test as *const dyn FuzzTest) };
FINDING_REPORT_CONTEXT.with(|ctx| {
*ctx.lock() = Some(FindingReportContext {
fuzz_test: fuzz_test_ptr,
input: input as *const GenericCorpusValue,
report_printed: false,
});
});
Self { _not_send: PhantomData }
}
}
impl Drop for FindingReportGuard {
fn drop(&mut self) {
let _ = FINDING_REPORT_CONTEXT.try_with(|ctx| {
*ctx.lock() = None;
});
}
}
/// Runs `f` with the thread-local `FINDING_REPORT_CONTEXT` set to `fuzz_test` and `input`,
/// ensuring that sanitizer crash callbacks invoked during `f` can print the counterexample report
/// and that the context is cleared when `f` returns or unwinds.
pub fn with_finding_report_context<R>(
fuzz_test: &dyn FuzzTest,
input: &GenericCorpusValue,
f: impl FnOnce() -> R,
) -> R {
// SAFETY: `fuzz_test` and `input` are borrowed for the entire body of
// `with_finding_report_context`, and `_report_guard` is a stack-local variable that cannot be
// forgotten by `f` and is dropped before this function returns or unwinds.
let _report_guard = unsafe { FindingReportGuard::enter(fuzz_test, input) };
f()
}
/// Prints the finding report if we are currently in an active finding report context on the
/// current thread and have not yet printed a report for this execution.
/// It only tries once to acquire the lock on `FINDING_REPORT_CONTEXT`.
pub(crate) fn try_print_finding_report() -> bool {
FINDING_REPORT_CONTEXT
.try_with(|ctx| {
let Some(mut report_guard) = ctx.try_lock() else {
return false;
};
let Some(active_exec) = report_guard.as_mut() else {
return false;
};
if active_exec.report_printed {
return false;
}
active_exec.report_printed = true;
// SAFETY: While `FINDING_REPORT_CONTEXT` is `Some` on this thread, `fuzz_test` and
// `input` point to live values borrowed by an active `with_finding_report_context`
// frame on the current thread's stack, and no mutable references to them exist.
unsafe {
(*active_exec.fuzz_test).print_finding_report(&*active_exec.input);
}
true
})
.unwrap_or(false)
}
pub type BoxedFuzzTest = Box<dyn FuzzTest + Send + Sync>;
pub struct FuzzTestRegistration {
pub info: &'static FuzzTestInfo,
pub factory: fn() -> BoxedFuzzTest,
}
inventory::collect!(FuzzTestRegistration);
#[allow(clippy::type_complexity)]
pub static FUZZ_TEST_NAME_TO_FACTORY: LazyLock<HashMap<&str, fn() -> BoxedFuzzTest>> =
LazyLock::new(|| {
inventory::iter
.into_iter()
.map(|&FuzzTestRegistration { info, factory }| (info.name, factory))
.collect()
});
pub struct InputStateAndDomain<I, D> {
pub input_state: Option<I>,
pub domain: D,
}
#[cfg(test)]
mod tests {
use super::*;
use googletest::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingFuzzTest {
print_count: AtomicUsize,
}
impl FuzzTest for CountingFuzzTest {
fn name(&self) -> &'static str {
"counting_fuzz_test"
}
fn activate(&mut self) {}
fn mutate(&mut self) {}
fn execute(&self, _args: &GenericCorpusValue) -> bool {
true
}
fn print_finding_report(&self, _args: &GenericCorpusValue) {
self.print_count.fetch_add(1, Ordering::Relaxed);
}
fn info(&self) -> &'static FuzzTestInfo {
unimplemented!()
}
fn domains(&self) -> Arc<Mutex<dyn GenericDomain>> {
unimplemented!()
}
}
#[googletest::test]
fn test_full_test_name_strips_crate_prefix() {
let info = FuzzTestInfo {
name: "unescaping_never_panics",
module_path: "escaping_test::__fuzztest_mod__unescaping_never_panics",
file: "third_party/googlefuzztest/rust/codelab/escaping_test.rs",
line: 72,
column: 1,
};
expect_that!(
info.full_test_name(),
eq("__fuzztest_mod__unescaping_never_panics::unescaping_never_panics")
);
}
#[googletest::test]
fn test_format_finding_report_matches_expected_format() {
let info = FuzzTestInfo {
name: "TEST_NAME",
module_path: "crate_name::SUITE_NAME",
file: "FILE",
line: 123,
column: 1,
};
let args =
vec![format_debug_arg(format!("{:?}", 17)), format_debug_arg(format!("{:?}", "ABC"))];
let report = format_finding_report(&info, &args);
expect_that!(
report,
eq(concat!(
"\n=================================================================\n",
"=== BUG FOUND!\n\n",
"FILE:123: Counterexample found for SUITE_NAME::TEST_NAME.\n",
"The test fails with input:\n",
"argument 0: 17\n",
"argument 1: \"ABC\"\n\n",
"=================================================================\n",
))
);
}
#[googletest::test]
fn test_format_debug_arg_truncates_long_values_at_char_boundary() {
let long_str = "é".repeat(1500);
let formatted = format_debug_arg(long_str);
expect_that!(formatted, ends_with(" ...<value too long>"));
expect_that!(formatted.len(), le(VALUE_MAX_PRINT_LENGTH + " ...<value too long>".len()));
}
#[googletest::test]
fn test_try_print_finding_report_prints_only_once_per_execution() {
let fuzz_test = CountingFuzzTest { print_count: AtomicUsize::new(0) };
let input: GenericCorpusValue = Box::new(42i32);
// Outside `with_finding_report_context`, nothing should be printed.
expect_that!(try_print_finding_report(), eq(false));
expect_that!(fuzz_test.print_count.load(Ordering::Relaxed), eq(0));
with_finding_report_context(&fuzz_test, &input, || {
expect_that!(try_print_finding_report(), eq(true));
expect_that!(try_print_finding_report(), eq(false));
expect_that!(fuzz_test.print_count.load(Ordering::Relaxed), eq(1));
});
// After `with_finding_report_context` returns, the context is cleared.
expect_that!(try_print_finding_report(), eq(false));
// Entering a new execution resets `report_printed`.
with_finding_report_context(&fuzz_test, &input, || {
expect_that!(try_print_finding_report(), eq(true));
expect_that!(fuzz_test.print_count.load(Ordering::Relaxed), eq(2));
});
}
#[googletest::test]
fn test_with_finding_report_context_is_isolated_per_thread() {
let fuzz_test = CountingFuzzTest { print_count: AtomicUsize::new(0) };
let input: GenericCorpusValue = Box::new(42i32);
with_finding_report_context(&fuzz_test, &input, || {
// Entering and exiting `with_finding_report_context` on another thread must not
// clobber or clear the current thread's `FINDING_REPORT_CONTEXT`.
std::thread::spawn(|| {
let other_test = CountingFuzzTest { print_count: AtomicUsize::new(0) };
let other_input: GenericCorpusValue = Box::new(99i32);
with_finding_report_context(&other_test, &other_input, || {});
})
.join()
.unwrap();
expect_that!(try_print_finding_report(), eq(true));
expect_that!(fuzz_test.print_count.load(Ordering::Relaxed), eq(1));
});
}
}