[scudo] For a realloc that shrinks, retag the extra. (#204031) When MTE is enabled and an allocations is reallocated from a large size to a smaller size, zero tag the rest of the allocation. Before this change only a single granule after the new size was zero tagged. This adds extra security and use after realloc protection if code would have tried to read/write into the old size, past the new size. GitOrigin-RevId: 5639b33d7567bf648f27418b126f5f6f6292b730
diff --git a/combined.h b/combined.h index 88c9715..409c0ab 100644 --- a/combined.h +++ b/combined.h
@@ -643,9 +643,10 @@ Chunk::storeHeader(Cookie, OldPtr, &Header); if (UNLIKELY(useMemoryTagging<AllocatorConfig>(Options))) { if (ClassId) { - resizeTaggedChunk(reinterpret_cast<uptr>(OldTaggedPtr) + OldSize, - reinterpret_cast<uptr>(OldTaggedPtr) + NewSize, - NewSize, untagPointer(BlockEnd)); + resizeTaggedChunk</*ClearAllTags*/ true>( + reinterpret_cast<uptr>(OldTaggedPtr) + OldSize, + reinterpret_cast<uptr>(OldTaggedPtr) + NewSize, NewSize, + untagPointer(BlockEnd)); storePrimaryAllocationStackMaybe(Options, OldPtr); } else { storeSecondaryAllocationStackMaybe(Options, OldPtr, NewSize); @@ -1497,7 +1498,8 @@ if (NextPage < PrevEnd && loadTag(NextPage) != NextPage) PrevEnd = NextPage; TaggedPtr = reinterpret_cast<void *>(TaggedUserPtr); - resizeTaggedChunk(PrevEnd, TaggedUserPtr + Size, Size, BlockEnd); + resizeTaggedChunk</*ClearAllTags*/ false>(PrevEnd, TaggedUserPtr + Size, + Size, BlockEnd); if (UNLIKELY(FillContents != NoFill && !Header.OriginOrWasZeroed)) { // If an allocation needs to be zeroed (i.e. calloc) we can normally // avoid zeroing the memory now since we can rely on memory having @@ -1706,21 +1708,29 @@ return reinterpret_cast<void *>(TaggedBegin); } + template <bool ClearAllTags> void resizeTaggedChunk(uptr OldPtr, uptr NewPtr, uptr NewSize, uptr BlockEnd) { uptr RoundOldPtr = roundUp(OldPtr, archMemoryTagGranuleSize()); uptr RoundNewPtr; if (RoundOldPtr >= NewPtr) { - // If the allocation is shrinking we just need to set the tag past the end - // of the allocation to 0. See explanation in storeEndMarker() above. RoundNewPtr = roundUp(NewPtr, archMemoryTagGranuleSize()); + if (ClearAllTags) { + // Shrinking resize, set the tag to zero for the rest of the allocation. + // This prevents use after realloc of the excess memory. + storeTags(untagPointer(RoundNewPtr), untagPointer(RoundOldPtr)); + } else { + // Add a zero tag right after the pointer to prevent overflow. + storeEndMarker(RoundNewPtr, NewSize, BlockEnd); + } } else { // Set the memory tag of the region // [RoundOldPtr, roundUp(NewPtr, archMemoryTagGranuleSize())) // to the pointer tag stored in OldPtr. RoundNewPtr = storeTags(RoundOldPtr, NewPtr); + // Add a zero tag right after the pointer to prevent overflow. + storeEndMarker(RoundNewPtr, NewSize, BlockEnd); } - storeEndMarker(RoundNewPtr, NewSize, BlockEnd); } void storePrimaryAllocationStackMaybe(const Options &Options, void *Ptr) {
diff --git a/tests/combined_test.cpp b/tests/combined_test.cpp index 48ae283..528afc4 100644 --- a/tests/combined_test.cpp +++ b/tests/combined_test.cpp
@@ -523,6 +523,44 @@ Allocator->deallocate(P, Origin); } +// Verify that a realloc that shrinks retags all of the extra size +// so it will fault it touched. +SCUDO_TYPED_TEST(ScudoCombinedDeathTest, ReallocateDecreasingTagged) { + auto *Allocator = this->Allocator.get(); + if (!Allocator->useMemoryTaggingTestOnly()) { + TEST_SKIP("Requires MTE"); + } + + const scudo::uptr GranuleSize = scudo::archMemoryTagGranuleSize(); + // Get the largest value that fits in the primary. + scudo::uptr Size = + TypeParam::Primary::SizeClassMap::MaxSize - scudo::Chunk::getHeaderSize(); + EXPECT_TRUE( + isPrimaryAllocation<TestAllocator<TypeParam>>(Size, 1U << MinAlignLog)); + void *P = Allocator->allocate(Size, Origin); + EXPECT_NE(P, nullptr); + memset(P, 'Z', Size); + + // Shrink three granule sizes to verify that extra space will fail. + EXPECT_GT(Size, GranuleSize * 3); + scudo::uptr NewSize = scudo::roundDown(Size - GranuleSize * 3, GranuleSize); + void *NewP = Allocator->reallocate(P, NewSize); + EXPECT_NE(NewP, nullptr); + EXPECT_EQ(P, NewP); + memset(NewP, 'Y', NewSize); + + // Check that accessing the entire allocation after new size causes a crash. + for (size_t I = NewSize; I < Size; I++) { + EXPECT_DEATH( + { + disableDebuggerdMaybe(); + reinterpret_cast<char *>(NewP)[I] = 'A'; + }, + ""); + } + Allocator->deallocate(P, Origin); +} + SCUDO_TYPED_TEST(ScudoCombinedTest, IterateOverChunks) { auto *Allocator = this->Allocator.get(); // Allocates a bunch of chunks, then iterate over all the chunks, ensuring