FuzzTest Rust: Switch sanitizer crash reporting to ReportErrorSummary hook.

Instead of relying on `__sanitizer_set_death_callback` and ASan-specific
`__asan_get_report_description`, override `__sanitizer_report_error_summary`
to capture crash summaries across all sanitizers (ASan, MSan, TSan, UBSan, LSan,
etc.), matching the C++ FuzzTest runtime approach.

Move the `__sanitizer_report_error_summary` override into `sanitizer_interface`
and expose an `extern "C"` callback registration entry point
(`FuzzTestSetSanitizerErrorSummaryCallback`) so that both C++ and Rust share the
exact same sanitizer hook and crash-type parser.

This fixes missing crash reports when sanitizers abort without triggering death
callbacks (such as MSan nested bugs), extracts detailed crash descriptions for
all sanitizers, and removes the unstable `#![feature(cfg_sanitize)]` requirement
from the `fuzztest` library crate for OSS Cargo builds.

PiperOrigin-RevId: 986015608
diff --git a/centipede/BUILD b/centipede/BUILD
index 4440a86..6279af0 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -2053,5 +2053,6 @@
         ":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 218a14b..1219ada 100644
--- a/fuzztest/internal/BUILD
+++ b/fuzztest/internal/BUILD
@@ -446,11 +446,21 @@
     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 @@
         ":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 5c1173e..1ccc8a3 100644
--- a/fuzztest/internal/CMakeLists.txt
+++ b/fuzztest/internal/CMakeLists.txt
@@ -425,6 +425,8 @@
   SRCS
     "sanitizer_interface.cc"
   DEPS
+    fuzztest::common_logging
+    absl::core_headers
     absl::status
     absl::statusor
     absl::strings
@@ -439,6 +441,7 @@
     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 a70ddb8..2eb4f28 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 <sanitizer/common_interface_defs.h>
-
-#include "./fuzztest/internal/sanitizer_interface.h"
 #endif
 
 #ifndef TRAP_PERF
@@ -164,20 +163,6 @@
          "\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<std::string> 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 (*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 4af138d..17dfdd7 100644
--- a/fuzztest/internal/sanitizer_interface.cc
+++ b/fuzztest/internal/sanitizer_interface.cc
@@ -14,9 +14,9 @@
 
 #include "./fuzztest/internal/sanitizer_interface.h"
 
+#include <atomic>
 #include <cstddef>
 #include <optional>
-#include <string>
 
 #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<FuzzTestSanitizerErrorSummaryCallback>
+    sanitizer_error_summary_callback{nullptr};
+
 namespace {
 
-std::optional<std::string> MaybeExtractTsanCrashType(
+std::optional<absl::string_view> MaybeExtractTsanCrashType(
     absl::string_view sanitizer_name, absl::string_view error_summary) {
   if (sanitizer_name != "ThreadSanitizer") return std::nullopt;
 
@@ -75,7 +80,7 @@
 
 }  // namespace
 
-absl::StatusOr<std::string> ParseCrashTypeFromSanitizerSummary(
+absl::StatusOr<absl::string_view> ParseCrashTypeFromSanitizerSummary(
     absl::string_view error_summary) {
   if (!absl::ConsumePrefix(&error_summary, "SUMMARY: ")) {
     return absl::InvalidArgumentError(absl::StrCat(
@@ -94,13 +99,41 @@
   if (error_summary.find("byte(s) leaked") != error_summary.npos) {
     return "memory-leak";
   }
-  if (auto tsan_crash_type =
+  if (std::optional<absl::string_view> 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<absl::string_view> 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 ff4be6c..68423e7 100644
--- a/fuzztest/internal/sanitizer_interface.h
+++ b/fuzztest/internal/sanitizer_interface.h
@@ -15,18 +15,41 @@
 #ifndef FUZZTEST_FUZZTEST_INTERNAL_SANITIZER_INTERFACE_H_
 #define FUZZTEST_FUZZTEST_INTERNAL_SANITIZER_INTERFACE_H_
 
-#include <string>
+#include <cstddef>
 
+#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` is
+// non-null and non-empty (`crash_type_size > 0`), points either to a static
+// string literal or into the `error_summary` buffer passed by the sanitizer
+// runtime, and is valid for reads 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<std::string> 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<absl::string_view> 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 0cf5c22..f64edbd 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 <string>
+#include <cstddef>
 
 #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 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeWhenItIsTheOnlyToken) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: SomeSanitizer: some-crash-type");
   ASSERT_TRUE(crash_type.ok());
@@ -37,7 +38,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeWhenFilePathIsPresent) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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, ParsesMemoryLeak) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: AddressSanitizer: 10 byte(s) leaked in 10 allocation(s)");
   ASSERT_TRUE(crash_type.ok());
@@ -53,7 +54,7 @@
 }
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForUBSan) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: UndefinedBehaviorSanitizer: null-pointer-use "
           "some/file.h:32:7");
@@ -62,7 +63,7 @@
 }
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForMSan) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: MemorySanitizer: use-of-uninitialized-value "
           "some/file.cc:570:11 in SomeFunction");
@@ -72,7 +73,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanDataRaceOnVptr) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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, ExtractsCrashTypeForTSanDataRace) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: data race "
           "some/file.cc:33:37 in operator()");
