Skip to content

scudo: Support free_sized and free_aligned_sized from C23 #146556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions compiler-rt/lib/scudo/standalone/chunk.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ namespace Chunk {
// but https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61414 prevents it from
// happening, as it will error, complaining the number of bits is not enough.
enum Origin : u8 {
Malloc = 0,
New = 1,
NewArray = 2,
Memalign = 3,
Malloc = 0, // malloc, calloc, realloc, free, free_sized
New = 1, // operator new, operator delete
NewArray = 2, // operator new [], operator delete []
Memalign = 3, // aligned_alloc, posix_memalign, memalign, pvalloc, valloc,
// free_aligned_sized
};

enum State : u8 { Available = 0, Allocated = 1, Quarantined = 2 };
Expand Down
87 changes: 69 additions & 18 deletions compiler-rt/lib/scudo/standalone/combined.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ class Allocator {
Primary.Options.set(OptionBit::DeallocTypeMismatch);
if (getFlags()->delete_size_mismatch)
Primary.Options.set(OptionBit::DeleteSizeMismatch);
if (getFlags()->free_size_mismatch)
Primary.Options.set(OptionBit::FreeSizeMismatch);
if (getFlags()->free_alignment_mismatch)
Primary.Options.set(OptionBit::FreeAlignmentMismatch);
if (getFlags()->delete_alignment_mismatch)
Primary.Options.set(OptionBit::DeleteAlignmentMismatch);
if (allocatorSupportsMemoryTagging<AllocatorConfig>() &&
systemSupportsMemoryTagging())
Primary.Options.set(OptionBit::UseMemoryTagging);
Expand Down Expand Up @@ -433,7 +439,8 @@ class Allocator {
}

NOINLINE void deallocate(void *Ptr, Chunk::Origin Origin, uptr DeleteSize = 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a separate patch
could you please

Move exiting deallocate into private as

NOINLINE void deallocate(void *Ptr, Chunk::Origin Origin,
                           const uptr *DeleteSize, uptr *Alignment) {   // no default values

and in public section replace
deallocate -> with inline "delete{,Sized}{,Aligned}"

and update existing calls as is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNUSED uptr Alignment = MinAlignment) {
bool HasDeleteSize = false, uptr DeleteAlignment = 0,
bool HasDeleteAlignment = false) {
if (UNLIKELY(!Ptr))
return;

Expand All @@ -456,6 +463,9 @@ class Allocator {
}
#endif // GWP_ASAN_HOOKS

if (UNLIKELY(HasDeleteAlignment && !isPowerOfTwo(DeleteAlignment)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's not be fore GuardedAlloc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because the isAligned check is also not above. If I move this one, arguably that one should also be moved. Thoughts?

reportAlignmentNotPowerOfTwo(DeleteAlignment);

if (UNLIKELY(!isAligned(reinterpret_cast<uptr>(Ptr), MinAlignment)))
reportMisalignedPointer(AllocatorAction::Deallocating, Ptr);

Expand All @@ -470,19 +480,41 @@ class Allocator {

const Options Options = Primary.Options.load();
if (Options.get(OptionBit::DeallocTypeMismatch)) {
if (UNLIKELY(Header.OriginOrWasZeroed != Origin)) {
// With the exception of memalign'd chunks, that can be still be free'd.
if (Header.OriginOrWasZeroed != Chunk::Origin::Memalign ||
Origin != Chunk::Origin::Malloc)
reportDeallocTypeMismatch(AllocatorAction::Deallocating, Ptr,
Header.OriginOrWasZeroed, Origin);
}
if (UNLIKELY(isOriginMismatch(
static_cast<Chunk::Origin>(Header.OriginOrWasZeroed), Origin,
HasDeleteSize)))
reportDeallocTypeMismatch(AllocatorAction::Deallocating, Ptr,
Header.OriginOrWasZeroed, Origin,
HasDeleteSize);
}

const uptr Size = getSize(Ptr, &Header);
if (DeleteSize && Options.get(OptionBit::DeleteSizeMismatch)) {
if (UNLIKELY(DeleteSize != Size))
reportDeleteSizeMismatch(Ptr, DeleteSize, Size);
switch (Origin) {
case Chunk::Origin::New:
FALLTHROUGH;
case Chunk::Origin::NewArray:
if (Options.get(OptionBit::DeleteSizeMismatch) && HasDeleteSize) {
if (UNLIKELY(DeleteSize != Size))
reportDeleteSizeMismatch(Ptr, DeleteSize, Size);
}
if (Options.get(OptionBit::DeleteAlignmentMismatch) &&
HasDeleteAlignment) {
if (UNLIKELY(!isAligned(reinterpret_cast<uptr>(Ptr), DeleteAlignment)))
reportDeleteAlignmentMismatch(Ptr, DeleteAlignment);
}
break;
case Chunk::Origin::Memalign:
if (Options.get(OptionBit::FreeAlignmentMismatch) && HasDeleteAlignment) {
if (UNLIKELY(!isAligned(reinterpret_cast<uptr>(Ptr), DeleteAlignment)))
reportFreeAlignmentMismatch(Ptr, DeleteAlignment);
}
FALLTHROUGH;
case Chunk::Origin::Malloc:
if (Options.get(OptionBit::FreeSizeMismatch) && HasDeleteSize) {
if (UNLIKELY(DeleteSize != Size))
reportFreeSizeMismatch(Ptr, DeleteSize, Size);
}
break;
}

quarantineOrDeallocateChunk(Options, TaggedPtr, &Header, Size);
Expand Down Expand Up @@ -529,14 +561,20 @@ class Allocator {
if (UNLIKELY(Header.State != Chunk::State::Allocated))
reportInvalidChunkState(AllocatorAction::Reallocating, OldPtr);

// Pointer has to be allocated with a malloc-type function. Some
// applications think that it is OK to realloc a memalign'ed pointer, which
// will trigger this check. It really isn't.
if (Options.get(OptionBit::DeallocTypeMismatch)) {
if (UNLIKELY(Header.OriginOrWasZeroed != Chunk::Origin::Malloc))
reportDeallocTypeMismatch(AllocatorAction::Reallocating, OldPtr,
Header.OriginOrWasZeroed,
Chunk::Origin::Malloc);
// There is no language prohibiting the use of realloc with
// aligned_alloc/posix_memalign/memalign and etc. The outcome in
// practice is that the newly allocated memory will typically not
// have the same alignment but will have minimum alignment. With
// regards to operator new, there is no guarantee that the allocator
// being used with malloc is the same as operator new. There is also
// no guarantee that they share the same minimum alignment guarantees.
// So we reject these.
if (UNLIKELY(Header.OriginOrWasZeroed == Chunk::Origin::New ||
Header.OriginOrWasZeroed == Chunk::Origin::NewArray))
reportDeallocTypeMismatch(
AllocatorAction::Reallocating, OldPtr, Header.OriginOrWasZeroed,
Chunk::Origin::Malloc, /*HasDeleteSize=*/false);
}

void *BlockBegin = getBlockBegin(OldTaggedPtr, &Header);
Expand Down Expand Up @@ -1746,6 +1784,19 @@ class Allocator {
return (Bytes - sizeof(AllocationRingBuffer)) /
sizeof(typename AllocationRingBuffer::Entry);
}

static bool isOriginMismatch(Chunk::Origin Alloc, Chunk::Origin Dealloc,
bool HasDeleteSize) {
if (Alloc == Dealloc) {
return false;
}
if (Alloc == Chunk::Origin::Memalign && Dealloc == Chunk::Origin::Malloc &&
!HasDeleteSize) {
// aligned_alloc with free is allowed, but not free_sized.
return false;
}
return true;
}
};

} // namespace scudo
Expand Down
15 changes: 15 additions & 0 deletions compiler-rt/lib/scudo/standalone/flags.inc
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,18 @@ SCUDO_FLAG(int, release_to_os_interval_ms, 5000,
SCUDO_FLAG(int, allocation_ring_buffer_size, 32768,
"Entries to keep in the allocation ring buffer for scudo. "
"Values less or equal to zero disable the buffer.")

SCUDO_FLAG(bool, delete_alignment_mismatch, true,
"Terminate on an alignment mismatch between a aligned-delete and "
"the actual "
"alignment of a chunk (as provided to new/new[]).")

SCUDO_FLAG(bool, free_size_mismatch, true,
"Terminate on a size mismatch between a free_sized and the actual "
"size of a chunk (as provided to malloc/calloc/realloc).")

SCUDO_FLAG(bool, free_alignment_mismatch, true,
"Terminate on an alignment mismatch between a free_aligned_sized "
"and the actual "
"alignment of a chunk (as provided to "
"aligned_alloc/posix_memalign/memalign).")
9 changes: 9 additions & 0 deletions compiler-rt/lib/scudo/standalone/internal_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@
#define UNUSED __attribute__((unused))
#define USED __attribute__((used))
#define NOEXCEPT noexcept
#ifdef __has_attribute
#if __has_attribute(fallthrough)
#define FALLTHROUGH __attribute__((fallthrough))
#else
#define FALLTHROUGH
#endif
#else
#define FALLTHROUGH
#endif

// This check is only available on Clang. This is essentially an alias of
// C++20's 'constinit' specifier which will take care of this when (if?) we can
Expand Down
3 changes: 3 additions & 0 deletions compiler-rt/lib/scudo/standalone/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ enum class OptionBit {
UseOddEvenTags,
UseMemoryTagging,
AddLargeAllocationSlack,
DeleteAlignmentMismatch,
FreeSizeMismatch,
FreeAlignmentMismatch,
};

struct Options {
Expand Down
63 changes: 60 additions & 3 deletions compiler-rt/lib/scudo/standalone/report.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,48 @@ void NORETURN reportMisalignedPointer(AllocatorAction Action, const void *Ptr) {
stringifyAction(Action), Ptr);
}

static const char *stringifyAllocOrigin(Chunk::Origin AllocOrigin) {
switch (AllocOrigin) {
case Chunk::Origin::Malloc:
return "malloc";
case Chunk::Origin::New:
return "operator new";
case Chunk::Origin::NewArray:
return "operator new []";
case Chunk::Origin::Memalign:
return "aligned_alloc";
}
return "<invalid origin>";
}

static const char *stringifyDeallocOrigin(Chunk::Origin DeallocOrigin,
bool HasDeleteSize) {
switch (DeallocOrigin) {
case Chunk::Origin::Malloc:
if (HasDeleteSize)
return "free_sized";
return "free";
case Chunk::Origin::New:
return "operator delete";
case Chunk::Origin::NewArray:
return "operator delete []";
case Chunk::Origin::Memalign:
return "free_aligned_sized";
}
return "<invalid origin>";
}

// The deallocation function used is at odds with the one used to allocate the
// chunk (eg: new[]/delete or malloc/delete, and so on).
void NORETURN reportDeallocTypeMismatch(AllocatorAction Action, const void *Ptr,
u8 TypeA, u8 TypeB) {
u8 TypeA, u8 TypeB,
bool HasDeleteSize) {
ScopedErrorReport Report;
Report.append("allocation type mismatch when %s address %p (%d vs %d)\n",
stringifyAction(Action), Ptr, TypeA, TypeB);
Report.append(
"allocation type mismatch when %s address %p (%s vs %s)\n",
stringifyAction(Action), Ptr,
stringifyAllocOrigin(static_cast<Chunk::Origin>(TypeA)),
stringifyDeallocOrigin(static_cast<Chunk::Origin>(TypeB), HasDeleteSize));
}

// The size specified to the delete operator does not match the one that was
Expand All @@ -161,6 +196,28 @@ void NORETURN reportDeleteSizeMismatch(const void *Ptr, uptr Size,
Size, ExpectedSize);
}

void NORETURN reportDeleteAlignmentMismatch(const void *Ptr, uptr Alignment) {
ScopedErrorReport Report;
Report.append(
"invalid aligned delete when deallocating address %p (%zu vs %zu)\n", Ptr,
getLeastSignificantSetBitIndex(reinterpret_cast<uptr>(Ptr)), Alignment);
}

void NORETURN reportFreeSizeMismatch(const void *Ptr, uptr Size,
uptr ExpectedSize) {
ScopedErrorReport Report;
Report.append(
"invalid sized free when deallocating address %p (%zu vs %zu)\n", Ptr,
Size, ExpectedSize);
}

void NORETURN reportFreeAlignmentMismatch(const void *Ptr, uptr Alignment) {
ScopedErrorReport Report;
Report.append(
"invalid aligned free when deallocating address %p (%zu vs %zu)\n", Ptr,
getLeastSignificantSetBitIndex(reinterpret_cast<uptr>(Ptr)), Alignment);
}

void NORETURN reportAlignmentNotPowerOfTwo(uptr Alignment) {
ScopedErrorReport Report;
Report.append(
Expand Down
6 changes: 5 additions & 1 deletion compiler-rt/lib/scudo/standalone/report.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ enum class AllocatorAction : u8 {
void NORETURN reportInvalidChunkState(AllocatorAction Action, const void *Ptr);
void NORETURN reportMisalignedPointer(AllocatorAction Action, const void *Ptr);
void NORETURN reportDeallocTypeMismatch(AllocatorAction Action, const void *Ptr,
u8 TypeA, u8 TypeB);
u8 TypeA, u8 TypeB, bool HasDeleteSize);
void NORETURN reportDeleteSizeMismatch(const void *Ptr, uptr Size,
uptr ExpectedSize);
void NORETURN reportDeleteAlignmentMismatch(const void *Ptr, uptr Alignment);
void NORETURN reportFreeSizeMismatch(const void *Ptr, uptr Size,
uptr ExpectedSize);
void NORETURN reportFreeAlignmentMismatch(const void *Ptr, uptr Alignment);

// C wrappers errors.
void NORETURN reportAlignmentNotPowerOfTwo(uptr Alignment);
Expand Down
26 changes: 13 additions & 13 deletions compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ void ScudoCombinedTest<Config>::BasicTest(scudo::uptr SizeLog) {
EXPECT_LE(Size, Allocator->getUsableSize(P));
memset(P, 0xaa, Size);
checkMemoryTaggingMaybe(Allocator, P, Size, Align);
Allocator->deallocate(P, Origin, Size);
Allocator->deallocate(P, Origin, Size, true);
}
}

Expand Down Expand Up @@ -374,7 +374,7 @@ SCUDO_TYPED_TEST(ScudoCombinedTest, ZeroContents) {
for (scudo::uptr I = 0; I < Size; I++)
ASSERT_EQ((reinterpret_cast<char *>(P))[I], '\0');
memset(P, 0xaa, Size);
Allocator->deallocate(P, Origin, Size);
Allocator->deallocate(P, Origin, Size, true);
}
}
}
Expand All @@ -392,7 +392,7 @@ SCUDO_TYPED_TEST(ScudoCombinedTest, ZeroFill) {
for (scudo::uptr I = 0; I < Size; I++)
ASSERT_EQ((reinterpret_cast<char *>(P))[I], '\0');
memset(P, 0xaa, Size);
Allocator->deallocate(P, Origin, Size);
Allocator->deallocate(P, Origin, Size, true);
}
}
}
Expand Down Expand Up @@ -709,7 +709,7 @@ SCUDO_TYPED_TEST(ScudoCombinedTest, ThreadedCombined) {

while (!V.empty()) {
auto Pair = V.back();
Allocator->deallocate(Pair.first, Origin, Pair.second);
Allocator->deallocate(Pair.first, Origin, Pair.second, true);
V.pop_back();
}
});
Expand Down Expand Up @@ -782,26 +782,26 @@ TEST(ScudoCombinedDeathTest, DeathCombined) {
EXPECT_NE(P, nullptr);

// Invalid sized deallocation.
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size + 8U), "");
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size + 8U, true), "");

