[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