@@ -91,7 +92,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanDestroyLocked) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: destroy of a locked mutex "
           "some/file.cc:12:34 in Foo");
@@ -101,7 +102,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanDoubleLock) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: double lock of a mutex some/file.cc:12:34 "
           "in Foo");
@@ -110,7 +111,7 @@
 }
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest, ExtractsCrashTypeForTSanDeadlock) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: lock-order-inversion (potential "
           "deadlock) some/file.cc:12:34 in Foo");
@@ -120,7 +121,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanMutexHeldWrongContext) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: mutex held in the wrong context "
           "some/file.cc:12:34 in Foo");
@@ -130,7 +131,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanExternalRace) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: race on external object "
           "some/file.cc:12:34 "
@@ -141,7 +142,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanBadReadLock) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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,
      ExtractsCrashTypeForTSanBadReadUnlock) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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,
      ExtractsCrashTypeForTSanErrnoInSignal) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: signal handler spoils errno "
           "some/file.cc:12:34 in Foo");
@@ -171,7 +172,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanSignalUnsafe) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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,
      ExtractsCrashTypeForTSanSwiftAccessRace) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: Swift access race some/file.cc:12:34 in "
           "Foo");
@@ -191,7 +192,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanThreadLeak) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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,
      ExtractsCrashTypeForTSanBadUnlock) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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,
      ExtractsCrashTypeForTSanInvalidMutex) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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,
      ExtractsCrashTypeForTSanHeapUseAfterFree) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: heap-use-after-free some/file.cc:12:34 "
           "in Foo");
@@ -230,7 +231,7 @@
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest,
      ExtractsCrashTypeForTSanFallbackSingleToken) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary(
           "SUMMARY: ThreadSanitizer: unknown-crash-type some/file.cc:12:34 "
           "in Foo");
