diff --git a/compiler-rt/lib/scudo/standalone/combined.h b/compiler-rt/lib/scudo/standalone/combined.h index 72b4b361475b2..b37ff23343946 100644 --- a/compiler-rt/lib/scudo/standalone/combined.h +++ b/compiler-rt/lib/scudo/standalone/combined.h @@ -988,6 +988,186 @@ class Allocator { : 0; } + void getErrorInfo(uptr FaultAddr, size_t MinDistance, size_t MaxDistance, + scudo_error_report *Reports, size_t &ReportIndex) { + auto *RingBuffer = getRingBuffer(); + if (RingBuffer == nullptr) + return; + + // No more room for any more error reports. + if (ReportIndex == NumErrorReports) + return; + + uptr UntaggedFaultAddr = untagPointer(FaultAddr); + BlockInfo Info = Primary.findNearestBlock(UntaggedFaultAddr); + + auto GetBlockInfo = [&](uptr Addr, Chunk::UnpackedHeader *Header, + uptr &ChunkAddr) { + uptr ChunkOffset = getChunkOffsetFromBlock( + reinterpret_cast(loadTagUnaligned(Addr))); + ChunkAddr = loadTagUnaligned(Addr + ChunkOffset); + uptr HeaderAddr = loadTag(Addr + ChunkOffset - Chunk::getHeaderSize()); + *Header = *reinterpret_cast(HeaderAddr); + }; + + auto CheckOOB = [&](uptr BlockAddr) { + if (BlockAddr < Info.RegionBegin || BlockAddr >= Info.RegionEnd) + return false; + + uptr ChunkAddr; + Chunk::UnpackedHeader Header; + GetBlockInfo(BlockAddr, &Header, ChunkAddr); + if (Header.State != Chunk::State::Allocated) + return false; + const u8 ChunkTag = extractTag(ChunkAddr); + if (extractTag(FaultAddr) != ChunkTag) { + // If the allocated size is zero, then there isn't a tag on this + // pointer, so allow a tag mismatch. + if (ChunkTag != 0 || Header.SizeOrUnusedBytes != 0) + return false; + } + auto *Report = &Reports[ReportIndex++]; + uptr UntaggedChunkAddr = untagPointer(ChunkAddr); + const u32 *ChunkData = reinterpret_cast(UntaggedChunkAddr); + Report->error_type = UntaggedFaultAddr < UntaggedChunkAddr + ? BUFFER_UNDERFLOW + : BUFFER_OVERFLOW; + Report->allocation_address = UntaggedChunkAddr; + Report->allocation_size = Header.SizeOrUnusedBytes; + if (RingBuffer->Depot) { + const u32 *TracePtr = reinterpret_cast(loadTagUnaligned( + reinterpret_cast(&ChunkData[MemTagAllocationTraceIndex]))); + collectTraceMaybe(RingBuffer->Depot, Report->allocation_trace, + *TracePtr); + } + const u32 *TidPtr = reinterpret_cast(loadTagUnaligned( + reinterpret_cast(&ChunkData[MemTagAllocationTidIndex]))); + Report->allocation_tid = *TidPtr; + return ReportIndex == NumErrorReports; + }; + + if (MinDistance == 0 && CheckOOB(Info.BlockBegin)) + return; + + for (size_t I = Max(MinDistance, 1); I != MaxDistance; ++I) + if (CheckOOB(Info.BlockBegin + I * Info.BlockSize) || + CheckOOB(Info.BlockBegin - I * Info.BlockSize)) + return; + } + + void getRingBufferErrorInfo(uintptr_t FaultAddr, scudo_error_report *Reports, + size_t &ReportIndex) { + auto *RingBuffer = getRingBuffer(); + if (RingBuffer == nullptr) + return; + + // No more room for any more error reports. + if (ReportIndex == NumErrorReports) + return; + + uptr Pos = atomic_load_relaxed(&RingBuffer->Pos); + if (Pos == 0) + return; + + const uptr RingBufferElements = RingBuffer->RingBufferElements; + + // Pos is a value that is always increasing. + uptr LastPos; + if (Pos < RingBufferElements) { + LastPos = 0; + } else { + LastPos = Pos - RingBufferElements; + } + for (uptr I = Pos; I > LastPos; --I) { + auto *Entry = + getRingBufferEntry(RingBuffer, (I - 1) % RingBufferElements); + uptr EntryPtr = atomic_load_relaxed(&Entry->Ptr); + if (!EntryPtr) + continue; + + uptr UntaggedEntryPtr = untagPointer(EntryPtr); + uptr EntrySize = atomic_load_relaxed(&Entry->AllocationSize); + u32 AllocationTrace = atomic_load_relaxed(&Entry->AllocationTrace); + u32 AllocationTid = atomic_load_relaxed(&Entry->AllocationTid); + u32 DeallocationTrace = atomic_load_relaxed(&Entry->DeallocationTrace); + u32 DeallocationTid = atomic_load_relaxed(&Entry->DeallocationTid); + if (DeallocationTid) { + // For UAF we only consider in-bounds fault addresses because + // out-of-bounds UAF is rare and attempting to detect it is very likely + // to result in false positives. + if (FaultAddr < EntryPtr || FaultAddr >= EntryPtr + EntrySize) + continue; + } else { + // Ring buffer OOB is only possible with secondary allocations. In this + // case we are guaranteed a guard region of at least a page on either + // side of the allocation (guard page on the right, guard page + tagged + // region on the left), so ignore any faults outside of that range. + if (FaultAddr < EntryPtr - getPageSizeCached() || + FaultAddr >= EntryPtr + EntrySize + getPageSizeCached()) + continue; + + // For UAF the ring buffer will contain two entries, one for the + // allocation and another for the deallocation. Don't report buffer + // overflow/underflow using the allocation entry if we have already + // collected a report from the deallocation entry. + bool Found = false; + for (uptr J = 0; J != ReportIndex; ++J) { + if (Reports[J].allocation_address == UntaggedEntryPtr) { + Found = true; + break; + } + } + if (Found) + continue; + } + + auto *Report = &Reports[ReportIndex++]; + if (DeallocationTid) + Report->error_type = USE_AFTER_FREE; + else if (FaultAddr < EntryPtr) + Report->error_type = BUFFER_UNDERFLOW; + else + Report->error_type = BUFFER_OVERFLOW; + + Report->allocation_address = UntaggedEntryPtr; + Report->allocation_size = EntrySize; + collectTraceMaybe(RingBuffer->Depot, Report->allocation_trace, + AllocationTrace); + Report->allocation_tid = AllocationTid; + collectTraceMaybe(RingBuffer->Depot, Report->deallocation_trace, + DeallocationTrace); + Report->deallocation_tid = DeallocationTid; + if (ReportIndex == NumErrorReports) { + // No more report entries. + return; + } + } + } + + void getErrorInfo(uintptr_t FaultAddr, struct scudo_error_info *ErrorInfo) { + const Options Options = Primary.Options.load(); + if (!useMemoryTagging(Options)) + return; + + bool TagExists = extractTag(FaultAddr) != 0; + size_t ReportIndex = 0; + if (TagExists) { + // Check for OOB in the current block and the two surrounding blocks. + // Beyond that, UAF is more likely. + getErrorInfo(FaultAddr, 0, 2, &ErrorInfo->reports[0], ReportIndex); + } + + // Check the ring buffer. For primary allocations this will only find UAF; + // for secondary allocations we can find either UAF or OOB. + getRingBufferErrorInfo(FaultAddr, &ErrorInfo->reports[0], ReportIndex); + + if (TagExists) { + // Check for OOB in the 28 blocks surrounding the 3 we checked earlier. + // Beyond that we are likely to hit false positives. + getErrorInfo(FaultAddr, 2, 16, &ErrorInfo->reports[0], ReportIndex); + } + } + static const uptr MaxTraceSize = 64; static void collectTraceMaybe(const StackDepot *Depot, diff --git a/compiler-rt/lib/scudo/standalone/include/scudo/interface.h b/compiler-rt/lib/scudo/standalone/include/scudo/interface.h index 9f2b93891999d..cf804594c677f 100644 --- a/compiler-rt/lib/scudo/standalone/include/scudo/interface.h +++ b/compiler-rt/lib/scudo/standalone/include/scudo/interface.h @@ -123,6 +123,9 @@ size_t __scudo_get_region_info_size(void); const char *__scudo_get_ring_buffer_addr(void); size_t __scudo_get_ring_buffer_size(void); +void __scudo_get_fault_error_info(uintptr_t fault_addr, + struct scudo_error_info *error_info); + #ifndef M_DECAY_TIME #define M_DECAY_TIME -100 #endif diff --git a/compiler-rt/lib/scudo/standalone/memtag.h b/compiler-rt/lib/scudo/standalone/memtag.h index 073e72c46f68a..653745f3d6147 100644 --- a/compiler-rt/lib/scudo/standalone/memtag.h +++ b/compiler-rt/lib/scudo/standalone/memtag.h @@ -259,6 +259,11 @@ inline uptr loadTag(uptr Ptr) { return TaggedPtr; } +inline uptr loadTagUnaligned(uptr Ptr) { + uptr AlignedPtr = Ptr & ~static_cast(0xF); + return loadTag(AlignedPtr) | (Ptr & 0xF); +} + #else inline constexpr bool systemSupportsMemoryTagging() { return false; } @@ -303,6 +308,11 @@ inline NORETURN uptr loadTag(uptr Ptr) { UNREACHABLE("memory tagging not supported"); } +inline uptr loadTagUnaligned(uptr Ptr) { + (void)Ptr; + UNREACHABLE("memory tagging not supported"); +} + #endif #pragma GCC diagnostic push diff --git a/compiler-rt/lib/scudo/standalone/primary32.h b/compiler-rt/lib/scudo/standalone/primary32.h index 5720c9f308dac..c3705a2b474eb 100644 --- a/compiler-rt/lib/scudo/standalone/primary32.h +++ b/compiler-rt/lib/scudo/standalone/primary32.h @@ -134,6 +134,9 @@ template class SizeClassAllocator32 { const char *getRegionInfoArrayAddress() const { return nullptr; } static uptr getRegionInfoArraySize() { return 0; } + // Not supported in SizeClassAllocator32. + BlockInfo findNearestBlock(UNUSED uptr Ptr) { return {}; } + // Not supported in SizeClassAllocator32. static BlockInfo findNearestBlock(UNUSED const char *RegionInfoData, UNUSED uptr Ptr) { diff --git a/compiler-rt/lib/scudo/standalone/primary64.h b/compiler-rt/lib/scudo/standalone/primary64.h index 6ee56d6c99291..2c3ccdf028530 100644 --- a/compiler-rt/lib/scudo/standalone/primary64.h +++ b/compiler-rt/lib/scudo/standalone/primary64.h @@ -93,6 +93,8 @@ template class SizeClassAllocator64 { static BlockInfo findNearestBlock(const char *RegionInfoData, uptr Ptr) NO_THREAD_SAFETY_ANALYSIS; + BlockInfo findNearestBlock(uptr Ptr); + void init(s32 ReleaseToOsInterval) NO_THREAD_SAFETY_ANALYSIS; void unmapTestOnly(); @@ -1350,6 +1352,57 @@ uptr SizeClassAllocator64::releaseToOS(ReleaseToOS ReleaseType) { return TotalReleasedBytes; } +template +BlockInfo SizeClassAllocator64::findNearestBlock(uptr Ptr) + NO_THREAD_SAFETY_ANALYSIS { + uptr ClassId; + uptr MinDistance = -1UL; + for (uptr I = 0; I != NumClasses; ++I) { + if (I == SizeClassMap::BatchClassId) + continue; + + ScopedLock ML(RegionInfoArray[I].MMLock); + uptr Begin = RegionInfoArray[I].RegionBeg; + uptr End = Begin + RegionInfoArray[I].MemMapInfo.AllocatedUser; + if (Begin > End || End - Begin < SizeClassMap::getSizeByClassId(I)) + continue; + uptr RegionDistance; + if (Begin <= Ptr) { + if (Ptr < End) + RegionDistance = 0; + else + RegionDistance = Ptr - End; + } else { + RegionDistance = Begin - Ptr; + } + + if (RegionDistance < MinDistance) { + MinDistance = RegionDistance; + ClassId = I; + if (RegionDistance == 0) + break; + } + } + + if (MinDistance > 8192) { + return {}; + } + + ScopedLock ML(RegionInfoArray[ClassId].MMLock); + BlockInfo B = {}; + B.RegionBegin = RegionInfoArray[ClassId].RegionBeg; + B.RegionEnd = + B.RegionBegin + RegionInfoArray[ClassId].MemMapInfo.AllocatedUser; + B.BlockSize = SizeClassMap::getSizeByClassId(ClassId); + B.BlockBegin = B.RegionBegin + uptr(sptr(Ptr - B.RegionBegin) / + sptr(B.BlockSize) * sptr(B.BlockSize)); + while (B.BlockBegin < B.RegionBegin) + B.BlockBegin += B.BlockSize; + while (B.RegionEnd < B.BlockBegin + B.BlockSize) + B.BlockBegin -= B.BlockSize; + return B; +} + template /* static */ BlockInfo SizeClassAllocator64::findNearestBlock( const char *RegionInfoData, uptr Ptr) NO_THREAD_SAFETY_ANALYSIS { diff --git a/compiler-rt/lib/scudo/standalone/tests/CMakeLists.txt b/compiler-rt/lib/scudo/standalone/tests/CMakeLists.txt index a85eb737dba0a..68ffc16bce780 100644 --- a/compiler-rt/lib/scudo/standalone/tests/CMakeLists.txt +++ b/compiler-rt/lib/scudo/standalone/tests/CMakeLists.txt @@ -93,6 +93,7 @@ set(SCUDO_UNIT_TEST_SOURCES combined_test.cpp common_test.cpp condition_variable_test.cpp + error_info_test.cpp flags_test.cpp list_test.cpp map_test.cpp diff --git a/compiler-rt/lib/scudo/standalone/tests/error_info_test.cpp b/compiler-rt/lib/scudo/standalone/tests/error_info_test.cpp new file mode 100644 index 0000000000000..d8d4dc4c027bc --- /dev/null +++ b/compiler-rt/lib/scudo/standalone/tests/error_info_test.cpp @@ -0,0 +1,160 @@ +//===-- error_info_test.cpp -------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "allocator_config.h" +#include "combined.h" +#include "memtag.h" +#include "tests/scudo_unit_test.h" +#include + +namespace scudo { + +template struct TestAllocator : Allocator { + TestAllocator() { this->initThreadMaybe(); } + ~TestAllocator() { this->unmapTestOnly(); } +}; + +template struct ScudoErrorInfoTest : public Test { + ScudoErrorInfoTest() { + setenv("SCUDO_OPTIONS", "allocation_ring_buffer_size=32768", 1); + Allocator = std::make_unique(); + Allocator->setTrackAllocationStacks(true); + } + + ~ScudoErrorInfoTest() { + Allocator->releaseToOS(scudo::ReleaseToOS::Force); + unsetenv("SCUDO_OPTIONS"); + } + + using AllocatorT = TestAllocator; + std::unique_ptr Allocator; +}; + +struct TestConfig : public scudo::DefaultConfig { + static const bool MaySupportMemoryTagging = true; +}; + +using ScudoErrorInfoTestTypes = ::testing::Types; +TYPED_TEST_SUITE(ScudoErrorInfoTest, ScudoErrorInfoTestTypes); + +TYPED_TEST(ScudoErrorInfoTest, RingBufferErrorInfo) { + auto *Allocator = this->Allocator.get(); + if (!scudo::archSupportsMemoryTagging() || + !Allocator->useMemoryTaggingTestOnly()) { + GTEST_SKIP() << "MTE not supported or enabled"; + } + + EXPECT_GT(Allocator->getRingBufferSize(), 0u); + EXPECT_NE(nullptr, Allocator->getRingBufferAddress()); + + const scudo::uptr Size = 64U; + void *P = nullptr; + P = Allocator->allocate(Size, Chunk::Origin::Malloc); + ASSERT_NE(P, nullptr); + Allocator->deallocate(P, Chunk::Origin::Malloc); + + scudo::uptr Ptr = reinterpret_cast(P); + + scudo_error_info ErrorInfo = {}; + size_t ReportIndex = 0; + Allocator->getRingBufferErrorInfo(Ptr, &ErrorInfo.reports[0], ReportIndex); + + EXPECT_EQ(ReportIndex, 1U); + EXPECT_EQ(ErrorInfo.reports[0].error_type, USE_AFTER_FREE); + EXPECT_EQ(ErrorInfo.reports[0].allocation_address, scudo::untagPointer(Ptr)); + EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size); + + // Now verify the ReportIndex is followed. + memset(&ErrorInfo, 0, sizeof(ErrorInfo)); + Allocator->getRingBufferErrorInfo(Ptr, &ErrorInfo.reports[0], ReportIndex); + EXPECT_EQ(ReportIndex, 2U); + EXPECT_EQ(ErrorInfo.reports[0].error_type, UNKNOWN); + EXPECT_EQ(ErrorInfo.reports[1].error_type, USE_AFTER_FREE); + EXPECT_EQ(ErrorInfo.reports[1].allocation_address, scudo::untagPointer(Ptr)); + EXPECT_EQ(ErrorInfo.reports[1].allocation_size, Size); + + // Verify if at max, nothing happens. + ReportIndex = 3; + memset(&ErrorInfo, 0, sizeof(ErrorInfo)); + Allocator->getRingBufferErrorInfo(Ptr, &ErrorInfo.reports[0], ReportIndex); + EXPECT_EQ(ReportIndex, 3U); + EXPECT_EQ(ErrorInfo.reports[0].error_type, UNKNOWN); + EXPECT_EQ(ErrorInfo.reports[1].error_type, UNKNOWN); + EXPECT_EQ(ErrorInfo.reports[2].error_type, UNKNOWN); +} + +TYPED_TEST(ScudoErrorInfoTest, GetErrorInfoUAF) { + auto *Allocator = this->Allocator.get(); + if (!scudo::archSupportsMemoryTagging() || + !Allocator->useMemoryTaggingTestOnly()) { + GTEST_SKIP() << "MTE not supported or enabled"; + } + + const scudo::uptr Size = 64U; + void *P = Allocator->allocate(Size, Chunk::Origin::Malloc); + ASSERT_NE(P, nullptr); + Allocator->deallocate(P, Chunk::Origin::Malloc); + + scudo::uptr Ptr = reinterpret_cast(P); + scudo_error_info ErrorInfo = {}; + Allocator->getErrorInfo(Ptr, &ErrorInfo); + + EXPECT_EQ(ErrorInfo.reports[0].error_type, USE_AFTER_FREE); + EXPECT_EQ(ErrorInfo.reports[0].allocation_address, scudo::untagPointer(Ptr)); + EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size); +} + +TYPED_TEST(ScudoErrorInfoTest, GetErrorInfoOverflow) { + auto *Allocator = this->Allocator.get(); + if (!scudo::archSupportsMemoryTagging() || + !Allocator->useMemoryTaggingTestOnly()) { + GTEST_SKIP() << "MTE not supported or enabled"; + } + + const scudo::uptr Size = 64U; + void *P = Allocator->allocate(Size, Chunk::Origin::Malloc); + ASSERT_NE(P, nullptr); + + scudo::uptr PtrAddr = reinterpret_cast(P); + scudo::uptr FaultAddr = PtrAddr + Size; + scudo_error_info ErrorInfo = {}; + Allocator->getErrorInfo(FaultAddr, &ErrorInfo); + + EXPECT_EQ(ErrorInfo.reports[0].error_type, BUFFER_OVERFLOW); + EXPECT_EQ(ErrorInfo.reports[0].allocation_address, + scudo::untagPointer(PtrAddr)); + EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size); + + Allocator->deallocate(P, Chunk::Origin::Malloc); +} + +TYPED_TEST(ScudoErrorInfoTest, GetErrorInfoUnderflow) { + auto *Allocator = this->Allocator.get(); + if (!scudo::archSupportsMemoryTagging() || + !Allocator->useMemoryTaggingTestOnly()) { + GTEST_SKIP() << "MTE not supported or enabled"; + } + + const scudo::uptr Size = 64U; + void *P = Allocator->allocate(Size, Chunk::Origin::Malloc); + ASSERT_NE(P, nullptr); + + scudo::uptr PtrAddr = reinterpret_cast(P); + scudo::uptr FaultAddr = PtrAddr - 1; + scudo_error_info ErrorInfo = {}; + Allocator->getErrorInfo(FaultAddr, &ErrorInfo); + + EXPECT_EQ(ErrorInfo.reports[0].error_type, BUFFER_UNDERFLOW); + EXPECT_EQ(ErrorInfo.reports[0].allocation_address, + scudo::untagPointer(PtrAddr)); + EXPECT_EQ(ErrorInfo.reports[0].allocation_size, Size); + + Allocator->deallocate(P, Chunk::Origin::Malloc); +} + +} // namespace scudo diff --git a/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp b/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp index 3a087c497b1a9..2fbfadfbf6240 100644 --- a/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp +++ b/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp @@ -237,6 +237,37 @@ SCUDO_TYPED_TEST(ScudoPrimaryTest, BasicPrimary) { } } +SCUDO_TYPED_TEST(ScudoPrimaryTest, FindNearestBlock) { + using Primary = TestAllocator; + std::unique_ptr Allocator(new Primary); + Allocator->init(/*ReleaseToOsInterval=*/-1); + typename Primary::SizeClassAllocatorT SizeClassAllocator; + SizeClassAllocator.init(nullptr, Allocator.get()); + + const scudo::uptr Size = 64U; + if (Primary::canAllocate(Size)) { + const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size); + void *P = SizeClassAllocator.allocate(ClassId); + ASSERT_NE(P, nullptr); + + scudo::uptr Ptr = reinterpret_cast(P); + scudo::BlockInfo Info = Allocator->findNearestBlock(Ptr); + + if (Info.BlockSize != 0) { + EXPECT_EQ(Info.BlockBegin, Ptr); + EXPECT_EQ(Info.BlockSize, + Primary::SizeClassMap::getSizeByClassId(ClassId)); + + scudo::BlockInfo Info2 = Allocator->findNearestBlock(Ptr + 10); + EXPECT_EQ(Info2.BlockBegin, Ptr); + } + + SizeClassAllocator.deallocate(ClassId, P); + } + + SizeClassAllocator.destroy(nullptr); +} + struct SmallRegionsConfig { static const bool MaySupportMemoryTagging = false; template using TSDRegistryT = void; diff --git a/compiler-rt/lib/scudo/standalone/wrappers_c.cpp b/compiler-rt/lib/scudo/standalone/wrappers_c.cpp index 573f0b7cda60f..daa0bdf6d1434 100644 --- a/compiler-rt/lib/scudo/standalone/wrappers_c.cpp +++ b/compiler-rt/lib/scudo/standalone/wrappers_c.cpp @@ -14,9 +14,13 @@ #include "wrappers_c.h" #include "wrappers_c_checks.h" +#include "string_utils.h" + #include #include +#include + #if defined(SCUDO_PREFIX_NAME) #define SCUDO_PREFIX(name) CONCATENATE(SCUDO_PREFIX_NAME, name) #define SCUDO_ALLOCATOR_STATIC static @@ -426,6 +430,13 @@ SCUDO_PREFIX(malloc_set_add_large_allocation_slack)(int add_slack) { INTERFACE void __scudo_print_stats(void) { Allocator.printStats(); } #if !SCUDO_FUCHSIA + +INTERFACE void +__scudo_get_fault_error_info(uintptr_t fault_address, + struct scudo_error_info *error_info) { + Allocator.getErrorInfo(fault_address, error_info); +} + INTERFACE void __scudo_get_error_info( struct scudo_error_info *error_info, uintptr_t fault_addr, const char *stack_depot, size_t stack_depot_size, const char *region_info,