Skip to content

Commit 0e3612e

Browse files
authored
[scudo] Add config option to modify get usable size behavior (#158710)
Currently, Scudo always returns the exact size allocated when calling getUsableSize. This can be a performance issue where some programs will get the usable size and do unnecessary calls to realloc since they think there isn't enough space in the allocation. By default, usable size will still return the exact size of the allocation. Note that if the exact behavior is disabled and MTE is on, then the code will still give an exact usable size.
1 parent 5d9d890 commit 0e3612e

File tree

4 files changed

+324
-19
lines changed

4 files changed

+324
-19
lines changed

compiler-rt/lib/scudo/standalone/allocator_config.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ BASE_OPTIONAL(const bool, MaySupportMemoryTagging, false)
5757
// Disable the quarantine code.
5858
BASE_OPTIONAL(const bool, QuarantineDisabled, false)
5959

60+
// If set to true, malloc_usable_size returns the exact size of the allocation.
61+
// If set to false, return the total available size in the allocation.
62+
BASE_OPTIONAL(const bool, ExactUsableSize, true)
63+
6064
// PRIMARY_REQUIRED_TYPE(NAME)
6165
//
6266
// SizeClassMap to use with the Primary.

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

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -706,19 +706,26 @@ class Allocator {
706706
if (!getChunkFromBlock(Block, &Chunk, &Header) &&
707707
!getChunkFromBlock(addHeaderTag(Block), &Chunk, &Header))
708708
return;
709-
} else {
710-
if (!getChunkFromBlock(addHeaderTag(Block), &Chunk, &Header))
711-
return;
709+
} else if (!getChunkFromBlock(addHeaderTag(Block), &Chunk, &Header)) {
710+
return;
712711
}
713-
if (Header.State == Chunk::State::Allocated) {
714-
uptr TaggedChunk = Chunk;
715-
if (allocatorSupportsMemoryTagging<AllocatorConfig>())
716-
TaggedChunk = untagPointer(TaggedChunk);
717-
if (useMemoryTagging<AllocatorConfig>(Primary.Options.load()))
718-
TaggedChunk = loadTag(Chunk);
719-
Callback(TaggedChunk, getSize(reinterpret_cast<void *>(Chunk), &Header),
720-
Arg);
712+
713+
if (Header.State != Chunk::State::Allocated)
714+
return;
715+
716+
uptr TaggedChunk = Chunk;
717+
if (allocatorSupportsMemoryTagging<AllocatorConfig>())
718+
TaggedChunk = untagPointer(TaggedChunk);
719+
uptr Size;
720+
if (UNLIKELY(useMemoryTagging<AllocatorConfig>(Primary.Options.load()))) {
721+
TaggedChunk = loadTag(Chunk);
722+
Size = getSize(reinterpret_cast<void *>(Chunk), &Header);
723+
} else if (AllocatorConfig::getExactUsableSize()) {
724+
Size = getSize(reinterpret_cast<void *>(Chunk), &Header);
725+
} else {
726+
Size = getUsableSize(reinterpret_cast<void *>(Chunk), &Header);
721727
}
728+
Callback(TaggedChunk, Size, Arg);
722729
};
723730
Primary.iterateOverBlocks(Lambda);
724731
Secondary.iterateOverBlocks(Lambda);
@@ -759,16 +766,50 @@ class Allocator {
759766
return false;
760767
}
761768

762-
// Return the usable size for a given chunk. Technically we lie, as we just
763-
// report the actual size of a chunk. This is done to counteract code actively
764-
// writing past the end of a chunk (like sqlite3) when the usable size allows
765-
// for it, which then forces realloc to copy the usable size of a chunk as
766-
// opposed to its actual size.
769+
ALWAYS_INLINE uptr getUsableSize(const void *Ptr,
770+
Chunk::UnpackedHeader *Header) {
771+
void *BlockBegin = getBlockBegin(Ptr, Header);
772+
if (LIKELY(Header->ClassId)) {
773+
return SizeClassMap::getSizeByClassId(Header->ClassId) -
774+
(reinterpret_cast<uptr>(Ptr) - reinterpret_cast<uptr>(BlockBegin));
775+
}
776+
777+
uptr UntaggedPtr = reinterpret_cast<uptr>(Ptr);
778+
if (allocatorSupportsMemoryTagging<AllocatorConfig>()) {
779+
UntaggedPtr = untagPointer(UntaggedPtr);
780+
BlockBegin = untagPointer(BlockBegin);
781+
}
782+
return SecondaryT::getBlockEnd(BlockBegin) - UntaggedPtr;
783+
}
784+
785+
// Return the usable size for a given chunk. If MTE is enabled or if the
786+
// ExactUsableSize config parameter is true, we report the exact size of
787+
// the original allocation size. Otherwise, we will return the total
788+
// actual usable size.
767789
uptr getUsableSize(const void *Ptr) {
768790
if (UNLIKELY(!Ptr))
769791
return 0;
770792

771-
return getAllocSize(Ptr);
793+
if (AllocatorConfig::getExactUsableSize() ||
794+
UNLIKELY(useMemoryTagging<AllocatorConfig>(Primary.Options.load())))
795+
return getAllocSize(Ptr);
796+
797+
initThreadMaybe();
798+
799+
#ifdef GWP_ASAN_HOOKS
800+
if (UNLIKELY(GuardedAlloc.pointerIsMine(Ptr)))
801+
return GuardedAlloc.getSize(Ptr);
802+
#endif // GWP_ASAN_HOOKS
803+
804+
Ptr = getHeaderTaggedPointer(const_cast<void *>(Ptr));
805+
Chunk::UnpackedHeader Header;
806+
Chunk::loadHeader(Cookie, Ptr, &Header);
807+
808+
// Getting the alloc size of a chunk only makes sense if it's allocated.
809+
if (UNLIKELY(Header.State != Chunk::State::Allocated))
810+
reportInvalidChunkState(AllocatorAction::Sizing, Ptr);
811+
812+
return getUsableSize(Ptr, &Header);
772813
}
773814

774815
uptr getAllocSize(const void *Ptr) {
@@ -951,6 +992,19 @@ class Allocator {
951992
MemorySize, 2, 16);
952993
}
953994

995+
uptr getBlockBeginTestOnly(const void *Ptr) {
996+
Chunk::UnpackedHeader Header;
997+
Chunk::loadHeader(Cookie, Ptr, &Header);
998+
DCHECK(Header.State == Chunk::State::Allocated);
999+
1000+
if (allocatorSupportsMemoryTagging<AllocatorConfig>())
1001+
Ptr = untagPointer(const_cast<void *>(Ptr));
1002+
void *Begin = getBlockBegin(Ptr, &Header);
1003+
if (allocatorSupportsMemoryTagging<AllocatorConfig>())
1004+
Begin = untagPointer(Begin);
1005+
return reinterpret_cast<uptr>(Begin);
1006+
}
1007+
9541008
private:
9551009
typedef typename PrimaryT::SizeClassMap SizeClassMap;
9561010

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

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,248 @@ TEST(ScudoCombinedTest, QuarantineDisabled) {
11521152
EXPECT_EQ(Stats.find("Stats: Quarantine"), std::string::npos);
11531153
}
11541154

1155+
struct UsableSizeClassConfig {
1156+
static const scudo::uptr NumBits = 1;
1157+
static const scudo::uptr MinSizeLog = 10;
1158+
static const scudo::uptr MidSizeLog = 10;
1159+
static const scudo::uptr MaxSizeLog = 13;
1160+
static const scudo::u16 MaxNumCachedHint = 8;
1161+
static const scudo::uptr MaxBytesCachedLog = 12;
1162+
static const scudo::uptr SizeDelta = 0;
1163+
};
1164+
1165+
struct TestExactUsableSizeConfig {
1166+
static const bool MaySupportMemoryTagging = false;
1167+
static const bool QuarantineDisabled = true;
1168+
1169+
template <class A> using TSDRegistryT = scudo::TSDRegistrySharedT<A, 1U, 1U>;
1170+
1171+
struct Primary {
1172+
// In order to properly test the usable size, this Primary config has
1173+
// four real size classes: 1024, 2048, 4096, 8192.
1174+
using SizeClassMap = scudo::FixedSizeClassMap<UsableSizeClassConfig>;
1175+
static const scudo::uptr RegionSizeLog = 21U;
1176+
static const scudo::s32 MinReleaseToOsIntervalMs = INT32_MIN;
1177+
static const scudo::s32 MaxReleaseToOsIntervalMs = INT32_MAX;
1178+
typedef scudo::uptr CompactPtrT;
1179+
static const scudo::uptr CompactPtrScale = 0;
1180+
static const bool EnableRandomOffset = true;
1181+
static const scudo::uptr MapSizeIncrement = 1UL << 18;
1182+
static const scudo::uptr GroupSizeLog = 18;
1183+
};
1184+
template <typename Config>
1185+
using PrimaryT = scudo::SizeClassAllocator64<Config>;
1186+
1187+
struct Secondary {
1188+
template <typename Config>
1189+
using CacheT = scudo::MapAllocatorNoCache<Config>;
1190+
};
1191+
1192+
template <typename Config> using SecondaryT = scudo::MapAllocator<Config>;
1193+
};
1194+
1195+
template <class AllocatorT> void VerifyExactUsableSize(AllocatorT &Allocator) {
1196+
// Scan through all sizes up to 10000 then some larger sizes.
1197+
for (scudo::uptr Size = 1; Size < 10000; Size++) {
1198+
void *P = Allocator.allocate(Size, Origin);
1199+
EXPECT_EQ(Size, Allocator.getUsableSize(P))
1200+
<< "Failed usable size at allocation size " << Size;
1201+
Allocator.deallocate(P, Origin);
1202+
}
1203+
1204+
// Verify that aligned allocations also return the exact size allocated.
1205+
const scudo::uptr AllocSize = 313;
1206+
for (scudo::uptr Align = 1; Align <= 8; Align++) {
1207+
void *P = Allocator.allocate(AllocSize, Origin, 1U << Align);
1208+
EXPECT_EQ(AllocSize, Allocator.getUsableSize(P))
1209+
<< "Failed usable size at allocation size " << AllocSize << " at align "
1210+
<< 1 << Align;
1211+
Allocator.deallocate(P, Origin);
1212+
}
1213+
1214+
// Verify an explicitly large allocations.
1215+
const scudo::uptr LargeAllocSize = 1000000;
1216+
void *P = Allocator.allocate(LargeAllocSize, Origin);
1217+
EXPECT_EQ(LargeAllocSize, Allocator.getUsableSize(P));
1218+
Allocator.deallocate(P, Origin);
1219+
1220+
// Now do it for aligned allocations for large allocations.
1221+
for (scudo::uptr Align = 1; Align <= 8; Align++) {
1222+
void *P = Allocator.allocate(LargeAllocSize, Origin, 1U << Align);
1223+
EXPECT_EQ(LargeAllocSize, Allocator.getUsableSize(P))
1224+
<< "Failed usable size at allocation size " << AllocSize << " at align "
1225+
<< 1 << Align;
1226+
Allocator.deallocate(P, Origin);
1227+
}
1228+
}
1229+
1230+
template <class AllocatorT>
1231+
void VerifyIterateOverUsableSize(AllocatorT &Allocator) {
1232+
// This will not verify if the size is the exact size or the size of the
1233+
// size class. Instead verify that the size matches the usable size and
1234+
// assume the other tests have verified getUsableSize.
1235+
std::unordered_map<void *, size_t> Pointers;
1236+
Pointers.insert({Allocator.allocate(128, Origin), 0U});
1237+
Pointers.insert({Allocator.allocate(128, Origin, 32), 0U});
1238+
Pointers.insert({Allocator.allocate(2000, Origin), 0U});
1239+
Pointers.insert({Allocator.allocate(2000, Origin, 64), 0U});
1240+
Pointers.insert({Allocator.allocate(8000, Origin), 0U});
1241+
Pointers.insert({Allocator.allocate(8000, Origin, 128), 0U});
1242+
Pointers.insert({Allocator.allocate(2000205, Origin), 0U});
1243+
Pointers.insert({Allocator.allocate(2000205, Origin, 128), 0U});
1244+
Pointers.insert({Allocator.allocate(2000205, Origin, 256), 0U});
1245+
1246+
Allocator.disable();
1247+
Allocator.iterateOverChunks(
1248+
0, static_cast<scudo::uptr>(SCUDO_MMAP_RANGE_SIZE - 1),
1249+
[](uintptr_t Base, size_t Size, void *Arg) {
1250+
std::unordered_map<void *, size_t> *Pointers =
1251+
reinterpret_cast<std::unordered_map<void *, size_t> *>(Arg);
1252+
(*Pointers)[reinterpret_cast<void *>(Base)] = Size;
1253+
},
1254+
reinterpret_cast<void *>(&Pointers));
1255+
Allocator.enable();
1256+
1257+
for (auto [Ptr, IterateSize] : Pointers) {
1258+
EXPECT_NE(0U, IterateSize)
1259+
<< "Pointer " << Ptr << " not found in iterateOverChunks call.";
1260+
EXPECT_EQ(IterateSize, Allocator.getUsableSize(Ptr))
1261+
<< "Pointer " << Ptr
1262+
<< " mismatch between iterate size and usable size.";
1263+
Allocator.deallocate(Ptr, Origin);
1264+
}
1265+
}
1266+
1267+
TEST(ScudoCombinedTest, ExactUsableSize) {
1268+
using AllocatorT = scudo::Allocator<TestExactUsableSizeConfig>;
1269+
auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
1270+
1271+
VerifyExactUsableSize<AllocatorT>(*Allocator);
1272+
VerifyIterateOverUsableSize<AllocatorT>(*Allocator);
1273+
}
1274+
1275+
struct TestExactUsableSizeMTEConfig : TestExactUsableSizeConfig {
1276+
static const bool MaySupportMemoryTagging = true;
1277+
};
1278+
1279+
TEST(ScudoCombinedTest, ExactUsableSizeMTE) {
1280+
if (!scudo::archSupportsMemoryTagging() ||
1281+
!scudo::systemDetectsMemoryTagFaultsTestOnly())
1282+
TEST_SKIP("Only supported on systems that can enable MTE.");
1283+
1284+
scudo::enableSystemMemoryTaggingTestOnly();
1285+
1286+
using AllocatorT = scudo::Allocator<TestExactUsableSizeMTEConfig>;
1287+
auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
1288+
1289+
VerifyExactUsableSize<AllocatorT>(*Allocator);
1290+
VerifyIterateOverUsableSize<AllocatorT>(*Allocator);
1291+
}
1292+
1293+
template <class AllocatorT>
1294+
void VerifyUsableSizePrimary(AllocatorT &Allocator) {
1295+
std::vector<scudo::uptr> SizeClasses = {1024U, 2048U, 4096U, 8192U};
1296+
for (size_t I = 0; I < SizeClasses.size(); I++) {
1297+
scudo::uptr SizeClass = SizeClasses[I];
1298+
scudo::uptr StartSize;
1299+
if (I == 0)
1300+
StartSize = 1;
1301+
else
1302+
StartSize = SizeClasses[I - 1];
1303+
scudo::uptr UsableSize = SizeClass - scudo::Chunk::getHeaderSize();
1304+
for (scudo::uptr Size = StartSize; Size < UsableSize; Size++) {
1305+
void *P = Allocator.allocate(Size, Origin);
1306+
EXPECT_EQ(UsableSize, Allocator.getUsableSize(P))
1307+
<< "Failed usable size at allocation size " << Size
1308+
<< " for size class " << SizeClass;
1309+
memset(P, 0xff, UsableSize);
1310+
EXPECT_EQ(Allocator.getBlockBeginTestOnly(P) + SizeClass,
1311+
reinterpret_cast<scudo::uptr>(P) + UsableSize);
1312+
Allocator.deallocate(P, Origin);
1313+
}
1314+
1315+
StartSize = UsableSize + 1;
1316+
}
1317+
1318+
std::vector<scudo::uptr> Alignments = {32U, 128U};
1319+
for (size_t I = 0; I < SizeClasses.size(); I++) {
1320+
scudo::uptr SizeClass = SizeClasses[I];
1321+
scudo::uptr AllocSize;
1322+
if (I == 0)
1323+
AllocSize = 1;
1324+
else
1325+
AllocSize = SizeClasses[I - 1] + 1;
1326+
1327+
for (auto Alignment : Alignments) {
1328+
void *P = Allocator.allocate(AllocSize, Origin, Alignment);
1329+
scudo::uptr UsableSize = Allocator.getUsableSize(P);
1330+
memset(P, 0xff, UsableSize);
1331+
EXPECT_EQ(Allocator.getBlockBeginTestOnly(P) + SizeClass,
1332+
reinterpret_cast<scudo::uptr>(P) + UsableSize)
1333+
<< "Failed usable size at allocation size " << AllocSize
1334+
<< " for size class " << SizeClass << " at alignment " << Alignment;
1335+
Allocator.deallocate(P, Origin);
1336+
}
1337+
}
1338+
}
1339+
1340+
template <class AllocatorT>
1341+
void VerifyUsableSizeSecondary(AllocatorT &Allocator) {
1342+
const scudo::uptr LargeAllocSize = 996780;
1343+
const scudo::uptr PageSize = scudo::getPageSizeCached();
1344+
void *P = Allocator.allocate(LargeAllocSize, Origin);
1345+
scudo::uptr UsableSize = Allocator.getUsableSize(P);
1346+
memset(P, 0xff, UsableSize);
1347+
// Assumes that the secondary always rounds up allocations to a page boundary.
1348+
EXPECT_EQ(scudo::roundUp(reinterpret_cast<scudo::uptr>(P) + LargeAllocSize,
1349+
PageSize),
1350+
reinterpret_cast<scudo::uptr>(P) + UsableSize);
1351+
Allocator.deallocate(P, Origin);
1352+
1353+
// Check aligned allocations now.
1354+
for (scudo::uptr Alignment = 1; Alignment <= 8; Alignment++) {
1355+
void *P = Allocator.allocate(LargeAllocSize, Origin, 1U << Alignment);
1356+
scudo::uptr UsableSize = Allocator.getUsableSize(P);
1357+
EXPECT_EQ(scudo::roundUp(reinterpret_cast<scudo::uptr>(P) + LargeAllocSize,
1358+
PageSize),
1359+
reinterpret_cast<scudo::uptr>(P) + UsableSize)
1360+
<< "Failed usable size at allocation size " << LargeAllocSize
1361+
<< " at alignment " << Alignment;
1362+
Allocator.deallocate(P, Origin);
1363+
}
1364+
}
1365+
1366+
struct TestFullUsableSizeConfig : TestExactUsableSizeConfig {
1367+
static const bool ExactUsableSize = false;
1368+
};
1369+
1370+
TEST(ScudoCombinedTest, FullUsableSize) {
1371+
using AllocatorT = scudo::Allocator<TestFullUsableSizeConfig>;
1372+
auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
1373+
1374+
VerifyUsableSizePrimary<AllocatorT>(*Allocator);
1375+
VerifyUsableSizeSecondary<AllocatorT>(*Allocator);
1376+
VerifyIterateOverUsableSize<AllocatorT>(*Allocator);
1377+
}
1378+
1379+
struct TestFullUsableSizeMTEConfig : TestFullUsableSizeConfig {
1380+
static const bool MaySupportMemoryTagging = true;
1381+
};
1382+
1383+
TEST(ScudoCombinedTest, FullUsableSizeMTE) {
1384+
if (!scudo::archSupportsMemoryTagging() ||
1385+
!scudo::systemDetectsMemoryTagFaultsTestOnly())
1386+
TEST_SKIP("Only supported on systems that can enable MTE.");
1387+
1388+
scudo::enableSystemMemoryTaggingTestOnly();
1389+
1390+
using AllocatorT = scudo::Allocator<TestFullUsableSizeMTEConfig>;
1391+
auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
1392+
1393+
// When MTE is enabled, you get exact sizes.
1394+
VerifyExactUsableSize<AllocatorT>(*Allocator);
1395+
VerifyIterateOverUsableSize<AllocatorT>(*Allocator);
1396+
}
11551397
// Verify that no special quarantine blocks appear in iterateOverChunks.
11561398
TEST(ScudoCombinedTest, QuarantineIterateOverChunks) {
11571399
using AllocatorT = TestAllocator<TestQuarantineConfig>;

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,13 @@ TEST_F(ScudoWrappersCTest, MallocInfo) {
588588
EXPECT_EQ(errno, 0);
589589
fclose(F);
590590
EXPECT_EQ(strncmp(Buffer, "<malloc version=\"scudo-", 23), 0);
591-
EXPECT_NE(nullptr, strstr(Buffer, "<alloc size=\"1234\" count=\""));
592-
EXPECT_NE(nullptr, strstr(Buffer, "<alloc size=\"4321\" count=\""));
591+
std::string expected;
592+
expected =
593+
"<alloc size=\"" + std::to_string(malloc_usable_size(P1)) + "\" count=\"";
594+
EXPECT_NE(nullptr, strstr(Buffer, expected.c_str()));
595+
expected =
596+
"<alloc size=\"" + std::to_string(malloc_usable_size(P2)) + "\" count=\"";
597+
EXPECT_NE(nullptr, strstr(Buffer, expected.c_str()));
593598

594599
free(P1);
595600
free(P2);

0 commit comments

Comments
 (0)