@@ -239,7 +240,7 @@
 }
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest, IgnoresTsanCrashTypeForNonTSan) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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, FailsOnMissingSummaryPrefix) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> crash_type =
       ParseCrashTypeFromSanitizerSummary("Missing SUMMARY prefix");
   ASSERT_FALSE(crash_type.ok());
   EXPECT_THAT(crash_type.status().message(),
@@ -255,11 +256,45 @@
 }
 
 TEST(ParseCrashTypeFromSanitizerSummaryTest, FailsOnMissingSanitizerName) {
-  const absl::StatusOr<std::string> crash_type =
+  const absl::StatusOr<absl::string_view> 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 1377f83..03d3300 100644
--- a/rust/BUILD
+++ b/rust/BUILD
@@ -31,6 +31,7 @@
         "@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 5ba278b..16340e4 100644
--- a/rust/e2e_tests/test_utils.rs
+++ b/rust/e2e_tests/test_utils.rs
@@ -78,3 +78,24 @@
 
     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 6ce859e..00a1612 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 @@
 
     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 @@
         &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 db3ede0..335f82a 100644
--- a/rust/src/crash_handler.rs
+++ b/rust/src/crash_handler.rs
@@ -12,41 +12,59 @@
 // 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<extern "C" fn()>);
-
-        pub safe fn __asan_get_report_description() -> *const c_char;
-    }
-
-    pub extern "C" fn sanitizer_death_callback() {
-        use crate::worker;
-
-        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);
-    }
+// SAFETY:
+// - `FuzzTestSetSanitizerErrorSummaryCallback` is declared with `extern "C"` linkage in
+//   `internal/sanitizer_interface.h` and defined in `internal/sanitizer_interface.cc`.
+// - The signature matches `void FuzzTestSetSanitizerErrorSummaryCallback(void (*)(const char*, size_t))`:
+//   `*const u8` is layout- and ABI-compatible with `const char*`, and `usize` is ABI-compatible
+//   with `size_t`.
+unsafe extern "C" {
+    fn FuzzTestSetSanitizerErrorSummaryCallback(
+        callback: unsafe extern "C" fn(crash_type_data: *const u8, crash_type_size: usize),
+    );
 }
 
-/// Be able to emit failures before exiting fully from the process for non-unwinding panics and/or
-/// unrecoverable crashes.
+/// Sanitizer error summary callback invoked by the C++ sanitizer interface.
+///
+/// # Safety
+///
+/// The caller must uphold the following preconditions:
+/// - If `crash_type_size > 0`, `crash_type_data` must be non-null and valid for reads of
+///   `crash_type_size` consecutive, initialized `u8` bytes for the duration of the call.
+/// - The pointed-to memory must not be mutated concurrently for the duration of the call.
+/// - `crash_type_size` must not exceed `isize::MAX`.
+unsafe extern "C" fn sanitizer_error_summary_callback(
+    crash_type_data: *const u8,
+    crash_type_size: usize,
+) {
+    let crash_type = if crash_type_data.is_null() || crash_type_size == 0 {
+        "Sanitizer crash"
+    } else {
+        // SAFETY:
+        // - Non-nullness: `crash_type_data` was verified non-null above.
+        // - Alignment: `u8` has alignment 1, so any non-null pointer is properly aligned.
+        // - Validity: The caller guarantees `crash_type_data` points to `crash_type_size`
+        //   consecutive, initialized `u8` bytes valid for reads for the duration of this call.
+        // - Aliasing: The pointed-to memory is read-only and not mutated during the call.
+        // - Size: The caller guarantees `crash_type_size <= isize::MAX`.
+        let crash_type_bytes =
+            unsafe { std::slice::from_raw_parts(crash_type_data, crash_type_size) };
+        std::str::from_utf8(crash_type_bytes).unwrap_or("Sanitizer crash (invalid utf8)")
+    };
+    crate::worker::try_emit_finding(crash_type, crash_type);
+}
+
+/// 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 an `unsafe extern "C" fn` matching the C callback
+    //   signature `FuzzTestSanitizerErrorSummaryCallback` (`void (*)(const char*, size_t)`).
+    // - As a function item, `sanitizer_error_summary_callback` has `'static` lifetime and remains
+    //   valid for the entire duration of program execution.
+    // - `FuzzTestSetSanitizerErrorSummaryCallback` stores the function pointer in a `std::atomic`
+    //   using `memory_order_relaxed`, so concurrent registration is data-race free.
+    unsafe {
+        FuzzTestSetSanitizerErrorSummaryCallback(sanitizer_error_summary_callback);
     }
 }
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index bc663ef..28259df 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 8eae57f..e19f21f 100644
--- a/rust/src/worker.rs
+++ b/rust/src/worker.rs
@@ -33,8 +33,8 @@
 
 /// 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<Option<DiagnosticSink>> = Mutex::new(None);
 
@@ -45,7 +45,7 @@
 ///
 /// 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<Option<ExecuteContext>> = Mutex::new(None);
 
@@ -350,7 +350,7 @@
 
     // 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);