1 //===-- primary64.h ---------------------------------------------*- C++ -*-===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #ifndef SCUDO_PRIMARY64_H_ 10 #define SCUDO_PRIMARY64_H_ 11 12 #include "bytemap.h" 13 #include "common.h" 14 #include "list.h" 15 #include "local_cache.h" 16 #include "mem_map.h" 17 #include "memtag.h" 18 #include "options.h" 19 #include "release.h" 20 #include "stats.h" 21 #include "string_utils.h" 22 #include "thread_annotations.h" 23 24 namespace scudo { 25 26 // SizeClassAllocator64 is an allocator tuned for 64-bit address space. 27 // 28 // It starts by reserving NumClasses * 2^RegionSizeLog bytes, equally divided in 29 // Regions, specific to each size class. Note that the base of that mapping is 30 // random (based to the platform specific map() capabilities). If 31 // PrimaryEnableRandomOffset is set, each Region actually starts at a random 32 // offset from its base. 33 // 34 // Regions are mapped incrementally on demand to fulfill allocation requests, 35 // those mappings being split into equally sized Blocks based on the size class 36 // they belong to. The Blocks created are shuffled to prevent predictable 37 // address patterns (the predictability increases with the size of the Blocks). 38 // 39 // The 1st Region (for size class 0) holds the TransferBatches. This is a 40 // structure used to transfer arrays of available pointers from the class size 41 // freelist to the thread specific freelist, and back. 42 // 43 // The memory used by this allocator is never unmapped, but can be partially 44 // released if the platform allows for it. 45 46 template <typename Config> class SizeClassAllocator64 { 47 public: 48 typedef typename Config::Primary::CompactPtrT CompactPtrT; 49 typedef typename Config::Primary::SizeClassMap SizeClassMap; 50 static const uptr CompactPtrScale = Config::Primary::CompactPtrScale; 51 static const uptr GroupSizeLog = Config::Primary::GroupSizeLog; 52 static const uptr GroupScale = GroupSizeLog - CompactPtrScale; 53 typedef SizeClassAllocator64<Config> ThisT; 54 typedef SizeClassAllocatorLocalCache<ThisT> CacheT; 55 typedef typename CacheT::TransferBatch TransferBatch; 56 typedef typename CacheT::BatchGroup BatchGroup; 57 58 static uptr getSizeByClassId(uptr ClassId) { 59 return (ClassId == SizeClassMap::BatchClassId) 60 ? roundUp(sizeof(TransferBatch), 1U << CompactPtrScale) 61 : SizeClassMap::getSizeByClassId(ClassId); 62 } 63 64 static bool canAllocate(uptr Size) { return Size <= SizeClassMap::MaxSize; } 65 66 void init(s32 ReleaseToOsInterval) NO_THREAD_SAFETY_ANALYSIS { 67 DCHECK(isAligned(reinterpret_cast<uptr>(this), alignof(ThisT))); 68 69 const uptr PageSize = getPageSizeCached(); 70 const uptr GroupSize = (1U << GroupSizeLog); 71 const uptr PagesInGroup = GroupSize / PageSize; 72 const uptr MinSizeClass = getSizeByClassId(1); 73 // When trying to release pages back to memory, visiting smaller size 74 // classes is expensive. Therefore, we only try to release smaller size 75 // classes when the amount of free blocks goes over a certain threshold (See 76 // the comment in releaseToOSMaybe() for more details). For example, for 77 // size class 32, we only do the release when the size of free blocks is 78 // greater than 97% of pages in a group. However, this may introduce another 79 // issue that if the number of free blocks is bouncing between 97% ~ 100%. 80 // Which means we may try many page releases but only release very few of 81 // them (less than 3% in a group). Even though we have 82 // `&ReleaseToOsIntervalMs` which slightly reduce the frequency of these 83 // calls but it will be better to have another guard to mitigate this issue. 84 // 85 // Here we add another constraint on the minimum size requirement. The 86 // constraint is determined by the size of in-use blocks in the minimal size 87 // class. Take size class 32 as an example, 88 // 89 // +- one memory group -+ 90 // +----------------------+------+ 91 // | 97% of free blocks | | 92 // +----------------------+------+ 93 // \ / 94 // 3% in-use blocks 95 // 96 // * The release size threshold is 97%. 97 // 98 // The 3% size in a group is about 7 pages. For two consecutive 99 // releaseToOSMaybe(), we require the difference between `PushedBlocks` 100 // should be greater than 7 pages. This mitigates the page releasing 101 // thrashing which is caused by memory usage bouncing around the threshold. 102 // The smallest size class takes longest time to do the page release so we 103 // use its size of in-use blocks as a heuristic. 104 SmallerBlockReleasePageDelta = 105 PagesInGroup * (1 + MinSizeClass / 16U) / 100; 106 107 // Reserve the space required for the Primary. 108 CHECK(ReservedMemory.create(/*Addr=*/0U, PrimarySize, 109 "scudo:primary_reserve")); 110 PrimaryBase = ReservedMemory.getBase(); 111 DCHECK_NE(PrimaryBase, 0U); 112 113 u32 Seed; 114 const u64 Time = getMonotonicTimeFast(); 115 if (!getRandom(reinterpret_cast<void *>(&Seed), sizeof(Seed))) 116 Seed = static_cast<u32>(Time ^ (PrimaryBase >> 12)); 117 118 for (uptr I = 0; I < NumClasses; I++) { 119 RegionInfo *Region = getRegionInfo(I); 120 // The actual start of a region is offset by a random number of pages 121 // when PrimaryEnableRandomOffset is set. 122 Region->RegionBeg = 123 (PrimaryBase + (I << Config::Primary::RegionSizeLog)) + 124 (Config::Primary::EnableRandomOffset 125 ? ((getRandomModN(&Seed, 16) + 1) * PageSize) 126 : 0); 127 Region->RandState = getRandomU32(&Seed); 128 // Releasing small blocks is expensive, set a higher threshold to avoid 129 // frequent page releases. 130 if (isSmallBlock(getSizeByClassId(I))) 131 Region->TryReleaseThreshold = PageSize * SmallerBlockReleasePageDelta; 132 else 133 Region->TryReleaseThreshold = PageSize; 134 Region->ReleaseInfo.LastReleaseAtNs = Time; 135 136 Region->MemMapInfo.MemMap = ReservedMemory.dispatch( 137 PrimaryBase + (I << Config::Primary::RegionSizeLog), RegionSize); 138 CHECK(Region->MemMapInfo.MemMap.isAllocated()); 139 } 140 shuffle(RegionInfoArray, NumClasses, &Seed); 141 142 setOption(Option::ReleaseInterval, static_cast<sptr>(ReleaseToOsInterval)); 143 } 144 145 void unmapTestOnly() NO_THREAD_SAFETY_ANALYSIS { 146 for (uptr I = 0; I < NumClasses; I++) { 147 RegionInfo *Region = getRegionInfo(I); 148 *Region = {}; 149 } 150 if (PrimaryBase) 151 ReservedMemory.release(); 152 PrimaryBase = 0U; 153 } 154 155 // When all blocks are freed, it has to be the same size as `AllocatedUser`. 156 void verifyAllBlocksAreReleasedTestOnly() { 157 // `BatchGroup` and `TransferBatch` also use the blocks from BatchClass. 158 uptr BatchClassUsedInFreeLists = 0; 159 for (uptr I = 0; I < NumClasses; I++) { 160 // We have to count BatchClassUsedInFreeLists in other regions first. 161 if (I == SizeClassMap::BatchClassId) 162 continue; 163 RegionInfo *Region = getRegionInfo(I); 164 ScopedLock ML(Region->MMLock); 165 ScopedLock FL(Region->FLLock); 166 const uptr BlockSize = getSizeByClassId(I); 167 uptr TotalBlocks = 0; 168 for (BatchGroup &BG : Region->FreeListInfo.BlockList) { 169 // `BG::Batches` are `TransferBatches`. +1 for `BatchGroup`. 170 BatchClassUsedInFreeLists += BG.Batches.size() + 1; 171 for (const auto &It : BG.Batches) 172 TotalBlocks += It.getCount(); 173 } 174 175 DCHECK_EQ(TotalBlocks, Region->MemMapInfo.AllocatedUser / BlockSize); 176 DCHECK_EQ(Region->FreeListInfo.PushedBlocks, 177 Region->FreeListInfo.PoppedBlocks); 178 } 179 180 RegionInfo *Region = getRegionInfo(SizeClassMap::BatchClassId); 181 ScopedLock ML(Region->MMLock); 182 ScopedLock FL(Region->FLLock); 183 const uptr BlockSize = getSizeByClassId(SizeClassMap::BatchClassId); 184 uptr TotalBlocks = 0; 185 for (BatchGroup &BG : Region->FreeListInfo.BlockList) { 186 if (LIKELY(!BG.Batches.empty())) { 187 for (const auto &It : BG.Batches) 188 TotalBlocks += It.getCount(); 189 } else { 190 // `BatchGroup` with empty freelist doesn't have `TransferBatch` record 191 // itself. 192 ++TotalBlocks; 193 } 194 } 195 DCHECK_EQ(TotalBlocks + BatchClassUsedInFreeLists, 196 Region->MemMapInfo.AllocatedUser / BlockSize); 197 DCHECK_GE(Region->FreeListInfo.PoppedBlocks, 198 Region->FreeListInfo.PushedBlocks); 199 const uptr BlocksInUse = 200 Region->FreeListInfo.PoppedBlocks - Region->FreeListInfo.PushedBlocks; 201 DCHECK_EQ(BlocksInUse, BatchClassUsedInFreeLists); 202 } 203 204 TransferBatch *popBatch(CacheT *C, uptr ClassId) { 205 DCHECK_LT(ClassId, NumClasses); 206 RegionInfo *Region = getRegionInfo(ClassId); 207 208 { 209 ScopedLock L(Region->FLLock); 210 TransferBatch *B = popBatchImpl(C, ClassId, Region); 211 if (LIKELY(B)) 212 return B; 213 } 214 215 bool PrintStats = false; 216 TransferBatch *B = nullptr; 217 218 while (true) { 219 // When two threads compete for `Region->MMLock`, we only want one of them 220 // does the populateFreeListAndPopBatch(). To avoid both of them doing 221 // that, always check the freelist before mapping new pages. 222 // 223 // TODO(chiahungduan): Use a condition variable so that we don't need to 224 // hold `Region->MMLock` here. 225 ScopedLock ML(Region->MMLock); 226 { 227 ScopedLock FL(Region->FLLock); 228 B = popBatchImpl(C, ClassId, Region); 229 if (LIKELY(B)) 230 return B; 231 } 232 233 const bool RegionIsExhausted = Region->Exhausted; 234 if (!RegionIsExhausted) 235 B = populateFreeListAndPopBatch(C, ClassId, Region); 236 PrintStats = !RegionIsExhausted && Region->Exhausted; 237 break; 238 } 239 240 // Note that `getStats()` requires locking each region so we can't call it 241 // while locking the Region->Mutex in the above. 242 if (UNLIKELY(PrintStats)) { 243 ScopedString Str; 244 getStats(&Str); 245 Str.append( 246 "Scudo OOM: The process has exhausted %zuM for size class %zu.\n", 247 RegionSize >> 20, getSizeByClassId(ClassId)); 248 Str.output(); 249 250 // Theoretically, BatchClass shouldn't be used up. Abort immediately when 251 // it happens. 252 if (ClassId == SizeClassMap::BatchClassId) 253 reportOutOfBatchClass(); 254 } 255 256 return B; 257 } 258 259 // Push the array of free blocks to the designated batch group. 260 void pushBlocks(CacheT *C, uptr ClassId, CompactPtrT *Array, u32 Size) { 261 DCHECK_LT(ClassId, NumClasses); 262 DCHECK_GT(Size, 0); 263 264 RegionInfo *Region = getRegionInfo(ClassId); 265 if (ClassId == SizeClassMap::BatchClassId) { 266 ScopedLock L(Region->FLLock); 267 pushBatchClassBlocks(Region, Array, Size); 268 return; 269 } 270 271 // TODO(chiahungduan): Consider not doing grouping if the group size is not 272 // greater than the block size with a certain scale. 273 274 // Sort the blocks so that blocks belonging to the same group can be pushed 275 // together. 276 bool SameGroup = true; 277 for (u32 I = 1; I < Size; ++I) { 278 if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I])) 279 SameGroup = false; 280 CompactPtrT Cur = Array[I]; 281 u32 J = I; 282 while (J > 0 && compactPtrGroup(Cur) < compactPtrGroup(Array[J - 1])) { 283 Array[J] = Array[J - 1]; 284 --J; 285 } 286 Array[J] = Cur; 287 } 288 289 { 290 ScopedLock L(Region->FLLock); 291 pushBlocksImpl(C, ClassId, Region, Array, Size, SameGroup); 292 } 293 } 294 295 void disable() NO_THREAD_SAFETY_ANALYSIS { 296 // The BatchClassId must be locked last since other classes can use it. 297 for (sptr I = static_cast<sptr>(NumClasses) - 1; I >= 0; I--) { 298 if (static_cast<uptr>(I) == SizeClassMap::BatchClassId) 299 continue; 300 getRegionInfo(static_cast<uptr>(I))->MMLock.lock(); 301 getRegionInfo(static_cast<uptr>(I))->FLLock.lock(); 302 } 303 getRegionInfo(SizeClassMap::BatchClassId)->MMLock.lock(); 304 getRegionInfo(SizeClassMap::BatchClassId)->FLLock.lock(); 305 } 306 307 void enable() NO_THREAD_SAFETY_ANALYSIS { 308 getRegionInfo(SizeClassMap::BatchClassId)->FLLock.unlock(); 309 getRegionInfo(SizeClassMap::BatchClassId)->MMLock.unlock(); 310 for (uptr I = 0; I < NumClasses; I++) { 311 if (I == SizeClassMap::BatchClassId) 312 continue; 313 getRegionInfo(I)->FLLock.unlock(); 314 getRegionInfo(I)->MMLock.unlock(); 315 } 316 } 317 318 template <typename F> void iterateOverBlocks(F Callback) { 319 for (uptr I = 0; I < NumClasses; I++) { 320 if (I == SizeClassMap::BatchClassId) 321 continue; 322 RegionInfo *Region = getRegionInfo(I); 323 // TODO: The call of `iterateOverBlocks` requires disabling 324 // SizeClassAllocator64. We may consider locking each region on demand 325 // only. 326 Region->FLLock.assertHeld(); 327 Region->MMLock.assertHeld(); 328 const uptr BlockSize = getSizeByClassId(I); 329 const uptr From = Region->RegionBeg; 330 const uptr To = From + Region->MemMapInfo.AllocatedUser; 331 for (uptr Block = From; Block < To; Block += BlockSize) 332 Callback(Block); 333 } 334 } 335 336 void getStats(ScopedString *Str) { 337 // TODO(kostyak): get the RSS per region. 338 uptr TotalMapped = 0; 339 uptr PoppedBlocks = 0; 340 uptr PushedBlocks = 0; 341 for (uptr I = 0; I < NumClasses; I++) { 342 RegionInfo *Region = getRegionInfo(I); 343 { 344 ScopedLock L(Region->MMLock); 345 TotalMapped += Region->MemMapInfo.MappedUser; 346 } 347 { 348 ScopedLock L(Region->FLLock); 349 PoppedBlocks += Region->FreeListInfo.PoppedBlocks; 350 PushedBlocks += Region->FreeListInfo.PushedBlocks; 351 } 352 } 353 Str->append("Stats: SizeClassAllocator64: %zuM mapped (%uM rss) in %zu " 354 "allocations; remains %zu\n", 355 TotalMapped >> 20, 0U, PoppedBlocks, 356 PoppedBlocks - PushedBlocks); 357 358 for (uptr I = 0; I < NumClasses; I++) { 359 RegionInfo *Region = getRegionInfo(I); 360 ScopedLock L1(Region->MMLock); 361 ScopedLock L2(Region->FLLock); 362 getStats(Str, I, Region); 363 } 364 } 365 366 bool setOption(Option O, sptr Value) { 367 if (O == Option::ReleaseInterval) { 368 const s32 Interval = Max(Min(static_cast<s32>(Value), 369 Config::Primary::MaxReleaseToOsIntervalMs), 370 Config::Primary::MinReleaseToOsIntervalMs); 371 atomic_store_relaxed(&ReleaseToOsIntervalMs, Interval); 372 return true; 373 } 374 // Not supported by the Primary, but not an error either. 375 return true; 376 } 377 378 uptr tryReleaseToOS(uptr ClassId, ReleaseToOS ReleaseType) { 379 RegionInfo *Region = getRegionInfo(ClassId); 380 // Note that the tryLock() may fail spuriously, given that it should rarely 381 // happen and page releasing is fine to skip, we don't take certain 382 // approaches to ensure one page release is done. 383 if (Region->MMLock.tryLock()) { 384 uptr BytesReleased = releaseToOSMaybe(Region, ClassId, ReleaseType); 385 Region->MMLock.unlock(); 386 return BytesReleased; 387 } 388 return 0; 389 } 390 391 uptr releaseToOS(ReleaseToOS ReleaseType) { 392 uptr TotalReleasedBytes = 0; 393 for (uptr I = 0; I < NumClasses; I++) { 394 if (I == SizeClassMap::BatchClassId) 395 continue; 396 RegionInfo *Region = getRegionInfo(I); 397 ScopedLock L(Region->MMLock); 398 TotalReleasedBytes += releaseToOSMaybe(Region, I, ReleaseType); 399 } 400 return TotalReleasedBytes; 401 } 402 403 const char *getRegionInfoArrayAddress() const { 404 return reinterpret_cast<const char *>(RegionInfoArray); 405 } 406 407 static uptr getRegionInfoArraySize() { return sizeof(RegionInfoArray); } 408 409 uptr getCompactPtrBaseByClassId(uptr ClassId) { 410 return getRegionInfo(ClassId)->RegionBeg; 411 } 412 413 CompactPtrT compactPtr(uptr ClassId, uptr Ptr) { 414 DCHECK_LE(ClassId, SizeClassMap::LargestClassId); 415 return compactPtrInternal(getCompactPtrBaseByClassId(ClassId), Ptr); 416 } 417 418 void *decompactPtr(uptr ClassId, CompactPtrT CompactPtr) { 419 DCHECK_LE(ClassId, SizeClassMap::LargestClassId); 420 return reinterpret_cast<void *>( 421 decompactPtrInternal(getCompactPtrBaseByClassId(ClassId), CompactPtr)); 422 } 423 424 static BlockInfo findNearestBlock(const char *RegionInfoData, 425 uptr Ptr) NO_THREAD_SAFETY_ANALYSIS { 426 const RegionInfo *RegionInfoArray = 427 reinterpret_cast<const RegionInfo *>(RegionInfoData); 428 429 uptr ClassId; 430 uptr MinDistance = -1UL; 431 for (uptr I = 0; I != NumClasses; ++I) { 432 if (I == SizeClassMap::BatchClassId) 433 continue; 434 uptr Begin = RegionInfoArray[I].RegionBeg; 435 // TODO(chiahungduan): In fact, We need to lock the RegionInfo::MMLock. 436 // However, the RegionInfoData is passed with const qualifier and lock the 437 // mutex requires modifying RegionInfoData, which means we need to remove 438 // the const qualifier. This may lead to another undefined behavior (The 439 // first one is accessing `AllocatedUser` without locking. It's better to 440 // pass `RegionInfoData` as `void *` then we can lock the mutex properly. 441 uptr End = Begin + RegionInfoArray[I].MemMapInfo.AllocatedUser; 442 if (Begin > End || End - Begin < SizeClassMap::getSizeByClassId(I)) 443 continue; 444 uptr RegionDistance; 445 if (Begin <= Ptr) { 446 if (Ptr < End) 447 RegionDistance = 0; 448 else 449 RegionDistance = Ptr - End; 450 } else { 451 RegionDistance = Begin - Ptr; 452 } 453 454 if (RegionDistance < MinDistance) { 455 MinDistance = RegionDistance; 456 ClassId = I; 457 } 458 } 459 460 BlockInfo B = {}; 461 if (MinDistance <= 8192) { 462 B.RegionBegin = RegionInfoArray[ClassId].RegionBeg; 463 B.RegionEnd = 464 B.RegionBegin + RegionInfoArray[ClassId].MemMapInfo.AllocatedUser; 465 B.BlockSize = SizeClassMap::getSizeByClassId(ClassId); 466 B.BlockBegin = 467 B.RegionBegin + uptr(sptr(Ptr - B.RegionBegin) / sptr(B.BlockSize) * 468 sptr(B.BlockSize)); 469 while (B.BlockBegin < B.RegionBegin) 470 B.BlockBegin += B.BlockSize; 471 while (B.RegionEnd < B.BlockBegin + B.BlockSize) 472 B.BlockBegin -= B.BlockSize; 473 } 474 return B; 475 } 476 477 AtomicOptions Options; 478 479 private: 480 static const uptr RegionSize = 1UL << Config::Primary::RegionSizeLog; 481 static const uptr NumClasses = SizeClassMap::NumClasses; 482 static const uptr PrimarySize = RegionSize * NumClasses; 483 484 static const uptr MapSizeIncrement = Config::Primary::MapSizeIncrement; 485 // Fill at most this number of batches from the newly map'd memory. 486 static const u32 MaxNumBatches = SCUDO_ANDROID ? 4U : 8U; 487 488 struct ReleaseToOsInfo { 489 uptr BytesInFreeListAtLastCheckpoint; 490 uptr RangesReleased; 491 uptr LastReleasedBytes; 492 u64 LastReleaseAtNs; 493 }; 494 495 struct BlocksInfo { 496 SinglyLinkedList<BatchGroup> BlockList = {}; 497 uptr PoppedBlocks = 0; 498 uptr PushedBlocks = 0; 499 }; 500 501 struct PagesInfo { 502 MemMapT MemMap = {}; 503 // Bytes mapped for user memory. 504 uptr MappedUser = 0; 505 // Bytes allocated for user memory. 506 uptr AllocatedUser = 0; 507 }; 508 509 struct UnpaddedRegionInfo { 510 // Mutex for operations on freelist 511 HybridMutex FLLock; 512 // Mutex for memmap operations 513 HybridMutex MMLock ACQUIRED_BEFORE(FLLock); 514 // `RegionBeg` is initialized before thread creation and won't be changed. 515 uptr RegionBeg = 0; 516 u32 RandState GUARDED_BY(MMLock) = 0; 517 BlocksInfo FreeListInfo GUARDED_BY(FLLock); 518 PagesInfo MemMapInfo GUARDED_BY(MMLock); 519 // The minimum size of pushed blocks to trigger page release. 520 uptr TryReleaseThreshold GUARDED_BY(MMLock) = 0; 521 ReleaseToOsInfo ReleaseInfo GUARDED_BY(MMLock) = {}; 522 bool Exhausted GUARDED_BY(MMLock) = false; 523 }; 524 struct RegionInfo : UnpaddedRegionInfo { 525 char Padding[SCUDO_CACHE_LINE_SIZE - 526 (sizeof(UnpaddedRegionInfo) % SCUDO_CACHE_LINE_SIZE)] = {}; 527 }; 528 static_assert(sizeof(RegionInfo) % SCUDO_CACHE_LINE_SIZE == 0, ""); 529 530 RegionInfo *getRegionInfo(uptr ClassId) { 531 DCHECK_LT(ClassId, NumClasses); 532 return &RegionInfoArray[ClassId]; 533 } 534 535 uptr getRegionBaseByClassId(uptr ClassId) { 536 return roundDown(getRegionInfo(ClassId)->RegionBeg - PrimaryBase, 537 RegionSize) + 538 PrimaryBase; 539 } 540 541 static CompactPtrT compactPtrInternal(uptr Base, uptr Ptr) { 542 return static_cast<CompactPtrT>((Ptr - Base) >> CompactPtrScale); 543 } 544 545 static uptr decompactPtrInternal(uptr Base, CompactPtrT CompactPtr) { 546 return Base + (static_cast<uptr>(CompactPtr) << CompactPtrScale); 547 } 548 549 static uptr compactPtrGroup(CompactPtrT CompactPtr) { 550 const uptr Mask = (static_cast<uptr>(1) << GroupScale) - 1; 551 return static_cast<uptr>(CompactPtr) & ~Mask; 552 } 553 static uptr decompactGroupBase(uptr Base, uptr CompactPtrGroupBase) { 554 DCHECK_EQ(CompactPtrGroupBase % (static_cast<uptr>(1) << (GroupScale)), 0U); 555 return Base + (CompactPtrGroupBase << CompactPtrScale); 556 } 557 558 ALWAYS_INLINE static bool isSmallBlock(uptr BlockSize) { 559 const uptr PageSize = getPageSizeCached(); 560 return BlockSize < PageSize / 16U; 561 } 562 563 ALWAYS_INLINE static bool isLargeBlock(uptr BlockSize) { 564 const uptr PageSize = getPageSizeCached(); 565 return BlockSize > PageSize; 566 } 567 568 void pushBatchClassBlocks(RegionInfo *Region, CompactPtrT *Array, u32 Size) 569 REQUIRES(Region->FLLock) { 570 DCHECK_EQ(Region, getRegionInfo(SizeClassMap::BatchClassId)); 571 572 // Free blocks are recorded by TransferBatch in freelist for all 573 // size-classes. In addition, TransferBatch is allocated from BatchClassId. 574 // In order not to use additional block to record the free blocks in 575 // BatchClassId, they are self-contained. I.e., A TransferBatch records the 576 // block address of itself. See the figure below: 577 // 578 // TransferBatch at 0xABCD 579 // +----------------------------+ 580 // | Free blocks' addr | 581 // | +------+------+------+ | 582 // | |0xABCD|... |... | | 583 // | +------+------+------+ | 584 // +----------------------------+ 585 // 586 // When we allocate all the free blocks in the TransferBatch, the block used 587 // by TransferBatch is also free for use. We don't need to recycle the 588 // TransferBatch. Note that the correctness is maintained by the invariant, 589 // 590 // The unit of each popBatch() request is entire TransferBatch. Return 591 // part of the blocks in a TransferBatch is invalid. 592 // 593 // This ensures that TransferBatch won't leak the address itself while it's 594 // still holding other valid data. 595 // 596 // Besides, BatchGroup is also allocated from BatchClassId and has its 597 // address recorded in the TransferBatch too. To maintain the correctness, 598 // 599 // The address of BatchGroup is always recorded in the last TransferBatch 600 // in the freelist (also imply that the freelist should only be 601 // updated with push_front). Once the last TransferBatch is popped, 602 // the block used by BatchGroup is also free for use. 603 // 604 // With this approach, the blocks used by BatchGroup and TransferBatch are 605 // reusable and don't need additional space for them. 606 607 Region->FreeListInfo.PushedBlocks += Size; 608 BatchGroup *BG = Region->FreeListInfo.BlockList.front(); 609 610 if (BG == nullptr) { 611 // Construct `BatchGroup` on the last element. 612 BG = reinterpret_cast<BatchGroup *>( 613 decompactPtr(SizeClassMap::BatchClassId, Array[Size - 1])); 614 --Size; 615 BG->Batches.clear(); 616 // BatchClass hasn't enabled memory group. Use `0` to indicate there's no 617 // memory group here. 618 BG->CompactPtrGroupBase = 0; 619 // `BG` is also the block of BatchClassId. Note that this is different 620 // from `CreateGroup` in `pushBlocksImpl` 621 BG->PushedBlocks = 1; 622 BG->BytesInBGAtLastCheckpoint = 0; 623 BG->MaxCachedPerBatch = TransferBatch::getMaxCached( 624 getSizeByClassId(SizeClassMap::BatchClassId)); 625 626 Region->FreeListInfo.BlockList.push_front(BG); 627 } 628 629 if (UNLIKELY(Size == 0)) 630 return; 631 632 // This happens under 2 cases. 633 // 1. just allocated a new `BatchGroup`. 634 // 2. Only 1 block is pushed when the freelist is empty. 635 if (BG->Batches.empty()) { 636 // Construct the `TransferBatch` on the last element. 637 TransferBatch *TB = reinterpret_cast<TransferBatch *>( 638 decompactPtr(SizeClassMap::BatchClassId, Array[Size - 1])); 639 TB->clear(); 640 // As mentioned above, addresses of `TransferBatch` and `BatchGroup` are 641 // recorded in the TransferBatch. 642 TB->add(Array[Size - 1]); 643 TB->add( 644 compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(BG))); 645 --Size; 646 DCHECK_EQ(BG->PushedBlocks, 1U); 647 // `TB` is also the block of BatchClassId. 648 BG->PushedBlocks += 1; 649 BG->Batches.push_front(TB); 650 } 651 652 TransferBatch *CurBatch = BG->Batches.front(); 653 DCHECK_NE(CurBatch, nullptr); 654 655 for (u32 I = 0; I < Size;) { 656 u16 UnusedSlots = 657 static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount()); 658 if (UnusedSlots == 0) { 659 CurBatch = reinterpret_cast<TransferBatch *>( 660 decompactPtr(SizeClassMap::BatchClassId, Array[I])); 661 CurBatch->clear(); 662 // Self-contained 663 CurBatch->add(Array[I]); 664 ++I; 665 // TODO(chiahungduan): Avoid the use of push_back() in `Batches` of 666 // BatchClassId. 667 BG->Batches.push_front(CurBatch); 668 UnusedSlots = static_cast<u16>(BG->MaxCachedPerBatch - 1); 669 } 670 // `UnusedSlots` is u16 so the result will be also fit in u16. 671 const u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I)); 672 CurBatch->appendFromArray(&Array[I], AppendSize); 673 I += AppendSize; 674 } 675 676 BG->PushedBlocks += Size; 677 } 678 679 // Push the blocks to their batch group. The layout will be like, 680 // 681 // FreeListInfo.BlockList - > BG -> BG -> BG 682 // | | | 683 // v v v 684 // TB TB TB 685 // | 686 // v 687 // TB 688 // 689 // Each BlockGroup(BG) will associate with unique group id and the free blocks 690 // are managed by a list of TransferBatch(TB). To reduce the time of inserting 691 // blocks, BGs are sorted and the input `Array` are supposed to be sorted so 692 // that we can get better performance of maintaining sorted property. 693 // Use `SameGroup=true` to indicate that all blocks in the array are from the 694 // same group then we will skip checking the group id of each block. 695 void pushBlocksImpl(CacheT *C, uptr ClassId, RegionInfo *Region, 696 CompactPtrT *Array, u32 Size, bool SameGroup = false) 697 REQUIRES(Region->FLLock) { 698 DCHECK_NE(ClassId, SizeClassMap::BatchClassId); 699 DCHECK_GT(Size, 0U); 700 701 auto CreateGroup = [&](uptr CompactPtrGroupBase) { 702 BatchGroup *BG = C->createGroup(); 703 BG->Batches.clear(); 704 TransferBatch *TB = C->createBatch(ClassId, nullptr); 705 TB->clear(); 706 707 BG->CompactPtrGroupBase = CompactPtrGroupBase; 708 BG->Batches.push_front(TB); 709 BG->PushedBlocks = 0; 710 BG->BytesInBGAtLastCheckpoint = 0; 711 BG->MaxCachedPerBatch = 712 TransferBatch::getMaxCached(getSizeByClassId(ClassId)); 713 714 return BG; 715 }; 716 717 auto InsertBlocks = [&](BatchGroup *BG, CompactPtrT *Array, u32 Size) { 718 SinglyLinkedList<TransferBatch> &Batches = BG->Batches; 719 TransferBatch *CurBatch = Batches.front(); 720 DCHECK_NE(CurBatch, nullptr); 721 722 for (u32 I = 0; I < Size;) { 723 DCHECK_GE(BG->MaxCachedPerBatch, CurBatch->getCount()); 724 u16 UnusedSlots = 725 static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount()); 726 if (UnusedSlots == 0) { 727 CurBatch = C->createBatch( 728 ClassId, 729 reinterpret_cast<void *>(decompactPtr(ClassId, Array[I]))); 730 CurBatch->clear(); 731 Batches.push_front(CurBatch); 732 UnusedSlots = BG->MaxCachedPerBatch; 733 } 734 // `UnusedSlots` is u16 so the result will be also fit in u16. 735 u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I)); 736 CurBatch->appendFromArray(&Array[I], AppendSize); 737 I += AppendSize; 738 } 739 740 BG->PushedBlocks += Size; 741 }; 742 743 Region->FreeListInfo.PushedBlocks += Size; 744 BatchGroup *Cur = Region->FreeListInfo.BlockList.front(); 745 746 if (ClassId == SizeClassMap::BatchClassId) { 747 if (Cur == nullptr) { 748 // Don't need to classify BatchClassId. 749 Cur = CreateGroup(/*CompactPtrGroupBase=*/0); 750 Region->FreeListInfo.BlockList.push_front(Cur); 751 } 752 InsertBlocks(Cur, Array, Size); 753 return; 754 } 755 756 // In the following, `Cur` always points to the BatchGroup for blocks that 757 // will be pushed next. `Prev` is the element right before `Cur`. 758 BatchGroup *Prev = nullptr; 759 760 while (Cur != nullptr && 761 compactPtrGroup(Array[0]) > Cur->CompactPtrGroupBase) { 762 Prev = Cur; 763 Cur = Cur->Next; 764 } 765 766 if (Cur == nullptr || 767 compactPtrGroup(Array[0]) != Cur->CompactPtrGroupBase) { 768 Cur = CreateGroup(compactPtrGroup(Array[0])); 769 if (Prev == nullptr) 770 Region->FreeListInfo.BlockList.push_front(Cur); 771 else 772 Region->FreeListInfo.BlockList.insert(Prev, Cur); 773 } 774 775 // All the blocks are from the same group, just push without checking group 776 // id. 777 if (SameGroup) { 778 for (u32 I = 0; I < Size; ++I) 779 DCHECK_EQ(compactPtrGroup(Array[I]), Cur->CompactPtrGroupBase); 780 781 InsertBlocks(Cur, Array, Size); 782 return; 783 } 784 785 // The blocks are sorted by group id. Determine the segment of group and 786 // push them to their group together. 787 u32 Count = 1; 788 for (u32 I = 1; I < Size; ++I) { 789 if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I])) { 790 DCHECK_EQ(compactPtrGroup(Array[I - 1]), Cur->CompactPtrGroupBase); 791 InsertBlocks(Cur, Array + I - Count, Count); 792 793 while (Cur != nullptr && 794 compactPtrGroup(Array[I]) > Cur->CompactPtrGroupBase) { 795 Prev = Cur; 796 Cur = Cur->Next; 797 } 798 799 if (Cur == nullptr || 800 compactPtrGroup(Array[I]) != Cur->CompactPtrGroupBase) { 801 Cur = CreateGroup(compactPtrGroup(Array[I])); 802 DCHECK_NE(Prev, nullptr); 803 Region->FreeListInfo.BlockList.insert(Prev, Cur); 804 } 805 806 Count = 1; 807 } else { 808 ++Count; 809 } 810 } 811 812 InsertBlocks(Cur, Array + Size - Count, Count); 813 } 814 815 // Pop one TransferBatch from a BatchGroup. The BatchGroup with the smallest 816 // group id will be considered first. 817 // 818 // The region mutex needs to be held while calling this method. 819 TransferBatch *popBatchImpl(CacheT *C, uptr ClassId, RegionInfo *Region) 820 REQUIRES(Region->FLLock) { 821 if (Region->FreeListInfo.BlockList.empty()) 822 return nullptr; 823 824 SinglyLinkedList<TransferBatch> &Batches = 825 Region->FreeListInfo.BlockList.front()->Batches; 826 827 if (Batches.empty()) { 828 DCHECK_EQ(ClassId, SizeClassMap::BatchClassId); 829 BatchGroup *BG = Region->FreeListInfo.BlockList.front(); 830 Region->FreeListInfo.BlockList.pop_front(); 831 832 // Block used by `BatchGroup` is from BatchClassId. Turn the block into 833 // `TransferBatch` with single block. 834 TransferBatch *TB = reinterpret_cast<TransferBatch *>(BG); 835 TB->clear(); 836 TB->add( 837 compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(TB))); 838 Region->FreeListInfo.PoppedBlocks += 1; 839 return TB; 840 } 841 842 TransferBatch *B = Batches.front(); 843 Batches.pop_front(); 844 DCHECK_NE(B, nullptr); 845 DCHECK_GT(B->getCount(), 0U); 846 847 if (Batches.empty()) { 848 BatchGroup *BG = Region->FreeListInfo.BlockList.front(); 849 Region->FreeListInfo.BlockList.pop_front(); 850 851 // We don't keep BatchGroup with zero blocks to avoid empty-checking while 852 // allocating. Note that block used by constructing BatchGroup is recorded 853 // as free blocks in the last element of BatchGroup::Batches. Which means, 854 // once we pop the last TransferBatch, the block is implicitly 855 // deallocated. 856 if (ClassId != SizeClassMap::BatchClassId) 857 C->deallocate(SizeClassMap::BatchClassId, BG); 858 } 859 860 Region->FreeListInfo.PoppedBlocks += B->getCount(); 861 862 return B; 863 } 864 865 // Refill the freelist and return one batch. 866 NOINLINE TransferBatch *populateFreeListAndPopBatch(CacheT *C, uptr ClassId, 867 RegionInfo *Region) 868 REQUIRES(Region->MMLock) EXCLUDES(Region->FLLock) { 869 const uptr Size = getSizeByClassId(ClassId); 870 const u16 MaxCount = TransferBatch::getMaxCached(Size); 871 872 const uptr RegionBeg = Region->RegionBeg; 873 const uptr MappedUser = Region->MemMapInfo.MappedUser; 874 const uptr TotalUserBytes = 875 Region->MemMapInfo.AllocatedUser + MaxCount * Size; 876 // Map more space for blocks, if necessary. 877 if (TotalUserBytes > MappedUser) { 878 // Do the mmap for the user memory. 879 const uptr MapSize = 880 roundUp(TotalUserBytes - MappedUser, MapSizeIncrement); 881 const uptr RegionBase = RegionBeg - getRegionBaseByClassId(ClassId); 882 if (UNLIKELY(RegionBase + MappedUser + MapSize > RegionSize)) { 883 Region->Exhausted = true; 884 return nullptr; 885 } 886 887 if (UNLIKELY(!Region->MemMapInfo.MemMap.remap( 888 RegionBeg + MappedUser, MapSize, "scudo:primary", 889 MAP_ALLOWNOMEM | MAP_RESIZABLE | 890 (useMemoryTagging<Config>(Options.load()) ? MAP_MEMTAG 891 : 0)))) { 892 return nullptr; 893 } 894 Region->MemMapInfo.MappedUser += MapSize; 895 C->getStats().add(StatMapped, MapSize); 896 } 897 898 const u32 NumberOfBlocks = 899 Min(MaxNumBatches * MaxCount, 900 static_cast<u32>((Region->MemMapInfo.MappedUser - 901 Region->MemMapInfo.AllocatedUser) / 902 Size)); 903 DCHECK_GT(NumberOfBlocks, 0); 904 905 constexpr u32 ShuffleArraySize = 906 MaxNumBatches * TransferBatch::MaxNumCached; 907 CompactPtrT ShuffleArray[ShuffleArraySize]; 908 DCHECK_LE(NumberOfBlocks, ShuffleArraySize); 909 910 const uptr CompactPtrBase = getCompactPtrBaseByClassId(ClassId); 911 uptr P = RegionBeg + Region->MemMapInfo.AllocatedUser; 912 for (u32 I = 0; I < NumberOfBlocks; I++, P += Size) 913 ShuffleArray[I] = compactPtrInternal(CompactPtrBase, P); 914 915 ScopedLock L(Region->FLLock); 916 917 if (ClassId != SizeClassMap::BatchClassId) { 918 u32 N = 1; 919 uptr CurGroup = compactPtrGroup(ShuffleArray[0]); 920 for (u32 I = 1; I < NumberOfBlocks; I++) { 921 if (UNLIKELY(compactPtrGroup(ShuffleArray[I]) != CurGroup)) { 922 shuffle(ShuffleArray + I - N, N, &Region->RandState); 923 pushBlocksImpl(C, ClassId, Region, ShuffleArray + I - N, N, 924 /*SameGroup=*/true); 925 N = 1; 926 CurGroup = compactPtrGroup(ShuffleArray[I]); 927 } else { 928 ++N; 929 } 930 } 931 932 shuffle(ShuffleArray + NumberOfBlocks - N, N, &Region->RandState); 933 pushBlocksImpl(C, ClassId, Region, &ShuffleArray[NumberOfBlocks - N], N, 934 /*SameGroup=*/true); 935 } else { 936 pushBatchClassBlocks(Region, ShuffleArray, NumberOfBlocks); 937 } 938 939 TransferBatch *B = popBatchImpl(C, ClassId, Region); 940 DCHECK_NE(B, nullptr); 941 942 // Note that `PushedBlocks` and `PoppedBlocks` are supposed to only record 943 // the requests from `PushBlocks` and `PopBatch` which are external 944 // interfaces. `populateFreeListAndPopBatch` is the internal interface so we 945 // should set the values back to avoid incorrectly setting the stats. 946 Region->FreeListInfo.PushedBlocks -= NumberOfBlocks; 947 948 const uptr AllocatedUser = Size * NumberOfBlocks; 949 C->getStats().add(StatFree, AllocatedUser); 950 Region->MemMapInfo.AllocatedUser += AllocatedUser; 951 952 return B; 953 } 954 955 void getStats(ScopedString *Str, uptr ClassId, RegionInfo *Region) 956 REQUIRES(Region->MMLock, Region->FLLock) { 957 if (Region->MemMapInfo.MappedUser == 0) 958 return; 959 const uptr BlockSize = getSizeByClassId(ClassId); 960 const uptr InUse = 961 Region->FreeListInfo.PoppedBlocks - Region->FreeListInfo.PushedBlocks; 962 const uptr BytesInFreeList = 963 Region->MemMapInfo.AllocatedUser - InUse * BlockSize; 964 uptr RegionPushedBytesDelta = 0; 965 if (BytesInFreeList >= 966 Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint) { 967 RegionPushedBytesDelta = 968 BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint; 969 } 970 const uptr TotalChunks = Region->MemMapInfo.AllocatedUser / BlockSize; 971 Str->append( 972 "%s %02zu (%6zu): mapped: %6zuK popped: %7zu pushed: %7zu " 973 "inuse: %6zu total: %6zu releases: %6zu last " 974 "released: %6zuK latest pushed bytes: %6zuK region: 0x%zx (0x%zx)\n", 975 Region->Exhausted ? "F" : " ", ClassId, getSizeByClassId(ClassId), 976 Region->MemMapInfo.MappedUser >> 10, Region->FreeListInfo.PoppedBlocks, 977 Region->FreeListInfo.PushedBlocks, InUse, TotalChunks, 978 Region->ReleaseInfo.RangesReleased, 979 Region->ReleaseInfo.LastReleasedBytes >> 10, 980 RegionPushedBytesDelta >> 10, Region->RegionBeg, 981 getRegionBaseByClassId(ClassId)); 982 } 983 984 NOINLINE uptr releaseToOSMaybe(RegionInfo *Region, uptr ClassId, 985 ReleaseToOS ReleaseType = ReleaseToOS::Normal) 986 REQUIRES(Region->MMLock) EXCLUDES(Region->FLLock) { 987 ScopedLock L(Region->FLLock); 988 989 const uptr BlockSize = getSizeByClassId(ClassId); 990 const uptr BytesInFreeList = 991 Region->MemMapInfo.AllocatedUser - (Region->FreeListInfo.PoppedBlocks - 992 Region->FreeListInfo.PushedBlocks) * 993 BlockSize; 994 if (UNLIKELY(BytesInFreeList == 0)) 995 return false; 996 997 const uptr AllocatedUserEnd = 998 Region->MemMapInfo.AllocatedUser + Region->RegionBeg; 999 const uptr CompactPtrBase = getCompactPtrBaseByClassId(ClassId); 1000 1001 // ====================================================================== // 1002 // 1. Check if we have enough free blocks and if it's worth doing a page 1003 // release. 1004 // ====================================================================== // 1005 if (ReleaseType != ReleaseToOS::ForceAll && 1006 !hasChanceToReleasePages(Region, BlockSize, BytesInFreeList, 1007 ReleaseType)) { 1008 return 0; 1009 } 1010 1011 // ====================================================================== // 1012 // 2. Determine which groups can release the pages. Use a heuristic to 1013 // gather groups that are candidates for doing a release. 1014 // ====================================================================== // 1015 SinglyLinkedList<BatchGroup> GroupsToRelease; 1016 if (ReleaseType == ReleaseToOS::ForceAll) { 1017 GroupsToRelease = Region->FreeListInfo.BlockList; 1018 Region->FreeListInfo.BlockList.clear(); 1019 } else { 1020 GroupsToRelease = collectGroupsToRelease( 1021 Region, BlockSize, AllocatedUserEnd, CompactPtrBase); 1022 } 1023 if (GroupsToRelease.empty()) 1024 return 0; 1025 1026 // Ideally, we should use a class like `ScopedUnlock`. However, this form of 1027 // unlocking is not supported by the thread-safety analysis. See 1028 // https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-alias-analysis 1029 // for more details. 1030 // Put it as local class so that we can mark the ctor/dtor with proper 1031 // annotations associated to the target lock. Note that accessing the 1032 // function variable in local class only works in thread-safety annotations. 1033 // TODO: Implement general `ScopedUnlock` when it's supported. 1034 class FLLockScopedUnlock { 1035 public: 1036 FLLockScopedUnlock(RegionInfo *Region) RELEASE(Region->FLLock) 1037 : R(Region) { 1038 R->FLLock.assertHeld(); 1039 R->FLLock.unlock(); 1040 } 1041 ~FLLockScopedUnlock() ACQUIRE(Region->FLLock) { R->FLLock.lock(); } 1042 1043 private: 1044 RegionInfo *R; 1045 }; 1046 1047 // Note that we have extracted the `GroupsToRelease` from region freelist. 1048 // It's safe to let pushBlocks()/popBatches() access the remaining region 1049 // freelist. In the steps 3 and 4, we will temporarily release the FLLock 1050 // and lock it again before step 5. 1051 1052 uptr ReleasedBytes = 0; 1053 { 1054 FLLockScopedUnlock UL(Region); 1055 // ==================================================================== // 1056 // 3. Mark the free blocks in `GroupsToRelease` in the 1057 // `PageReleaseContext`. Then we can tell which pages are in-use by 1058 // querying `PageReleaseContext`. 1059 // ==================================================================== // 1060 PageReleaseContext Context = markFreeBlocks( 1061 Region, BlockSize, AllocatedUserEnd, CompactPtrBase, GroupsToRelease); 1062 if (UNLIKELY(!Context.hasBlockMarked())) { 1063 ScopedLock L(Region->FLLock); 1064 mergeGroupsToReleaseBack(Region, GroupsToRelease); 1065 return 0; 1066 } 1067 1068 // ==================================================================== // 1069 // 4. Release the unused physical pages back to the OS. 1070 // ==================================================================== // 1071 RegionReleaseRecorder<MemMapT> Recorder(&Region->MemMapInfo.MemMap, 1072 Region->RegionBeg, 1073 Context.getReleaseOffset()); 1074 auto SkipRegion = [](UNUSED uptr RegionIndex) { return false; }; 1075 releaseFreeMemoryToOS(Context, Recorder, SkipRegion); 1076 if (Recorder.getReleasedRangesCount() > 0) { 1077 Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList; 1078 Region->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount(); 1079 Region->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes(); 1080 } 1081 Region->ReleaseInfo.LastReleaseAtNs = getMonotonicTimeFast(); 1082 ReleasedBytes = Recorder.getReleasedBytes(); 1083 } 1084 1085 // ====================================================================== // 1086 // 5. Merge the `GroupsToRelease` back to the freelist. 1087 // ====================================================================== // 1088 mergeGroupsToReleaseBack(Region, GroupsToRelease); 1089 1090 return ReleasedBytes; 1091 } 1092 1093 bool hasChanceToReleasePages(RegionInfo *Region, uptr BlockSize, 1094 uptr BytesInFreeList, ReleaseToOS ReleaseType) 1095 REQUIRES(Region->MMLock, Region->FLLock) { 1096 DCHECK_GE(Region->FreeListInfo.PoppedBlocks, 1097 Region->FreeListInfo.PushedBlocks); 1098 const uptr PageSize = getPageSizeCached(); 1099 1100 // Always update `BytesInFreeListAtLastCheckpoint` with the smallest value 1101 // so that we won't underestimate the releasable pages. For example, the 1102 // following is the region usage, 1103 // 1104 // BytesInFreeListAtLastCheckpoint AllocatedUser 1105 // v v 1106 // |---------------------------------------> 1107 // ^ ^ 1108 // BytesInFreeList ReleaseThreshold 1109 // 1110 // In general, if we have collected enough bytes and the amount of free 1111 // bytes meets the ReleaseThreshold, we will try to do page release. If we 1112 // don't update `BytesInFreeListAtLastCheckpoint` when the current 1113 // `BytesInFreeList` is smaller, we may take longer time to wait for enough 1114 // freed blocks because we miss the bytes between 1115 // (BytesInFreeListAtLastCheckpoint - BytesInFreeList). 1116 if (BytesInFreeList <= 1117 Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint) { 1118 Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList; 1119 } 1120 1121 const uptr RegionPushedBytesDelta = 1122 BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint; 1123 if (RegionPushedBytesDelta < PageSize) 1124 return false; 1125 1126 // Releasing smaller blocks is expensive, so we want to make sure that a 1127 // significant amount of bytes are free, and that there has been a good 1128 // amount of batches pushed to the freelist before attempting to release. 1129 if (isSmallBlock(BlockSize) && ReleaseType == ReleaseToOS::Normal) 1130 if (RegionPushedBytesDelta < Region->TryReleaseThreshold) 1131 return false; 1132 1133 if (ReleaseType == ReleaseToOS::Normal) { 1134 const s32 IntervalMs = atomic_load_relaxed(&ReleaseToOsIntervalMs); 1135 if (IntervalMs < 0) 1136 return false; 1137 1138 // The constant 8 here is selected from profiling some apps and the number 1139 // of unreleased pages in the large size classes is around 16 pages or 1140 // more. Choose half of it as a heuristic and which also avoids page 1141 // release every time for every pushBlocks() attempt by large blocks. 1142 const bool ByPassReleaseInterval = 1143 isLargeBlock(BlockSize) && RegionPushedBytesDelta > 8 * PageSize; 1144 if (!ByPassReleaseInterval) { 1145 if (Region->ReleaseInfo.LastReleaseAtNs + 1146 static_cast<u64>(IntervalMs) * 1000000 > 1147 getMonotonicTimeFast()) { 1148 // Memory was returned recently. 1149 return false; 1150 } 1151 } 1152 } // if (ReleaseType == ReleaseToOS::Normal) 1153 1154 return true; 1155 } 1156 1157 SinglyLinkedList<BatchGroup> 1158 collectGroupsToRelease(RegionInfo *Region, const uptr BlockSize, 1159 const uptr AllocatedUserEnd, const uptr CompactPtrBase) 1160 REQUIRES(Region->MMLock, Region->FLLock) { 1161 const uptr GroupSize = (1U << GroupSizeLog); 1162 const uptr PageSize = getPageSizeCached(); 1163 SinglyLinkedList<BatchGroup> GroupsToRelease; 1164 1165 // We are examining each group and will take the minimum distance to the 1166 // release threshold as the next Region::TryReleaseThreshold(). Note that if 1167 // the size of free blocks has reached the release threshold, the distance 1168 // to the next release will be PageSize * SmallerBlockReleasePageDelta. See 1169 // the comment on `SmallerBlockReleasePageDelta` for more details. 1170 uptr MinDistToThreshold = GroupSize; 1171 1172 for (BatchGroup *BG = Region->FreeListInfo.BlockList.front(), 1173 *Prev = nullptr; 1174 BG != nullptr;) { 1175 // Group boundary is always GroupSize-aligned from CompactPtr base. The 1176 // layout of memory groups is like, 1177 // 1178 // (CompactPtrBase) 1179 // #1 CompactPtrGroupBase #2 CompactPtrGroupBase ... 1180 // | | | 1181 // v v v 1182 // +-----------------------+-----------------------+ 1183 // \ / \ / 1184 // --- GroupSize --- --- GroupSize --- 1185 // 1186 // After decompacting the CompactPtrGroupBase, we expect the alignment 1187 // property is held as well. 1188 const uptr BatchGroupBase = 1189 decompactGroupBase(CompactPtrBase, BG->CompactPtrGroupBase); 1190 DCHECK_LE(Region->RegionBeg, BatchGroupBase); 1191 DCHECK_GE(AllocatedUserEnd, BatchGroupBase); 1192 DCHECK_EQ((Region->RegionBeg - BatchGroupBase) % GroupSize, 0U); 1193 // TransferBatches are pushed in front of BG.Batches. The first one may 1194 // not have all caches used. 1195 const uptr NumBlocks = (BG->Batches.size() - 1) * BG->MaxCachedPerBatch + 1196 BG->Batches.front()->getCount(); 1197 const uptr BytesInBG = NumBlocks * BlockSize; 1198 1199 if (BytesInBG <= BG->BytesInBGAtLastCheckpoint) { 1200 BG->BytesInBGAtLastCheckpoint = BytesInBG; 1201 Prev = BG; 1202 BG = BG->Next; 1203 continue; 1204 } 1205 1206 const uptr PushedBytesDelta = BG->BytesInBGAtLastCheckpoint - BytesInBG; 1207 1208 // Given the randomness property, we try to release the pages only if the 1209 // bytes used by free blocks exceed certain proportion of group size. Note 1210 // that this heuristic only applies when all the spaces in a BatchGroup 1211 // are allocated. 1212 if (isSmallBlock(BlockSize)) { 1213 const uptr BatchGroupEnd = BatchGroupBase + GroupSize; 1214 const uptr AllocatedGroupSize = AllocatedUserEnd >= BatchGroupEnd 1215 ? GroupSize 1216 : AllocatedUserEnd - BatchGroupBase; 1217 const uptr ReleaseThreshold = 1218 (AllocatedGroupSize * (100 - 1U - BlockSize / 16U)) / 100U; 1219 const bool HighDensity = BytesInBG >= ReleaseThreshold; 1220 const bool MayHaveReleasedAll = NumBlocks >= (GroupSize / BlockSize); 1221 // If all blocks in the group are released, we will do range marking 1222 // which is fast. Otherwise, we will wait until we have accumulated 1223 // a certain amount of free memory. 1224 const bool ReachReleaseDelta = 1225 MayHaveReleasedAll 1226 ? true 1227 : PushedBytesDelta >= PageSize * SmallerBlockReleasePageDelta; 1228 1229 if (!HighDensity) { 1230 DCHECK_LE(BytesInBG, ReleaseThreshold); 1231 // The following is the usage of a memroy group, 1232 // 1233 // BytesInBG ReleaseThreshold 1234 // / \ v 1235 // +---+---------------------------+-----+ 1236 // | | | | | 1237 // +---+---------------------------+-----+ 1238 // \ / ^ 1239 // PushedBytesDelta GroupEnd 1240 MinDistToThreshold = 1241 Min(MinDistToThreshold, 1242 ReleaseThreshold - BytesInBG + PushedBytesDelta); 1243 } else { 1244 // If it reaches high density at this round, the next time we will try 1245 // to release is based on SmallerBlockReleasePageDelta 1246 MinDistToThreshold = 1247 Min(MinDistToThreshold, PageSize * SmallerBlockReleasePageDelta); 1248 } 1249 1250 if (!HighDensity || !ReachReleaseDelta) { 1251 Prev = BG; 1252 BG = BG->Next; 1253 continue; 1254 } 1255 } 1256 1257 // If `BG` is the first BatchGroup in the list, we only need to advance 1258 // `BG` and call FreeListInfo.BlockList::pop_front(). No update is needed 1259 // for `Prev`. 1260 // 1261 // (BG) (BG->Next) 1262 // Prev Cur BG 1263 // | | | 1264 // v v v 1265 // nil +--+ +--+ 1266 // |X | -> | | -> ... 1267 // +--+ +--+ 1268 // 1269 // Otherwise, `Prev` will be used to extract the `Cur` from the 1270 // `FreeListInfo.BlockList`. 1271 // 1272 // (BG) (BG->Next) 1273 // Prev Cur BG 1274 // | | | 1275 // v v v 1276 // +--+ +--+ +--+ 1277 // | | -> |X | -> | | -> ... 1278 // +--+ +--+ +--+ 1279 // 1280 // After FreeListInfo.BlockList::extract(), 1281 // 1282 // Prev Cur BG 1283 // | | | 1284 // v v v 1285 // +--+ +--+ +--+ 1286 // | |-+ |X | +->| | -> ... 1287 // +--+ | +--+ | +--+ 1288 // +--------+ 1289 // 1290 // Note that we need to advance before pushing this BatchGroup to 1291 // GroupsToRelease because it's a destructive operation. 1292 1293 BatchGroup *Cur = BG; 1294 BG = BG->Next; 1295 1296 // Ideally, we may want to update this only after successful release. 1297 // However, for smaller blocks, each block marking is a costly operation. 1298 // Therefore, we update it earlier. 1299 // TODO: Consider updating this after releasing pages if `ReleaseRecorder` 1300 // can tell the released bytes in each group. 1301 Cur->BytesInBGAtLastCheckpoint = BytesInBG; 1302 1303 if (Prev != nullptr) 1304 Region->FreeListInfo.BlockList.extract(Prev, Cur); 1305 else 1306 Region->FreeListInfo.BlockList.pop_front(); 1307 GroupsToRelease.push_back(Cur); 1308 } 1309 1310 // Only small blocks have the adaptive `TryReleaseThreshold`. 1311 if (isSmallBlock(BlockSize)) { 1312 // If the MinDistToThreshold is not updated, that means each memory group 1313 // may have only pushed less than a page size. In that case, just set it 1314 // back to normal. 1315 if (MinDistToThreshold == GroupSize) 1316 MinDistToThreshold = PageSize * SmallerBlockReleasePageDelta; 1317 Region->TryReleaseThreshold = MinDistToThreshold; 1318 } 1319 1320 return GroupsToRelease; 1321 } 1322 1323 PageReleaseContext 1324 markFreeBlocks(RegionInfo *Region, const uptr BlockSize, 1325 const uptr AllocatedUserEnd, const uptr CompactPtrBase, 1326 SinglyLinkedList<BatchGroup> &GroupsToRelease) 1327 REQUIRES(Region->MMLock) EXCLUDES(Region->FLLock) { 1328 const uptr GroupSize = (1U << GroupSizeLog); 1329 auto DecompactPtr = [CompactPtrBase](CompactPtrT CompactPtr) { 1330 return decompactPtrInternal(CompactPtrBase, CompactPtr); 1331 }; 1332 1333 const uptr ReleaseBase = decompactGroupBase( 1334 CompactPtrBase, GroupsToRelease.front()->CompactPtrGroupBase); 1335 const uptr LastGroupEnd = 1336 Min(decompactGroupBase(CompactPtrBase, 1337 GroupsToRelease.back()->CompactPtrGroupBase) + 1338 GroupSize, 1339 AllocatedUserEnd); 1340 // The last block may straddle the group boundary. Rounding up to BlockSize 1341 // to get the exact range. 1342 const uptr ReleaseEnd = 1343 roundUpSlow(LastGroupEnd - Region->RegionBeg, BlockSize) + 1344 Region->RegionBeg; 1345 const uptr ReleaseRangeSize = ReleaseEnd - ReleaseBase; 1346 const uptr ReleaseOffset = ReleaseBase - Region->RegionBeg; 1347 1348 PageReleaseContext Context(BlockSize, /*NumberOfRegions=*/1U, 1349 ReleaseRangeSize, ReleaseOffset); 1350 // We may not be able to do the page release in a rare case that we may 1351 // fail on PageMap allocation. 1352 if (UNLIKELY(!Context.ensurePageMapAllocated())) 1353 return Context; 1354 1355 for (BatchGroup &BG : GroupsToRelease) { 1356 const uptr BatchGroupBase = 1357 decompactGroupBase(CompactPtrBase, BG.CompactPtrGroupBase); 1358 const uptr BatchGroupEnd = BatchGroupBase + GroupSize; 1359 const uptr AllocatedGroupSize = AllocatedUserEnd >= BatchGroupEnd 1360 ? GroupSize 1361 : AllocatedUserEnd - BatchGroupBase; 1362 const uptr BatchGroupUsedEnd = BatchGroupBase + AllocatedGroupSize; 1363 const bool MayContainLastBlockInRegion = 1364 BatchGroupUsedEnd == AllocatedUserEnd; 1365 const bool BlockAlignedWithUsedEnd = 1366 (BatchGroupUsedEnd - Region->RegionBeg) % BlockSize == 0; 1367 1368 uptr MaxContainedBlocks = AllocatedGroupSize / BlockSize; 1369 if (!BlockAlignedWithUsedEnd) 1370 ++MaxContainedBlocks; 1371 1372 const uptr NumBlocks = (BG.Batches.size() - 1) * BG.MaxCachedPerBatch + 1373 BG.Batches.front()->getCount(); 1374 1375 if (NumBlocks == MaxContainedBlocks) { 1376 for (const auto &It : BG.Batches) { 1377 if (&It != BG.Batches.front()) 1378 DCHECK_EQ(It.getCount(), BG.MaxCachedPerBatch); 1379 for (u16 I = 0; I < It.getCount(); ++I) 1380 DCHECK_EQ(compactPtrGroup(It.get(I)), BG.CompactPtrGroupBase); 1381 } 1382 1383 Context.markRangeAsAllCounted(BatchGroupBase, BatchGroupUsedEnd, 1384 Region->RegionBeg, /*RegionIndex=*/0, 1385 Region->MemMapInfo.AllocatedUser); 1386 } else { 1387 DCHECK_LT(NumBlocks, MaxContainedBlocks); 1388 // Note that we don't always visit blocks in each BatchGroup so that we 1389 // may miss the chance of releasing certain pages that cross 1390 // BatchGroups. 1391 Context.markFreeBlocksInRegion( 1392 BG.Batches, DecompactPtr, Region->RegionBeg, /*RegionIndex=*/0, 1393 Region->MemMapInfo.AllocatedUser, MayContainLastBlockInRegion); 1394 } 1395 } 1396 1397 DCHECK(Context.hasBlockMarked()); 1398 1399 return Context; 1400 } 1401 1402 void mergeGroupsToReleaseBack(RegionInfo *Region, 1403 SinglyLinkedList<BatchGroup> &GroupsToRelease) 1404 REQUIRES(Region->MMLock, Region->FLLock) { 1405 // After merging two freelists, we may have redundant `BatchGroup`s that 1406 // need to be recycled. The number of unused `BatchGroup`s is expected to be 1407 // small. Pick a constant which is inferred from real programs. 1408 constexpr uptr MaxUnusedSize = 8; 1409 CompactPtrT Blocks[MaxUnusedSize]; 1410 u32 Idx = 0; 1411 RegionInfo *BatchClassRegion = getRegionInfo(SizeClassMap::BatchClassId); 1412 // We can't call pushBatchClassBlocks() to recycle the unused `BatchGroup`s 1413 // when we are manipulating the freelist of `BatchClassRegion`. Instead, we 1414 // should just push it back to the freelist when we merge two `BatchGroup`s. 1415 // This logic hasn't been implemented because we haven't supported releasing 1416 // pages in `BatchClassRegion`. 1417 DCHECK_NE(BatchClassRegion, Region); 1418 1419 // Merge GroupsToRelease back to the Region::FreeListInfo.BlockList. Note 1420 // that both `Region->FreeListInfo.BlockList` and `GroupsToRelease` are 1421 // sorted. 1422 for (BatchGroup *BG = Region->FreeListInfo.BlockList.front(), 1423 *Prev = nullptr; 1424 ;) { 1425 if (BG == nullptr || GroupsToRelease.empty()) { 1426 if (!GroupsToRelease.empty()) 1427 Region->FreeListInfo.BlockList.append_back(&GroupsToRelease); 1428 break; 1429 } 1430 1431 DCHECK(!BG->Batches.empty()); 1432 1433 if (BG->CompactPtrGroupBase < 1434 GroupsToRelease.front()->CompactPtrGroupBase) { 1435 Prev = BG; 1436 BG = BG->Next; 1437 continue; 1438 } 1439 1440 BatchGroup *Cur = GroupsToRelease.front(); 1441 TransferBatch *UnusedTransferBatch = nullptr; 1442 GroupsToRelease.pop_front(); 1443 1444 if (BG->CompactPtrGroupBase == Cur->CompactPtrGroupBase) { 1445 BG->PushedBlocks += Cur->PushedBlocks; 1446 // We have updated `BatchGroup::BytesInBGAtLastCheckpoint` while 1447 // collecting the `GroupsToRelease`. 1448 BG->BytesInBGAtLastCheckpoint = Cur->BytesInBGAtLastCheckpoint; 1449 const uptr MaxCachedPerBatch = BG->MaxCachedPerBatch; 1450 1451 // Note that the first TransferBatches in both `Batches` may not be 1452 // full and only the first TransferBatch can have non-full blocks. Thus 1453 // we have to merge them before appending one to another. 1454 if (Cur->Batches.front()->getCount() == MaxCachedPerBatch) { 1455 BG->Batches.append_back(&Cur->Batches); 1456 } else { 1457 TransferBatch *NonFullBatch = Cur->Batches.front(); 1458 Cur->Batches.pop_front(); 1459 const u16 NonFullBatchCount = NonFullBatch->getCount(); 1460 // The remaining Batches in `Cur` are full. 1461 BG->Batches.append_back(&Cur->Batches); 1462 1463 if (BG->Batches.front()->getCount() == MaxCachedPerBatch) { 1464 // Only 1 non-full TransferBatch, push it to the front. 1465 BG->Batches.push_front(NonFullBatch); 1466 } else { 1467 const u16 NumBlocksToMove = static_cast<u16>( 1468 Min(static_cast<u16>(MaxCachedPerBatch - 1469 BG->Batches.front()->getCount()), 1470 NonFullBatchCount)); 1471 BG->Batches.front()->appendFromTransferBatch(NonFullBatch, 1472 NumBlocksToMove); 1473 if (NonFullBatch->isEmpty()) 1474 UnusedTransferBatch = NonFullBatch; 1475 else 1476 BG->Batches.push_front(NonFullBatch); 1477 } 1478 } 1479 1480 const u32 NeededSlots = UnusedTransferBatch == nullptr ? 1U : 2U; 1481 if (UNLIKELY(Idx + NeededSlots > MaxUnusedSize)) { 1482 ScopedLock L(BatchClassRegion->FLLock); 1483 pushBatchClassBlocks(BatchClassRegion, Blocks, Idx); 1484 Idx = 0; 1485 } 1486 Blocks[Idx++] = 1487 compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(Cur)); 1488 if (UnusedTransferBatch) { 1489 Blocks[Idx++] = 1490 compactPtr(SizeClassMap::BatchClassId, 1491 reinterpret_cast<uptr>(UnusedTransferBatch)); 1492 } 1493 Prev = BG; 1494 BG = BG->Next; 1495 continue; 1496 } 1497 1498 // At here, the `BG` is the first BatchGroup with CompactPtrGroupBase 1499 // larger than the first element in `GroupsToRelease`. We need to insert 1500 // `GroupsToRelease::front()` (which is `Cur` below) before `BG`. 1501 // 1502 // 1. If `Prev` is nullptr, we simply push `Cur` to the front of 1503 // FreeListInfo.BlockList. 1504 // 2. Otherwise, use `insert()` which inserts an element next to `Prev`. 1505 // 1506 // Afterwards, we don't need to advance `BG` because the order between 1507 // `BG` and the new `GroupsToRelease::front()` hasn't been checked. 1508 if (Prev == nullptr) 1509 Region->FreeListInfo.BlockList.push_front(Cur); 1510 else 1511 Region->FreeListInfo.BlockList.insert(Prev, Cur); 1512 DCHECK_EQ(Cur->Next, BG); 1513 Prev = Cur; 1514 } 1515 1516 if (Idx != 0) { 1517 ScopedLock L(BatchClassRegion->FLLock); 1518 pushBatchClassBlocks(BatchClassRegion, Blocks, Idx); 1519 } 1520 1521 if (SCUDO_DEBUG) { 1522 BatchGroup *Prev = Region->FreeListInfo.BlockList.front(); 1523 for (BatchGroup *Cur = Prev->Next; Cur != nullptr; 1524 Prev = Cur, Cur = Cur->Next) { 1525 CHECK_LT(Prev->CompactPtrGroupBase, Cur->CompactPtrGroupBase); 1526 } 1527 } 1528 } 1529 1530 // TODO: `PrimaryBase` can be obtained from ReservedMemory. This needs to be 1531 // deprecated. 1532 uptr PrimaryBase = 0; 1533 ReservedMemoryT ReservedMemory = {}; 1534 // The minimum size of pushed blocks that we will try to release the pages in 1535 // that size class. 1536 uptr SmallerBlockReleasePageDelta = 0; 1537 atomic_s32 ReleaseToOsIntervalMs = {}; 1538 alignas(SCUDO_CACHE_LINE_SIZE) RegionInfo RegionInfoArray[NumClasses]; 1539 }; 1540 1541 } // namespace scudo 1542 1543 #endif // SCUDO_PRIMARY64_H_ 1544