Skip to content

Commit 54ddd07

Browse files
authored
[scudo] Update header without read-modify-write operation (#66955)
We used to update the deallocated block with atomic_compare_exchange_strong to ensure the concurrent double-free will be detected. However, this operation incurs huge performance overhead which takes over 50% execution time in deallocate(). Given that we already have the checksum to guard the most double-free cases and other block verifications in the primary allocator, use atomic-store instead.
1 parent 12866a2 commit 54ddd07

File tree

6 files changed

+30
-80
lines changed

6 files changed

+30
-80
lines changed

compiler-rt/lib/scudo/standalone/chunk.h

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,6 @@ inline void loadHeader(u32 Cookie, const void *Ptr,
128128
reportHeaderCorruption(const_cast<void *>(Ptr));
129129
}
130130

131-
inline void compareExchangeHeader(u32 Cookie, void *Ptr,
132-
UnpackedHeader *NewUnpackedHeader,
133-
UnpackedHeader *OldUnpackedHeader) {
134-
NewUnpackedHeader->Checksum =
135-
computeHeaderChecksum(Cookie, Ptr, NewUnpackedHeader);
136-
PackedHeader NewPackedHeader = bit_cast<PackedHeader>(*NewUnpackedHeader);
137-
PackedHeader OldPackedHeader = bit_cast<PackedHeader>(*OldUnpackedHeader);
138-
if (UNLIKELY(!atomic_compare_exchange_strong(
139-
getAtomicHeader(Ptr), &OldPackedHeader, NewPackedHeader,
140-
memory_order_relaxed)))
141-
reportHeaderRace(Ptr);
142-
}
143-
144131
inline bool isValid(u32 Cookie, const void *Ptr,
145132
UnpackedHeader *NewUnpackedHeader) {
146133
PackedHeader NewPackedHeader = atomic_load_relaxed(getConstAtomicHeader(Ptr));

compiler-rt/lib/scudo/standalone/combined.h

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,13 @@ class Allocator {
6767
if (UNLIKELY(Header.State != Chunk::State::Quarantined))
6868
reportInvalidChunkState(AllocatorAction::Recycling, Ptr);
6969

70-
Chunk::UnpackedHeader NewHeader = Header;
71-
NewHeader.State = Chunk::State::Available;
72-
Chunk::compareExchangeHeader(Allocator.Cookie, Ptr, &NewHeader, &Header);
70+
Header.State = Chunk::State::Available;
71+
Chunk::storeHeader(Allocator.Cookie, Ptr, &Header);
7372

7473
if (allocatorSupportsMemoryTagging<Config>())
7574
Ptr = untagPointer(Ptr);
76-
void *BlockBegin = Allocator::getBlockBegin(Ptr, &NewHeader);
77-
Cache.deallocate(NewHeader.ClassId, BlockBegin);
75+
void *BlockBegin = Allocator::getBlockBegin(Ptr, &Header);
76+
Cache.deallocate(Header.ClassId, BlockBegin);
7877
}
7978

8079
// We take a shortcut when allocating a quarantine batch by working with the
@@ -117,9 +116,8 @@ class Allocator {
117116
DCHECK_EQ(Header.Offset, 0);
118117
DCHECK_EQ(Header.SizeOrUnusedBytes, sizeof(QuarantineBatch));
119118

120-
Chunk::UnpackedHeader NewHeader = Header;
121-
NewHeader.State = Chunk::State::Available;
122-
Chunk::compareExchangeHeader(Allocator.Cookie, Ptr, &NewHeader, &Header);
119+
Header.State = Chunk::State::Available;
120+
Chunk::storeHeader(Allocator.Cookie, Ptr, &Header);
123121
Cache.deallocate(QuarantineClassId,
124122
reinterpret_cast<void *>(reinterpret_cast<uptr>(Ptr) -
125123
Chunk::getHeaderSize()));
@@ -610,47 +608,46 @@ class Allocator {
610608
if (UNLIKELY(!isAligned(reinterpret_cast<uptr>(OldPtr), MinAlignment)))
611609
reportMisalignedPointer(AllocatorAction::Reallocating, OldPtr);
612610

613-
Chunk::UnpackedHeader OldHeader;
614-
Chunk::loadHeader(Cookie, OldPtr, &OldHeader);
611+
Chunk::UnpackedHeader Header;
612+
Chunk::loadHeader(Cookie, OldPtr, &Header);
615613

616-
if (UNLIKELY(OldHeader.State != Chunk::State::Allocated))
614+
if (UNLIKELY(Header.State != Chunk::State::Allocated))
617615
reportInvalidChunkState(AllocatorAction::Reallocating, OldPtr);
618616

619617
// Pointer has to be allocated with a malloc-type function. Some
620618
// applications think that it is OK to realloc a memalign'ed pointer, which
621619
// will trigger this check. It really isn't.
622620
if (Options.get(OptionBit::DeallocTypeMismatch)) {
623-
if (UNLIKELY(OldHeader.OriginOrWasZeroed != Chunk::Origin::Malloc))
621+
if (UNLIKELY(Header.OriginOrWasZeroed != Chunk::Origin::Malloc))
624622
reportDeallocTypeMismatch(AllocatorAction::Reallocating, OldPtr,
625-
OldHeader.OriginOrWasZeroed,
623+
Header.OriginOrWasZeroed,
626624
Chunk::Origin::Malloc);
627625
}
628626

629-
void *BlockBegin = getBlockBegin(OldTaggedPtr, &OldHeader);
627+
void *BlockBegin = getBlockBegin(OldTaggedPtr, &Header);
630628
uptr BlockEnd;
631629
uptr OldSize;
632-
const uptr ClassId = OldHeader.ClassId;
630+
const uptr ClassId = Header.ClassId;
633631
if (LIKELY(ClassId)) {
634632
BlockEnd = reinterpret_cast<uptr>(BlockBegin) +
635633
SizeClassMap::getSizeByClassId(ClassId);
636-
OldSize = OldHeader.SizeOrUnusedBytes;
634+
OldSize = Header.SizeOrUnusedBytes;
637635
} else {
638636
BlockEnd = SecondaryT::getBlockEnd(BlockBegin);
639637
OldSize = BlockEnd - (reinterpret_cast<uptr>(OldTaggedPtr) +
640-
OldHeader.SizeOrUnusedBytes);
638+
Header.SizeOrUnusedBytes);
641639
}
642640
// If the new chunk still fits in the previously allocated block (with a
643641
// reasonable delta), we just keep the old block, and update the chunk
644642
// header to reflect the size change.
645643
if (reinterpret_cast<uptr>(OldTaggedPtr) + NewSize <= BlockEnd) {
646644
if (NewSize > OldSize || (OldSize - NewSize) < getPageSizeCached()) {
647-
Chunk::UnpackedHeader NewHeader = OldHeader;
648-
NewHeader.SizeOrUnusedBytes =
645+
Header.SizeOrUnusedBytes =
649646
(ClassId ? NewSize
650647
: BlockEnd -
651648
(reinterpret_cast<uptr>(OldTaggedPtr) + NewSize)) &
652649
Chunk::SizeOrUnusedBytesMask;
653-
Chunk::compareExchangeHeader(Cookie, OldPtr, &NewHeader, &OldHeader);
650+
Chunk::storeHeader(Cookie, OldPtr, &Header);
654651
if (UNLIKELY(useMemoryTagging<Config>(Options))) {
655652
if (ClassId) {
656653
resizeTaggedChunk(reinterpret_cast<uptr>(OldTaggedPtr) + OldSize,
@@ -672,7 +669,7 @@ class Allocator {
672669
void *NewPtr = allocate(NewSize, Chunk::Origin::Malloc, Alignment);
673670
if (LIKELY(NewPtr)) {
674671
memcpy(NewPtr, OldTaggedPtr, Min(NewSize, OldSize));
675-
quarantineOrDeallocateChunk(Options, OldTaggedPtr, &OldHeader, OldSize);
672+
quarantineOrDeallocateChunk(Options, OldTaggedPtr, &Header, OldSize);
676673
}
677674
return NewPtr;
678675
}
@@ -1111,31 +1108,30 @@ class Allocator {
11111108
Chunk::UnpackedHeader *Header,
11121109
uptr Size) NO_THREAD_SAFETY_ANALYSIS {
11131110
void *Ptr = getHeaderTaggedPointer(TaggedPtr);
1114-
Chunk::UnpackedHeader NewHeader = *Header;
11151111
// If the quarantine is disabled, the actual size of a chunk is 0 or larger
11161112
// than the maximum allowed, we return a chunk directly to the backend.
11171113
// This purposefully underflows for Size == 0.
11181114
const bool BypassQuarantine = !Quarantine.getCacheSize() ||
11191115
((Size - 1) >= QuarantineMaxChunkSize) ||
1120-
!NewHeader.ClassId;
1116+
!Header->ClassId;
11211117
if (BypassQuarantine)
1122-
NewHeader.State = Chunk::State::Available;
1118+
Header->State = Chunk::State::Available;
11231119
else
1124-
NewHeader.State = Chunk::State::Quarantined;
1125-
NewHeader.OriginOrWasZeroed = useMemoryTagging<Config>(Options) &&
1126-
NewHeader.ClassId &&
1127-
!TSDRegistry.getDisableMemInit();
1128-
Chunk::compareExchangeHeader(Cookie, Ptr, &NewHeader, Header);
1120+
Header->State = Chunk::State::Quarantined;
1121+
Header->OriginOrWasZeroed = useMemoryTagging<Config>(Options) &&
1122+
Header->ClassId &&
1123+
!TSDRegistry.getDisableMemInit();
1124+
Chunk::storeHeader(Cookie, Ptr, Header);
11291125

11301126
if (UNLIKELY(useMemoryTagging<Config>(Options))) {
11311127
u8 PrevTag = extractTag(reinterpret_cast<uptr>(TaggedPtr));
11321128
storeDeallocationStackMaybe(Options, Ptr, PrevTag, Size);
1133-
if (NewHeader.ClassId) {
1129+
if (Header->ClassId) {
11341130
if (!TSDRegistry.getDisableMemInit()) {
11351131
uptr TaggedBegin, TaggedEnd;
11361132
const uptr OddEvenMask = computeOddEvenMaskForPointerMaybe(
1137-
Options, reinterpret_cast<uptr>(getBlockBegin(Ptr, &NewHeader)),
1138-
NewHeader.ClassId);
1133+
Options, reinterpret_cast<uptr>(getBlockBegin(Ptr, Header)),
1134+
Header->ClassId);
11391135
// Exclude the previous tag so that immediate use after free is
11401136
// detected 100% of the time.
11411137
setRandomTag(Ptr, Size, OddEvenMask | (1UL << PrevTag), &TaggedBegin,
@@ -1146,8 +1142,8 @@ class Allocator {
11461142
if (BypassQuarantine) {
11471143
if (allocatorSupportsMemoryTagging<Config>())
11481144
Ptr = untagPointer(Ptr);
1149-
void *BlockBegin = getBlockBegin(Ptr, &NewHeader);
1150-
const uptr ClassId = NewHeader.ClassId;
1145+
void *BlockBegin = getBlockBegin(Ptr, Header);
1146+
const uptr ClassId = Header->ClassId;
11511147
if (LIKELY(ClassId)) {
11521148
bool UnlockRequired;
11531149
auto *TSD = TSDRegistry.getTSDAndLock(&UnlockRequired);

compiler-rt/lib/scudo/standalone/report.cpp

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,6 @@ void NORETURN reportHeaderCorruption(void *Ptr) {
6767
Report.append("corrupted chunk header at address %p\n", Ptr);
6868
}
6969

70-
// Two threads have attempted to modify a chunk header at the same time. This is
71-
// symptomatic of a race-condition in the application code, or general lack of
72-
// proper locking.
73-
void NORETURN reportHeaderRace(void *Ptr) {
74-
ScopedErrorReport Report;
75-
Report.append("race on chunk header at address %p\n", Ptr);
76-
}
77-
7870
// The allocator was compiled with parameters that conflict with field size
7971
// requirements.
8072
void NORETURN reportSanityCheckError(const char *Field) {

compiler-rt/lib/scudo/standalone/report.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ void NORETURN reportInvalidFlag(const char *FlagType, const char *Value);
2323

2424
// Chunk header related errors.
2525
void NORETURN reportHeaderCorruption(void *Ptr);
26-
void NORETURN reportHeaderRace(void *Ptr);
2726

2827
// Sanity checks related error.
2928
void NORETURN reportSanityCheckError(const char *Field);

compiler-rt/lib/scudo/standalone/tests/chunk_test.cpp

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,6 @@ TEST(ScudoChunkDeathTest, ChunkBasic) {
3737
free(Block);
3838
}
3939

40-
TEST(ScudoChunkTest, ChunkCmpXchg) {
41-
initChecksum();
42-
const scudo::uptr Size = 0x100U;
43-
scudo::Chunk::UnpackedHeader OldHeader = {};
44-
OldHeader.OriginOrWasZeroed = scudo::Chunk::Origin::Malloc;
45-
OldHeader.ClassId = 0x42U;
46-
OldHeader.SizeOrUnusedBytes = Size;
47-
OldHeader.State = scudo::Chunk::State::Allocated;
48-
void *Block = malloc(HeaderSize + Size);
49-
void *P = reinterpret_cast<void *>(reinterpret_cast<scudo::uptr>(Block) +
50-
HeaderSize);
51-
scudo::Chunk::storeHeader(Cookie, P, &OldHeader);
52-
memset(P, 'A', Size);
53-
scudo::Chunk::UnpackedHeader NewHeader = OldHeader;
54-
NewHeader.State = scudo::Chunk::State::Quarantined;
55-
scudo::Chunk::compareExchangeHeader(Cookie, P, &NewHeader, &OldHeader);
56-
NewHeader = {};
57-
EXPECT_TRUE(scudo::Chunk::isValid(Cookie, P, &NewHeader));
58-
EXPECT_EQ(NewHeader.State, scudo::Chunk::State::Quarantined);
59-
EXPECT_FALSE(scudo::Chunk::isValid(InvalidCookie, P, &NewHeader));
60-
free(Block);
61-
}
62-
6340
TEST(ScudoChunkDeathTest, CorruptHeader) {
6441
initChecksum();
6542
const scudo::uptr Size = 0x100U;

compiler-rt/lib/scudo/standalone/tests/report_test.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ TEST(ScudoReportDeathTest, Generic) {
2323
EXPECT_DEATH(scudo::reportError("TEST123"), "Scudo ERROR.*TEST123");
2424
EXPECT_DEATH(scudo::reportInvalidFlag("ABC", "DEF"), "Scudo ERROR.*ABC.*DEF");
2525
EXPECT_DEATH(scudo::reportHeaderCorruption(P), "Scudo ERROR.*42424242");
26-
EXPECT_DEATH(scudo::reportHeaderRace(P), "Scudo ERROR.*42424242");
2726
EXPECT_DEATH(scudo::reportSanityCheckError("XYZ"), "Scudo ERROR.*XYZ");
2827
EXPECT_DEATH(scudo::reportAlignmentTooBig(123, 456), "Scudo ERROR.*123.*456");
2928
EXPECT_DEATH(scudo::reportAllocationSizeTooBig(123, 456, 789),

0 commit comments

Comments
 (0)