[scudo] Create a non-static getErrorInfo function. (#199770) Create a getErrorInfo function that operates on the Allocator and doesn't require passing in all of the extra information. Add interface function __scudo_get_fault_error_info that calls this new function. Add all needed functions to support the new getErrorInfo. This is being added to replace the static version from Android that required linking in a copy of libscudo to use. This new version will be used directly from libc. GitOrigin-RevId: a0ac7521342567573da5b1e47c94f6199faada39
diff --git a/combined.h b/combined.h index 72b4b36..b37ff23 100644 --- a/combined.h +++ b/combined.h
@@ -988,6 +988,186 @@ : 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<const char *>(loadTagUnaligned(Addr))); + ChunkAddr = loadTagUnaligned(Addr + ChunkOffset); + uptr HeaderAddr = loadTag(Addr + ChunkOffset - Chunk::getHeaderSize()); + *Header = *reinterpret_cast<const Chunk::UnpackedHeader *>(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<const u32 *>(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<const u32 *>(loadTagUnaligned( + reinterpret_cast<uptr>(&ChunkData[MemTagAllocationTraceIndex]))); + collectTraceMaybe(RingBuffer->Depot, Report->allocation_trace, + *TracePtr); + } + const u32 *TidPtr = reinterpret_cast<const u32 *>(loadTagUnaligned( + reinterpret_cast<uptr>(&ChunkData[MemTagAllocationTidIndex]))); + Report->allocation_tid = *TidPtr; + return ReportIndex == NumErrorReports; + }; + + if (MinDistance == 0 && CheckOOB(Info.BlockBegin)) + return; + + for (size_t I = Max<size_t>(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<AllocatorConfig>(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/include/scudo/interface.h b/include/scudo/interface.h index 9f2b938..cf80459 100644 --- a/include/scudo/interface.h +++ b/include/scudo/interface.h
@@ -123,6 +123,9 @@ 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/memtag.h b/memtag.h index 073e72c..653745f 100644 --- a/memtag.h +++ b/memtag.h
@@ -259,6 +259,11 @@ return TaggedPtr; } +inline uptr loadTagUnaligned(uptr Ptr) { + uptr AlignedPtr = Ptr & ~static_cast<uptr>(0xF); + return loadTag(AlignedPtr) | (Ptr & 0xF); +} + #else inline constexpr bool systemSupportsMemoryTagging() { return false; } @@ -303,6 +308,11 @@ 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/primary32.h b/primary32.h index 5720c9f..c3705a2 100644 --- a/primary32.h +++ b/primary32.h
@@ -135,6 +135,9 @@ 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) { return {};
diff --git a/primary64.h b/primary64.h index 6ee56d6..2c3ccdf 100644 --- a/primary64.h +++ b/primary64.h
@@ -93,6 +93,8 @@ 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(); @@ -1351,6 +1353,57 @@ } template <typename Config> +BlockInfo SizeClassAllocator64<Config>::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 <typename Config> /* static */ BlockInfo SizeClassAllocator64<Config>::findNearestBlock( const char *RegionInfoData, uptr Ptr) NO_THREAD_SAFETY_ANALYSIS { const RegionInfo *RegionInfoArray =
diff --git a/tests/error_info_test.cpp b/tests/error_info_test.cpp new file mode 100644 index 0000000..d8d4dc4 --- /dev/null +++ b/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 <stdlib.h> + +namespace scudo { + +template <typename Config> struct TestAllocator : Allocator<Config> { + TestAllocator() { this->initThreadMaybe(); } + ~TestAllocator() { this->unmapTestOnly(); } +}; + +template <class TypeParam> struct ScudoErrorInfoTest : public Test { + ScudoErrorInfoTest() { + setenv("SCUDO_OPTIONS", "allocation_ring_buffer_size=32768", 1); + Allocator = std::make_unique<AllocatorT>(); + Allocator->setTrackAllocationStacks(true); + } + + ~ScudoErrorInfoTest() { + Allocator->releaseToOS(scudo::ReleaseToOS::Force); + unsetenv("SCUDO_OPTIONS"); + } + + using AllocatorT = TestAllocator<TypeParam>; + std::unique_ptr<AllocatorT> Allocator; +}; + +struct TestConfig : public scudo::DefaultConfig { + static const bool MaySupportMemoryTagging = true; +}; + +using ScudoErrorInfoTestTypes = ::testing::Types<TestConfig>; +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<scudo::uptr>(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<scudo::uptr>(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<scudo::uptr>(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<scudo::uptr>(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/tests/primary_test.cpp b/tests/primary_test.cpp index 3a087c4..2fbfadf 100644 --- a/tests/primary_test.cpp +++ b/tests/primary_test.cpp
@@ -237,6 +237,37 @@ } } +SCUDO_TYPED_TEST(ScudoPrimaryTest, FindNearestBlock) { + using Primary = TestAllocator<TypeParam, scudo::DefaultSizeClassMap>; + std::unique_ptr<Primary> 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<scudo::uptr>(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 <typename> using TSDRegistryT = void;
diff --git a/wrappers_c.cpp b/wrappers_c.cpp index 573f0b7..daa0bdf 100644 --- a/wrappers_c.cpp +++ b/wrappers_c.cpp
@@ -14,9 +14,13 @@ #include "wrappers_c.h" #include "wrappers_c_checks.h" +#include "string_utils.h" + #include <stdint.h> #include <stdio.h> +#include <sys/uio.h> + #if defined(SCUDO_PREFIX_NAME) #define SCUDO_PREFIX(name) CONCATENATE(SCUDO_PREFIX_NAME, name) #define SCUDO_ALLOCATOR_STATIC static @@ -426,6 +430,13 @@ 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,