diff --git a/centipede/BUILD b/centipede/BUILD index 036f7e2d5..12e133a51 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -2040,5 +2040,6 @@ cc_static_library( ":engine_worker", ":sancov_runtime", ":weak_sancov_stubs", + "@com_google_fuzztest//fuzztest/internal:sanitizer_interface", ], ) diff --git a/fuzztest/internal/BUILD b/fuzztest/internal/BUILD index 218a14b39..1219adad1 100644 --- a/fuzztest/internal/BUILD +++ b/fuzztest/internal/BUILD @@ -446,11 +446,21 @@ cc_library( name = "sanitizer_interface", srcs = ["sanitizer_interface.cc"], hdrs = ["sanitizer_interface.h"], + # Link statically and unconditionally into the main executable so the + # strong sanitizer error summary hook overrides the default weak definition + # in the sanitizer runtime (which is also linked statically into the + # executable). + linkstatic = True, deps = [ + "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", - ], + "@com_google_fuzztest//common:logging", + ] + select({ + "//conditions:default": [], + }), + alwayslink = True, ) cc_test( @@ -460,6 +470,7 @@ cc_test( ":sanitizer_interface", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings:string_view", "@googletest//:gtest_main", ], ) diff --git a/fuzztest/internal/CMakeLists.txt b/fuzztest/internal/CMakeLists.txt index 5c1173e69..1ccc8a326 100644 --- a/fuzztest/internal/CMakeLists.txt +++ b/fuzztest/internal/CMakeLists.txt @@ -425,6 +425,8 @@ fuzztest_cc_library( SRCS "sanitizer_interface.cc" DEPS + fuzztest::common_logging + absl::core_headers absl::status absl::statusor absl::strings @@ -439,6 +441,7 @@ fuzztest_cc_test( fuzztest::sanitizer_interface absl::status absl::statusor + absl::string_view GTest::gmock_main ) diff --git a/fuzztest/internal/runtime.cc b/fuzztest/internal/runtime.cc index a70ddb8b8..2eb4f28a5 100644 --- a/fuzztest/internal/runtime.cc +++ b/fuzztest/internal/runtime.cc @@ -68,6 +68,7 @@ #include "./fuzztest/internal/io.h" #include "./fuzztest/internal/logging.h" #include "./fuzztest/internal/printer.h" +#include "./fuzztest/internal/sanitizer_interface.h" #include "./fuzztest/internal/serialization.h" #include "./fuzztest/internal/status.h" @@ -75,8 +76,6 @@ defined(THREAD_SANITIZER) #define FUZZTEST_HAS_SANITIZER #include - -#include "./fuzztest/internal/sanitizer_interface.h" #endif #ifndef TRAP_PERF @@ -164,20 +163,6 @@ absl::string_view GetSeparator() { "\n"; } -#if defined(FUZZTEST_HAS_SANITIZER) -// clang-format off -extern "C" void __attribute__((visibility("default"))) -__sanitizer_report_error_summary(const char* error_summary) { - // clang-format on - absl::StatusOr crash_type = - ParseCrashTypeFromSanitizerSummary(error_summary); - FUZZTEST_LOG_IF(ERROR, !crash_type.ok()) - << "Failed to extract sanitizer crash type: " << crash_type.status(); - Runtime::instance().SetCrashTypeIfUnset( - std::move(crash_type).value_or("Sanitizer crash")); -} -#endif - } // namespace ReproducerOutputLocation GetReproducerOutputLocation() { @@ -246,6 +231,11 @@ void PrintReproducerIfRequested(RawSink out, const FuzzTest& test, void (*crash_handler_hook)(); Runtime::Runtime() { + FuzzTestSetSanitizerErrorSummaryCallback( + [](const char* crash_type_data, size_t crash_type_size) { + Runtime::instance().SetCrashTypeIfUnset( + std::string(crash_type_data, crash_type_size)); + }); if (const char* crash_metadata_path = std::getenv("FUZZTEST_CRASH_METADATA_PATH"); crash_metadata_path != nullptr) { diff --git a/fuzztest/internal/sanitizer_interface.cc b/fuzztest/internal/sanitizer_interface.cc index 4af138d7b..17dfdd77d 100644 --- a/fuzztest/internal/sanitizer_interface.cc +++ b/fuzztest/internal/sanitizer_interface.cc @@ -14,9 +14,9 @@ #include "./fuzztest/internal/sanitizer_interface.h" +#include #include #include -#include #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -24,11 +24,16 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/strings/strip.h" +#include "./common/logging.h" namespace fuzztest::internal { + +std::atomic + sanitizer_error_summary_callback{nullptr}; + namespace { -std::optional MaybeExtractTsanCrashType( +std::optional MaybeExtractTsanCrashType( absl::string_view sanitizer_name, absl::string_view error_summary) { if (sanitizer_name != "ThreadSanitizer") return std::nullopt; @@ -75,7 +80,7 @@ std::optional MaybeExtractTsanCrashType( } // namespace -absl::StatusOr ParseCrashTypeFromSanitizerSummary( +absl::StatusOr ParseCrashTypeFromSanitizerSummary( absl::string_view error_summary) { if (!absl::ConsumePrefix(&error_summary, "SUMMARY: ")) { return absl::InvalidArgumentError(absl::StrCat( @@ -94,13 +99,41 @@ absl::StatusOr ParseCrashTypeFromSanitizerSummary( if (error_summary.find("byte(s) leaked") != error_summary.npos) { return "memory-leak"; } - if (auto tsan_crash_type = + if (std::optional tsan_crash_type = MaybeExtractTsanCrashType(sanitizer_name, error_summary); tsan_crash_type.has_value()) { return *tsan_crash_type; } const size_t space_pos = error_summary.find(' '); - return std::string(error_summary.substr(0, space_pos)); + return error_summary.substr(0, space_pos); } } // namespace fuzztest::internal + +// clang-format off +extern "C" void __attribute__((visibility("default"), used)) +__sanitizer_report_error_summary(const char* error_summary) { + const FuzzTestSanitizerErrorSummaryCallback callback = + fuzztest::internal::sanitizer_error_summary_callback.load( + std::memory_order_relaxed); + if (callback == nullptr) return; + absl::StatusOr crash_type = + fuzztest::internal::ParseCrashTypeFromSanitizerSummary( + absl::NullSafeStringView(error_summary)); + FUZZTEST_LOG_IF(ERROR, !crash_type.ok()) + << "Failed to extract sanitizer crash type: " << crash_type.status(); + const absl::string_view resolved_crash_type = + crash_type.value_or("Sanitizer crash"); + callback(resolved_crash_type.data(), resolved_crash_type.size()); +} +// clang-format on + +extern "C" void FuzzTestSetSanitizerErrorSummaryCallback( + FuzzTestSanitizerErrorSummaryCallback callback) { + // Ensure the sanitizer error summary hook is retained by the linker (e.g., + // under -Wl,--gc-sections) whenever a callback is registered. + void (*volatile hook)(const char*) = &__sanitizer_report_error_summary; + (void)hook; + fuzztest::internal::sanitizer_error_summary_callback.store( + callback, std::memory_order_relaxed); +} diff --git a/fuzztest/internal/sanitizer_interface.h b/fuzztest/internal/sanitizer_interface.h index ff4be6c49..2735b5d80 100644 --- a/fuzztest/internal/sanitizer_interface.h +++ b/fuzztest/internal/sanitizer_interface.h @@ -15,18 +15,40 @@ #ifndef FUZZTEST_FUZZTEST_INTERNAL_SANITIZER_INTERFACE_H_ #define FUZZTEST_FUZZTEST_INTERNAL_SANITIZER_INTERFACE_H_ -#include +#include +#include "absl/base/attributes.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" +extern "C" { + +using FuzzTestSanitizerErrorSummaryCallback = + void (*)(const char* crash_type_data, size_t crash_type_size); + +// Registers a callback to be invoked with the parsed crash type whenever the +// sanitizer runtime reports an error summary. +// +// The `(crash_type_data, crash_type_size)` slice passed to `callback` points +// either to a static string literal or into the `error_summary` buffer passed +// by the sanitizer runtime, and is valid for the duration of the callback +// invocation (or longer if the input `error_summary` outlives the call). +void FuzzTestSetSanitizerErrorSummaryCallback( + FuzzTestSanitizerErrorSummaryCallback callback); + +} // extern "C" + namespace fuzztest::internal { // Parses the crash type from the sanitizer error summary. // The summary is expected to be in the format: // "SUMMARY: SomeSanitizer: some-crash-type ..." -absl::StatusOr ParseCrashTypeFromSanitizerSummary( - absl::string_view error_summary); +// +// The returned `absl::string_view` points either to a static string literal or +// into `error_summary`, and remains valid for as long as `error_summary` is +// valid. +absl::StatusOr ParseCrashTypeFromSanitizerSummary( + absl::string_view error_summary ABSL_ATTRIBUTE_LIFETIME_BOUND); } // namespace fuzztest::internal diff --git a/fuzztest/internal/sanitizer_interface_test.cc b/fuzztest/internal/sanitizer_interface_test.cc index 0cf5c22de..f64edbd2a 100644 --- a/fuzztest/internal/sanitizer_interface_test.cc +++ b/fuzztest/internal/sanitizer_interface_test.cc @@ -14,12 +14,13 @@ #include "./fuzztest/internal/sanitizer_interface.h" -#include +#include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/string_view.h" namespace fuzztest::internal { namespace { @@ -28,7 +29,7 @@ using ::testing::HasSubstr; TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeWhenItIsTheOnlyToken) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: SomeSanitizer: some-crash-type"); ASSERT_TRUE(crash_type.ok()); @@ -37,7 +38,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeWhenFilePathIsPresent) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: AddressSanitizer: heap-use-after-free some/file.cc:1234:5"); ASSERT_TRUE(crash_type.ok()); @@ -45,7 +46,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, } TEST(ParseCrashTypeFromSanitizerSummaryTest, ParsesMemoryLeak) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: AddressSanitizer: 10 byte(s) leaked in 10 allocation(s)"); ASSERT_TRUE(crash_type.ok()); @@ -53,7 +54,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, ParsesMemoryLeak) { } TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForUBSan) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: UndefinedBehaviorSanitizer: null-pointer-use " "some/file.h:32:7"); @@ -62,7 +63,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForUBSan) { } TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForMSan) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: MemorySanitizer: use-of-uninitialized-value " "some/file.cc:570:11 in SomeFunction"); @@ -72,7 +73,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForMSan) { TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDataRaceOnVptr) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual " "call) some/file.cc:12:34 in Foo"); @@ -81,7 +82,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, } TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDataRace) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: data race " "some/file.cc:33:37 in operator()"); @@ -91,7 +92,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDataRace) { TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDestroyLocked) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: destroy of a locked mutex " "some/file.cc:12:34 in Foo"); @@ -101,7 +102,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDoubleLock) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: double lock of a mutex some/file.cc:12:34 " "in Foo"); @@ -110,7 +111,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, } TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDeadlock) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: lock-order-inversion (potential " "deadlock) some/file.cc:12:34 in Foo"); @@ -120,7 +121,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDeadlock) { TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanMutexHeldWrongContext) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: mutex held in the wrong context " "some/file.cc:12:34 in Foo"); @@ -130,7 +131,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanExternalRace) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: race on external object " "some/file.cc:12:34 " @@ -141,7 +142,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanBadReadLock) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: read lock of a write locked mutex " "some/file.cc:12:34 in Foo"); @@ -151,7 +152,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanBadReadUnlock) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: read unlock of a write locked mutex " "some/file.cc:12:34 in Foo"); @@ -161,7 +162,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanErrnoInSignal) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: signal handler spoils errno " "some/file.cc:12:34 in Foo"); @@ -171,7 +172,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanSignalUnsafe) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: signal-unsafe call inside of a signal " "some/file.cc:12:34 in Foo"); @@ -181,7 +182,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanSwiftAccessRace) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: Swift access race some/file.cc:12:34 in " "Foo"); @@ -191,7 +192,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanThreadLeak) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: thread leak some/file.cc:12:34 in Foo"); ASSERT_TRUE(crash_type.ok()); @@ -200,7 +201,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanBadUnlock) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: unlock of an unlocked mutex (or by a " "wrong thread) some/file.cc:12:34 in Foo"); @@ -210,7 +211,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanInvalidMutex) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: use of an invalid mutex (e.g. " "uninitialized or destroyed) some/file.cc:12:34 in Foo"); @@ -220,7 +221,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanHeapUseAfterFree) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: heap-use-after-free some/file.cc:12:34 " "in Foo"); @@ -230,7 +231,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanFallbackSingleToken) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: ThreadSanitizer: unknown-crash-type some/file.cc:12:34 " "in Foo"); @@ -239,7 +240,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, } TEST(ParseCrashTypeFromSanitizerSummaryTest, IgnoresTsanCrashTypeForNonTSan) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary( "SUMMARY: AddressSanitizer: data race some/file.cc:12:34 in Foo"); ASSERT_TRUE(crash_type.ok()); @@ -247,7 +248,7 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, IgnoresTsanCrashTypeForNonTSan) { } TEST(ParseCrashTypeFromSanitizerSummaryTest, FailsOnMissingSummaryPrefix) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary("Missing SUMMARY prefix"); ASSERT_FALSE(crash_type.ok()); EXPECT_THAT(crash_type.status().message(), @@ -255,11 +256,45 @@ TEST(ParseCrashTypeFromSanitizerSummaryTest, FailsOnMissingSummaryPrefix) { } TEST(ParseCrashTypeFromSanitizerSummaryTest, FailsOnMissingSanitizerName) { - const absl::StatusOr crash_type = + const absl::StatusOr crash_type = ParseCrashTypeFromSanitizerSummary("SUMMARY: No sanitizer name"); ASSERT_FALSE(crash_type.ok()); EXPECT_THAT(crash_type.status().message(), HasSubstr("No sanitizer name")); } +extern "C" void __sanitizer_report_error_summary(const char* error_summary); + +void InvokeSanitizerReportErrorSummary(const char* error_summary) { + __sanitizer_report_error_summary(error_summary); +} + +TEST(FuzzTestSanitizerErrorSummaryCallbackTest, + InvokesCallbackWithParsedOrFallbackCrashType) { + static absl::string_view recorded_crash_type; + recorded_crash_type = "unset"; + + FuzzTestSetSanitizerErrorSummaryCallback(nullptr); + InvokeSanitizerReportErrorSummary( + "SUMMARY: AddressSanitizer: heap-use-after-free some/file.cc:12:3"); + EXPECT_EQ(recorded_crash_type, "unset"); + + FuzzTestSetSanitizerErrorSummaryCallback([](const char* crash_type_data, + size_t crash_type_size) { + recorded_crash_type = absl::string_view(crash_type_data, crash_type_size); + }); + + InvokeSanitizerReportErrorSummary( + "SUMMARY: AddressSanitizer: heap-use-after-free some/file.cc:12:3"); + EXPECT_EQ(recorded_crash_type, "heap-use-after-free"); + + InvokeSanitizerReportErrorSummary(nullptr); + EXPECT_EQ(recorded_crash_type, "Sanitizer crash"); + + InvokeSanitizerReportErrorSummary("Invalid summary"); + EXPECT_EQ(recorded_crash_type, "Sanitizer crash"); + + FuzzTestSetSanitizerErrorSummaryCallback(nullptr); +} + } // namespace } // namespace fuzztest::internal diff --git a/rust/BUILD b/rust/BUILD index 1377f83ea..03d330011 100644 --- a/rust/BUILD +++ b/rust/BUILD @@ -31,6 +31,7 @@ rust_library( "@com_google_fuzztest//rust:__subpackages__", ], deps = [ + "@com_google_fuzztest//fuzztest/internal:sanitizer_interface", "@com_google_fuzztest//rust/coverage", "@com_google_fuzztest//rust/engine", "@com_google_fuzztest//rust/options:fuzztest_options", diff --git a/rust/e2e_tests/test_utils.rs b/rust/e2e_tests/test_utils.rs index 5ba278b63..16340e4af 100644 --- a/rust/e2e_tests/test_utils.rs +++ b/rust/e2e_tests/test_utils.rs @@ -78,3 +78,24 @@ pub fn run_centipede_with_args_expect_termination(fixture: &EnvVars, args: &[&st String::from_utf8_lossy(&process.stderr).to_string() } + +/// Returns stderr of the target binary with `args` and `envs` that is expected to terminate. +/// +/// In addition to `envs`, the function will also pass `FUZZTEST_CENTIPEDE_BINARY_PATH`, +/// `FUZZTEST_PRINT_SUBPROCESS_LOG=true`, and `RUST_TEST_NOCAPTURE=1`. +pub fn run_target_binary_with_args_and_env_expect_termination( + fixture: &EnvVars, + args: &[&str], + envs: &[(&str, &str)], +) -> String { + let process = Command::new(&fixture.target_binary_path) + .args(args) + .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") + .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) + .env("RUST_TEST_NOCAPTURE", "1") + .envs(envs.iter().copied()) + .output() + .expect("Target binary should have executed"); + + String::from_utf8_lossy(&process.stderr).to_string() +} diff --git a/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs b/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs index 6ce859e92..00a1612b6 100644 --- a/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs +++ b/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs @@ -38,10 +38,19 @@ fn ensure_use_after_free_signature_with_asan(fixture: &EnvVars) { let stderr = test_utils::run_centipede_with_args_expect_termination(fixture, &args); - expect_that!( - stderr, - matchers::contains_substring("Property function ran but address sanitizer caught a bug") - ); + expect_that!(stderr, matchers::contains_regex("Failure[ \t]*: heap-use-after-free")); +} + +#[gtest] +#[cfg(sanitize = "address")] +fn standalone_mode_reports_use_after_free_with_asan(fixture: &EnvVars) { + let args = ["__fuzztest_mod__use_after_free_asan_death_test::use_after_free_asan_death_test"]; + let envs = [("FUZZTEST_FUZZ_FOR", "15s")]; + + let stderr = + test_utils::run_target_binary_with_args_and_env_expect_termination(fixture, &args, &envs); + + expect_that!(stderr, matchers::contains_regex("Failure[ \t]*: heap-use-after-free")); } // TODO(yamilmorales): Enable this test on presubmit with --config=msan. @@ -56,14 +65,21 @@ fn ensure_sanitizer_crash_signature_with_msan(fixture: &EnvVars) { &format!("--workdir={}", work_dir.display()), "--exit_on_crash", "--test_name=__fuzztest_mod__msan_death_test.msan_death_test", - "--use_cmp_features=0", // Prevent msan from detecting nested bugs and aborting without - // triggering the death callback. ]; let stderr = test_utils::run_centipede_with_args_expect_termination(fixture, &args); - expect_that!( - stderr, - matchers::contains_substring("Property function ran but a sanitizer caught a bug") - ); + expect_that!(stderr, matchers::contains_regex("Failure[ \t]*: use-of-uninitialized-value")); +} + +#[gtest] +#[cfg(sanitize = "memory")] +fn standalone_mode_reports_uninitialized_value_with_msan(fixture: &EnvVars) { + let args = ["__fuzztest_mod__msan_death_test::msan_death_test"]; + let envs = [("FUZZTEST_FUZZ_FOR", "15s")]; + + let stderr = + test_utils::run_target_binary_with_args_and_env_expect_termination(fixture, &args, &envs); + + expect_that!(stderr, matchers::contains_regex("Failure[ \t]*: use-of-uninitialized-value")); } diff --git a/rust/src/crash_handler.rs b/rust/src/crash_handler.rs index db3ede096..dbe74af42 100644 --- a/rust/src/crash_handler.rs +++ b/rust/src/crash_handler.rs @@ -12,41 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(any(sanitize = "address", sanitize = "memory"))] -mod callbacks { - use std::ffi::{c_char, CStr}; - - unsafe extern "C" { - pub safe fn __sanitizer_set_death_callback(callback: Option); - - pub safe fn __asan_get_report_description() -> *const c_char; - } - - pub extern "C" fn sanitizer_death_callback() { - use crate::worker; +unsafe extern "C" { + fn FuzzTestSetSanitizerErrorSummaryCallback( + callback: unsafe extern "C" fn(crash_type_data: *const u8, crash_type_size: usize), + ); +} - let (description, signature) = if cfg!(sanitize = "address") { - let char_ptr = __asan_get_report_description(); - // Safety: `ptr` points to a valid null terminated string. - let signature_cstr = unsafe { CStr::from_ptr(char_ptr) }; - ( - "Property function ran but address sanitizer caught a bug", - signature_cstr.to_str().unwrap_or("ASan crash"), - ) - } else { - ("Property function ran but a sanitizer caught a bug", "Sanitizer crash") - }; - worker::try_emit_finding(description, signature); - } +unsafe extern "C" fn sanitizer_error_summary_callback( + crash_type_data: *const u8, + crash_type_size: usize, +) { + // SAFETY: `FuzzTestSetSanitizerErrorSummaryCallback` guarantees `crash_type_data` + // and `crash_type_size` form a valid ASCII byte slice for the duration of the callback. + let crash_type_bytes = unsafe { std::slice::from_raw_parts(crash_type_data, crash_type_size) }; + let crash_type = std::str::from_utf8(crash_type_bytes).unwrap_or("Sanitizer crash"); + crate::worker::try_emit_finding(crash_type, crash_type); } -/// Be able to emit failures before exiting fully from the process for non-unwinding panics and/or -/// unrecoverable crashes. +/// Registers the sanitizer error summary callback and ensures the sanitizer crash handler hook is +/// linked into the binary. pub fn register_crash_handler() { - // TODO(yamilmorales): Consider allowing more sanitizers here, and find some other way to - // recognize sanitizers if this feature is not stabilized by the time we need to support Cargo. - #[cfg(any(sanitize = "address", sanitize = "memory"))] - { - callbacks::__sanitizer_set_death_callback(Some(callbacks::sanitizer_death_callback)); + // SAFETY: `sanitizer_error_summary_callback` is a valid function pointer with C ABI. + unsafe { + FuzzTestSetSanitizerErrorSummaryCallback(sanitizer_error_summary_callback); } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index bc663ef5b..28259dfb6 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -14,7 +14,6 @@ #![deny(clippy::absolute_paths)] #![deny(unused_imports)] -#![feature(cfg_sanitize)] mod crash_handler; pub mod domains; diff --git a/rust/src/worker.rs b/rust/src/worker.rs index 8eae57fa8..e19f21f6c 100644 --- a/rust/src/worker.rs +++ b/rust/src/worker.rs @@ -33,8 +33,8 @@ use std::time::Instant; /// The DiagnosticSink provided by the engine while creating the adapter. /// -/// Storing the `DiagnosticSink` in this global is necessary because C death callbacks (e.g., -/// sanitizer traps in `crash_handler.rs`) do not have access to the adapter instance and must rely +/// Storing the `DiagnosticSink` in this global is necessary because C callbacks (e.g., +/// sanitizer hooks in `crash_handler.rs`) do not have access to the adapter instance and must rely /// on a global lookup to report crashes. static DIAGNOSTIC_SINK: Mutex> = Mutex::new(None); @@ -45,7 +45,7 @@ static DIAGNOSTIC_SINK: Mutex> = Mutex::new(None); /// /// The inner ExecuteContext is used as a witness token to prove to the fuzzing engine that findings /// are being emitted during the execution of a test case. -/// A global is necessary because C death callbacks (e.g. sanitizer traps in `crash_handler.rs`) do +/// A global is necessary because C callbacks (e.g. sanitizer hooks in `crash_handler.rs`) do /// not have access to the adapter instance. static EXECUTE_CONTEXT: Mutex> = Mutex::new(None); @@ -350,7 +350,7 @@ pub unsafe extern "C" fn construct_adapter_callback( // NOTE: `safe_sink` is not passed to `construct_adapter` or stored in `RustFuzzTestAdapter`. // Instead, it is maintained in the global `DIAGNOSTIC_SINK` mutex via `set_diagnostic_sink`. - // This is necessary because C death callbacks (e.g., sanitizer traps in `crash_handler.rs`) do not + // This is necessary because C callbacks (e.g., sanitizer hooks in `crash_handler.rs`) do not // have access to the adapter `self` pointer and must rely on a global lookup to report crashes. let adapter = manager.construct_adapter(); let boxed_adapter = Box::new(adapter);