// Misaligned pointer. Potentially unused if EXPECT_DEATH isn't available.
UNUSED void *MisalignedP =
reinterpret_cast<void *>(reinterpret_cast<scudo::uptr>(P) | 1U);
EXPECT_DEATH(Allocator->deallocate(MisalignedP, Origin, Size), "");
EXPECT_DEATH(Allocator->reallocate(MisalignedP, Size * 2U), "");
EXPECT_DEATH(Allocator->deallocate(MisalignedP, Origin, Size, true), "");
EXPECT_DEATH(Allocator->reallocate(MisalignedP, Size * 2U, true), "");

// Header corruption.
scudo::u64 *H =
reinterpret_cast<scudo::u64 *>(scudo::Chunk::getAtomicHeader(P));
*H ^= 0x42U;
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size), "");
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size, true), "");
*H ^= 0x420042U;
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size), "");
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size, true), "");
*H ^= 0x420000U;

// Invalid chunk state.
Allocator->deallocate(P, Origin, Size);
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size), "");
Allocator->deallocate(P, Origin, Size, true);
EXPECT_DEATH(Allocator->deallocate(P, Origin, Size, true), "");
EXPECT_DEATH(Allocator->reallocate(P, Size * 2U), "");
EXPECT_DEATH(Allocator->getUsableSize(P), "");
}
Expand Down Expand Up @@ -908,13 +908,13 @@ SCUDO_TYPED_TEST(ScudoCombinedTest, DisableMemInit) {
memset(Ptrs[I], 0xaa, Size);
}
for (unsigned I = 0; I != Ptrs.size(); ++I)
Allocator->deallocate(Ptrs[I], Origin, Size);
Allocator->deallocate(Ptrs[I], Origin, Size, true);
for (unsigned I = 0; I != Ptrs.size(); ++I) {
Ptrs[I] = Allocator->allocate(Size - 8, Origin);
memset(Ptrs[I], 0xbb, Size - 8);
}
for (unsigned I = 0; I != Ptrs.size(); ++I)
Allocator->deallocate(Ptrs[I], Origin, Size - 8);
Allocator->deallocate(Ptrs[I], Origin, Size - 8, true);
for (unsigned I = 0; I != Ptrs.size(); ++I) {
Ptrs[I] = Allocator->allocate(Size, Origin, 1U << MinAlignLog, true);
for (scudo::uptr J = 0; J < Size; ++J)
Expand Down
2 changes: 1 addition & 1 deletion compiler-rt/lib/scudo/standalone/tests/report_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ TEST(ScudoReportDeathTest, Generic) {
scudo::reportMisalignedPointer(scudo::AllocatorAction::Deallocating, P),
"Scudo ERROR.*deallocating.*42424242");
EXPECT_DEATH(scudo::reportDeallocTypeMismatch(
scudo::AllocatorAction::Reallocating, P, 0, 1),
scudo::AllocatorAction::Reallocating, P, 0, 1, false),
"Scudo ERROR.*reallocating.*42424242");
EXPECT_DEATH(scudo::reportDeleteSizeMismatch(P, 123, 456),
"Scudo ERROR.*42424242.*123.*456");
Expand Down
Loading
Loading