• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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::PrimaryCompactPtrT CompactPtrT;
49   static const uptr CompactPtrScale = Config::PrimaryCompactPtrScale;
50   static const uptr GroupSizeLog = Config::PrimaryGroupSizeLog;
51   static const uptr GroupScale = GroupSizeLog - CompactPtrScale;
52   typedef typename Config::SizeClassMap SizeClassMap;
53   typedef SizeClassAllocator64<Config> ThisT;
54   typedef SizeClassAllocatorLocalCache<ThisT> CacheT;
55   typedef typename CacheT::TransferBatch TransferBatch;
56   typedef typename CacheT::BatchGroup BatchGroup;
57 
getSizeByClassId(uptr ClassId)58   static uptr getSizeByClassId(uptr ClassId) {
59     return (ClassId == SizeClassMap::BatchClassId)
60                ? roundUp(sizeof(TransferBatch), 1U << CompactPtrScale)
61                : SizeClassMap::getSizeByClassId(ClassId);
62   }
63 
canAllocate(uptr Size)64   static bool canAllocate(uptr Size) { return Size <= SizeClassMap::MaxSize; }
65 
init(s32 ReleaseToOsInterval)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 = (PrimaryBase + (I << Config::PrimaryRegionSizeLog)) +
123                           (Config::PrimaryEnableRandomOffset
124                                ? ((getRandomModN(&Seed, 16) + 1) * PageSize)
125                                : 0);
126       Region->RandState = getRandomU32(&Seed);
127       // Releasing small blocks is expensive, set a higher threshold to avoid
128       // frequent page releases.
129       if (isSmallBlock(getSizeByClassId(I)))
130         Region->TryReleaseThreshold = PageSize * SmallerBlockReleasePageDelta;
131       else
132         Region->TryReleaseThreshold = PageSize;
133       Region->ReleaseInfo.LastReleaseAtNs = Time;
134     }
135     shuffle(RegionInfoArray, NumClasses, &Seed);
136 
137     setOption(Option::ReleaseInterval, static_cast<sptr>(ReleaseToOsInterval));
138   }
139 
unmapTestOnly()140   void unmapTestOnly() NO_THREAD_SAFETY_ANALYSIS {
141     for (uptr I = 0; I < NumClasses; I++) {
142       RegionInfo *Region = getRegionInfo(I);
143       *Region = {};
144     }
145     if (PrimaryBase)
146       ReservedMemory.release();
147     PrimaryBase = 0U;
148   }
149 
popBatch(CacheT * C,uptr ClassId)150   TransferBatch *popBatch(CacheT *C, uptr ClassId) {
151     DCHECK_LT(ClassId, NumClasses);
152     RegionInfo *Region = getRegionInfo(ClassId);
153     bool PrintStats = false;
154     {
155       ScopedLock L(Region->Mutex);
156       TransferBatch *B = popBatchImpl(C, ClassId, Region);
157       if (LIKELY(B)) {
158         Region->Stats.PoppedBlocks += B->getCount();
159         return B;
160       }
161 
162       const bool RegionIsExhausted = Region->Exhausted;
163       if (UNLIKELY(RegionIsExhausted ||
164                    !populateFreeList(C, ClassId, Region))) {
165         PrintStats = !RegionIsExhausted && Region->Exhausted;
166       } else {
167         B = popBatchImpl(C, ClassId, Region);
168         // if `populateFreeList` succeeded, we are supposed to get free blocks.
169         DCHECK_NE(B, nullptr);
170         Region->Stats.PoppedBlocks += B->getCount();
171         return B;
172       }
173     }
174 
175     // Note that `getStats()` requires locking each region so we can't call it
176     // while locking the Region->Mutex in the above.
177     if (UNLIKELY(PrintStats)) {
178       ScopedString Str;
179       getStats(&Str);
180       Str.append(
181           "Scudo OOM: The process has exhausted %zuM for size class %zu.\n",
182           RegionSize >> 20, getSizeByClassId(ClassId));
183       Str.output();
184     }
185     return nullptr;
186   }
187 
188   // Push the array of free blocks to the designated batch group.
pushBlocks(CacheT * C,uptr ClassId,CompactPtrT * Array,u32 Size)189   void pushBlocks(CacheT *C, uptr ClassId, CompactPtrT *Array, u32 Size) {
190     DCHECK_LT(ClassId, NumClasses);
191     DCHECK_GT(Size, 0);
192 
193     RegionInfo *Region = getRegionInfo(ClassId);
194     if (ClassId == SizeClassMap::BatchClassId) {
195       bool PrintStats = false;
196       {
197         ScopedLock L(Region->Mutex);
198         // Constructing a batch group in the free list will use two blocks in
199         // BatchClassId. If we are pushing BatchClassId blocks, we will use the
200         // blocks in the array directly (can't delegate local cache which will
201         // cause a recursive allocation). However, The number of free blocks may
202         // be less than two. Therefore, populate the free list before inserting
203         // the blocks.
204         const bool NeedToRefill = Size == 1U && Region->FreeList.empty();
205         // If BatchClass has been exhausted, the program should have been
206         // aborted.
207         DCHECK(!Region->Exhausted);
208 
209         if (UNLIKELY(
210                 NeedToRefill &&
211                 !populateFreeList(C, SizeClassMap::BatchClassId, Region))) {
212           PrintStats = true;
213         } else {
214           pushBlocksImpl(C, SizeClassMap::BatchClassId, Region, Array, Size);
215           Region->Stats.PushedBlocks += Size;
216         }
217       }
218 
219       // Note that `getStats()` requires the lock of each region so we can't
220       // call it while locking the Region->Mutex in the above.
221       if (UNLIKELY(PrintStats)) {
222         ScopedString Str;
223         getStats(&Str);
224         Str.append(
225             "Scudo OOM: The process has exhausted %zuM for size class %zu.\n",
226             RegionSize >> 20, getSizeByClassId(ClassId));
227         Str.output();
228         // Theoretically, BatchClass shouldn't be used up. Abort immediately
229         // when it happens.
230         reportOutOfBatchClass();
231       }
232 
233       return;
234     }
235 
236     // TODO(chiahungduan): Consider not doing grouping if the group size is not
237     // greater than the block size with a certain scale.
238 
239     // Sort the blocks so that blocks belonging to the same group can be pushed
240     // together.
241     bool SameGroup = true;
242     for (u32 I = 1; I < Size; ++I) {
243       if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I]))
244         SameGroup = false;
245       CompactPtrT Cur = Array[I];
246       u32 J = I;
247       while (J > 0 && compactPtrGroup(Cur) < compactPtrGroup(Array[J - 1])) {
248         Array[J] = Array[J - 1];
249         --J;
250       }
251       Array[J] = Cur;
252     }
253 
254     ScopedLock L(Region->Mutex);
255     pushBlocksImpl(C, ClassId, Region, Array, Size, SameGroup);
256 
257     Region->Stats.PushedBlocks += Size;
258     if (ClassId != SizeClassMap::BatchClassId)
259       releaseToOSMaybe(Region, ClassId);
260   }
261 
disable()262   void disable() NO_THREAD_SAFETY_ANALYSIS {
263     // The BatchClassId must be locked last since other classes can use it.
264     for (sptr I = static_cast<sptr>(NumClasses) - 1; I >= 0; I--) {
265       if (static_cast<uptr>(I) == SizeClassMap::BatchClassId)
266         continue;
267       getRegionInfo(static_cast<uptr>(I))->Mutex.lock();
268     }
269     getRegionInfo(SizeClassMap::BatchClassId)->Mutex.lock();
270   }
271 
enable()272   void enable() NO_THREAD_SAFETY_ANALYSIS {
273     getRegionInfo(SizeClassMap::BatchClassId)->Mutex.unlock();
274     for (uptr I = 0; I < NumClasses; I++) {
275       if (I == SizeClassMap::BatchClassId)
276         continue;
277       getRegionInfo(I)->Mutex.unlock();
278     }
279   }
280 
iterateOverBlocks(F Callback)281   template <typename F> void iterateOverBlocks(F Callback) {
282     for (uptr I = 0; I < NumClasses; I++) {
283       if (I == SizeClassMap::BatchClassId)
284         continue;
285       RegionInfo *Region = getRegionInfo(I);
286       // TODO: The call of `iterateOverBlocks` requires disabling
287       // SizeClassAllocator64. We may consider locking each region on demand
288       // only.
289       Region->Mutex.assertHeld();
290       const uptr BlockSize = getSizeByClassId(I);
291       const uptr From = Region->RegionBeg;
292       const uptr To = From + Region->AllocatedUser;
293       for (uptr Block = From; Block < To; Block += BlockSize)
294         Callback(Block);
295     }
296   }
297 
getStats(ScopedString * Str)298   void getStats(ScopedString *Str) {
299     // TODO(kostyak): get the RSS per region.
300     uptr TotalMapped = 0;
301     uptr PoppedBlocks = 0;
302     uptr PushedBlocks = 0;
303     for (uptr I = 0; I < NumClasses; I++) {
304       RegionInfo *Region = getRegionInfo(I);
305       ScopedLock L(Region->Mutex);
306       if (Region->MappedUser)
307         TotalMapped += Region->MappedUser;
308       PoppedBlocks += Region->Stats.PoppedBlocks;
309       PushedBlocks += Region->Stats.PushedBlocks;
310     }
311     Str->append("Stats: SizeClassAllocator64: %zuM mapped (%uM rss) in %zu "
312                 "allocations; remains %zu\n",
313                 TotalMapped >> 20, 0U, PoppedBlocks,
314                 PoppedBlocks - PushedBlocks);
315 
316     for (uptr I = 0; I < NumClasses; I++) {
317       RegionInfo *Region = getRegionInfo(I);
318       ScopedLock L(Region->Mutex);
319       getStats(Str, I, Region, 0);
320     }
321   }
322 
setOption(Option O,sptr Value)323   bool setOption(Option O, sptr Value) {
324     if (O == Option::ReleaseInterval) {
325       const s32 Interval = Max(
326           Min(static_cast<s32>(Value), Config::PrimaryMaxReleaseToOsIntervalMs),
327           Config::PrimaryMinReleaseToOsIntervalMs);
328       atomic_store_relaxed(&ReleaseToOsIntervalMs, Interval);
329       return true;
330     }
331     // Not supported by the Primary, but not an error either.
332     return true;
333   }
334 
releaseToOS(ReleaseToOS ReleaseType)335   uptr releaseToOS(ReleaseToOS ReleaseType) {
336     uptr TotalReleasedBytes = 0;
337     for (uptr I = 0; I < NumClasses; I++) {
338       if (I == SizeClassMap::BatchClassId)
339         continue;
340       RegionInfo *Region = getRegionInfo(I);
341       ScopedLock L(Region->Mutex);
342       TotalReleasedBytes += releaseToOSMaybe(Region, I, ReleaseType);
343     }
344     return TotalReleasedBytes;
345   }
346 
getRegionInfoArrayAddress()347   const char *getRegionInfoArrayAddress() const {
348     return reinterpret_cast<const char *>(RegionInfoArray);
349   }
350 
getRegionInfoArraySize()351   static uptr getRegionInfoArraySize() { return sizeof(RegionInfoArray); }
352 
getCompactPtrBaseByClassId(uptr ClassId)353   uptr getCompactPtrBaseByClassId(uptr ClassId) {
354     return getRegionInfo(ClassId)->RegionBeg;
355   }
356 
compactPtr(uptr ClassId,uptr Ptr)357   CompactPtrT compactPtr(uptr ClassId, uptr Ptr) {
358     DCHECK_LE(ClassId, SizeClassMap::LargestClassId);
359     return compactPtrInternal(getCompactPtrBaseByClassId(ClassId), Ptr);
360   }
361 
decompactPtr(uptr ClassId,CompactPtrT CompactPtr)362   void *decompactPtr(uptr ClassId, CompactPtrT CompactPtr) {
363     DCHECK_LE(ClassId, SizeClassMap::LargestClassId);
364     return reinterpret_cast<void *>(
365         decompactPtrInternal(getCompactPtrBaseByClassId(ClassId), CompactPtr));
366   }
367 
findNearestBlock(const char * RegionInfoData,uptr Ptr)368   static BlockInfo findNearestBlock(const char *RegionInfoData,
369                                     uptr Ptr) NO_THREAD_SAFETY_ANALYSIS {
370     const RegionInfo *RegionInfoArray =
371         reinterpret_cast<const RegionInfo *>(RegionInfoData);
372 
373     uptr ClassId;
374     uptr MinDistance = -1UL;
375     for (uptr I = 0; I != NumClasses; ++I) {
376       if (I == SizeClassMap::BatchClassId)
377         continue;
378       uptr Begin = RegionInfoArray[I].RegionBeg;
379       // TODO(chiahungduan): In fact, We need to lock the RegionInfo::Mutex.
380       // However, the RegionInfoData is passed with const qualifier and lock the
381       // mutex requires modifying RegionInfoData, which means we need to remove
382       // the const qualifier. This may lead to another undefined behavior (The
383       // first one is accessing `AllocatedUser` without locking. It's better to
384       // pass `RegionInfoData` as `void *` then we can lock the mutex properly.
385       uptr End = Begin + RegionInfoArray[I].AllocatedUser;
386       if (Begin > End || End - Begin < SizeClassMap::getSizeByClassId(I))
387         continue;
388       uptr RegionDistance;
389       if (Begin <= Ptr) {
390         if (Ptr < End)
391           RegionDistance = 0;
392         else
393           RegionDistance = Ptr - End;
394       } else {
395         RegionDistance = Begin - Ptr;
396       }
397 
398       if (RegionDistance < MinDistance) {
399         MinDistance = RegionDistance;
400         ClassId = I;
401       }
402     }
403 
404     BlockInfo B = {};
405     if (MinDistance <= 8192) {
406       B.RegionBegin = RegionInfoArray[ClassId].RegionBeg;
407       B.RegionEnd = B.RegionBegin + RegionInfoArray[ClassId].AllocatedUser;
408       B.BlockSize = SizeClassMap::getSizeByClassId(ClassId);
409       B.BlockBegin =
410           B.RegionBegin + uptr(sptr(Ptr - B.RegionBegin) / sptr(B.BlockSize) *
411                                sptr(B.BlockSize));
412       while (B.BlockBegin < B.RegionBegin)
413         B.BlockBegin += B.BlockSize;
414       while (B.RegionEnd < B.BlockBegin + B.BlockSize)
415         B.BlockBegin -= B.BlockSize;
416     }
417     return B;
418   }
419 
420   AtomicOptions Options;
421 
422 private:
423   static const uptr RegionSize = 1UL << Config::PrimaryRegionSizeLog;
424   static const uptr NumClasses = SizeClassMap::NumClasses;
425   static const uptr PrimarySize = RegionSize * NumClasses;
426 
427   static const uptr MapSizeIncrement = Config::PrimaryMapSizeIncrement;
428   // Fill at most this number of batches from the newly map'd memory.
429   static const u32 MaxNumBatches = SCUDO_ANDROID ? 4U : 8U;
430 
431   struct RegionStats {
432     uptr PoppedBlocks;
433     uptr PushedBlocks;
434   };
435 
436   struct ReleaseToOsInfo {
437     uptr BytesInFreeListAtLastCheckpoint;
438     uptr RangesReleased;
439     uptr LastReleasedBytes;
440     u64 LastReleaseAtNs;
441   };
442 
443   struct UnpaddedRegionInfo {
444     HybridMutex Mutex;
445     SinglyLinkedList<BatchGroup> FreeList GUARDED_BY(Mutex);
446     // This is initialized before thread creation.
447     uptr RegionBeg = 0;
448     RegionStats Stats GUARDED_BY(Mutex) = {};
449     u32 RandState GUARDED_BY(Mutex) = 0;
450     // Bytes mapped for user memory.
451     uptr MappedUser GUARDED_BY(Mutex) = 0;
452     // Bytes allocated for user memory.
453     uptr AllocatedUser GUARDED_BY(Mutex) = 0;
454     // The minimum size of pushed blocks to trigger page release.
455     uptr TryReleaseThreshold GUARDED_BY(Mutex) = 0;
456     MemMapT MemMap = {};
457     ReleaseToOsInfo ReleaseInfo GUARDED_BY(Mutex) = {};
458     bool Exhausted GUARDED_BY(Mutex) = false;
459   };
460   struct RegionInfo : UnpaddedRegionInfo {
461     char Padding[SCUDO_CACHE_LINE_SIZE -
462                  (sizeof(UnpaddedRegionInfo) % SCUDO_CACHE_LINE_SIZE)] = {};
463   };
464   static_assert(sizeof(RegionInfo) % SCUDO_CACHE_LINE_SIZE == 0, "");
465 
466   // TODO: `PrimaryBase` can be obtained from ReservedMemory. This needs to be
467   // deprecated.
468   uptr PrimaryBase = 0;
469   ReservedMemoryT ReservedMemory = {};
470   // The minimum size of pushed blocks that we will try to release the pages in
471   // that size class.
472   uptr SmallerBlockReleasePageDelta = 0;
473   atomic_s32 ReleaseToOsIntervalMs = {};
474   alignas(SCUDO_CACHE_LINE_SIZE) RegionInfo RegionInfoArray[NumClasses];
475 
getRegionInfo(uptr ClassId)476   RegionInfo *getRegionInfo(uptr ClassId) {
477     DCHECK_LT(ClassId, NumClasses);
478     return &RegionInfoArray[ClassId];
479   }
480 
getRegionBaseByClassId(uptr ClassId)481   uptr getRegionBaseByClassId(uptr ClassId) {
482     return roundDown(getRegionInfo(ClassId)->RegionBeg - PrimaryBase,
483                      RegionSize) +
484            PrimaryBase;
485   }
486 
compactPtrInternal(uptr Base,uptr Ptr)487   static CompactPtrT compactPtrInternal(uptr Base, uptr Ptr) {
488     return static_cast<CompactPtrT>((Ptr - Base) >> CompactPtrScale);
489   }
490 
decompactPtrInternal(uptr Base,CompactPtrT CompactPtr)491   static uptr decompactPtrInternal(uptr Base, CompactPtrT CompactPtr) {
492     return Base + (static_cast<uptr>(CompactPtr) << CompactPtrScale);
493   }
494 
compactPtrGroup(CompactPtrT CompactPtr)495   static uptr compactPtrGroup(CompactPtrT CompactPtr) {
496     const uptr Mask = (static_cast<uptr>(1) << GroupScale) - 1;
497     return static_cast<uptr>(CompactPtr) & ~Mask;
498   }
decompactGroupBase(uptr Base,uptr CompactPtrGroupBase)499   static uptr decompactGroupBase(uptr Base, uptr CompactPtrGroupBase) {
500     DCHECK_EQ(CompactPtrGroupBase % (static_cast<uptr>(1) << (GroupScale)), 0U);
501     return Base + (CompactPtrGroupBase << CompactPtrScale);
502   }
503 
isSmallBlock(uptr BlockSize)504   ALWAYS_INLINE static bool isSmallBlock(uptr BlockSize) {
505     const uptr PageSize = getPageSizeCached();
506     return BlockSize < PageSize / 16U;
507   }
508 
509   // Push the blocks to their batch group. The layout will be like,
510   //
511   // FreeList - > BG -> BG -> BG
512   //              |     |     |
513   //              v     v     v
514   //              TB    TB    TB
515   //              |
516   //              v
517   //              TB
518   //
519   // Each BlockGroup(BG) will associate with unique group id and the free blocks
520   // are managed by a list of TransferBatch(TB). To reduce the time of inserting
521   // blocks, BGs are sorted and the input `Array` are supposed to be sorted so
522   // that we can get better performance of maintaining sorted property.
523   // Use `SameGroup=true` to indicate that all blocks in the array are from the
524   // same group then we will skip checking the group id of each block.
525   //
526   // The region mutex needs to be held while calling this method.
527   void pushBlocksImpl(CacheT *C, uptr ClassId, RegionInfo *Region,
528                       CompactPtrT *Array, u32 Size, bool SameGroup = false)
529       REQUIRES(Region->Mutex) {
530     DCHECK_GT(Size, 0U);
531 
532     auto CreateGroup = [&](uptr CompactPtrGroupBase) {
533       BatchGroup *BG = nullptr;
534       TransferBatch *TB = nullptr;
535       if (ClassId == SizeClassMap::BatchClassId) {
536         DCHECK_GE(Size, 2U);
537 
538         // Free blocks are recorded by TransferBatch in freelist, blocks of
539         // BatchClassId are included. In order not to use additional memory to
540         // record blocks of BatchClassId, they are self-contained. I.e., A
541         // TransferBatch may record the block address of itself. See the figure
542         // below:
543         //
544         // TransferBatch at 0xABCD
545         // +----------------------------+
546         // | Free blocks' addr          |
547         // | +------+------+------+     |
548         // | |0xABCD|...   |...   |     |
549         // | +------+------+------+     |
550         // +----------------------------+
551         //
552         // The safeness of manipulating TransferBatch is kept by the invariant,
553         //
554         //   The unit of each pop-block request is a TransferBatch. Return
555         //   part of the blocks in a TransferBatch is not allowed.
556         //
557         // This ensures that TransferBatch won't leak the address itself while
558         // it's still holding other valid data.
559         //
560         // Besides, BatchGroup uses the same size-class as TransferBatch does
561         // and its address is recorded in the TransferBatch too. To maintain the
562         // safeness, the invariant to keep is,
563         //
564         //   The address of itself is always recorded in the last TransferBatch
565         //   of the freelist (also imply that the freelist should only be
566         //   updated with push_front). Once the last TransferBatch is popped,
567         //   the BatchGroup becomes invalid.
568         //
569         // As a result, the blocks used by BatchGroup and TransferBatch are
570         // reusable and don't need additional space for them.
571         BG = reinterpret_cast<BatchGroup *>(
572             decompactPtr(ClassId, Array[Size - 1]));
573         BG->Batches.clear();
574 
575         TB = reinterpret_cast<TransferBatch *>(
576             decompactPtr(ClassId, Array[Size - 2]));
577         TB->clear();
578 
579         // Append the blocks used by BatchGroup and TransferBatch immediately so
580         // that we ensure that they are in the last TransBatch.
581         TB->appendFromArray(Array + Size - 2, 2);
582         Size -= 2;
583       } else {
584         BG = C->createGroup();
585         BG->Batches.clear();
586 
587         TB = C->createBatch(ClassId, nullptr);
588         TB->clear();
589       }
590 
591       BG->CompactPtrGroupBase = CompactPtrGroupBase;
592       // TODO(chiahungduan): Avoid the use of push_back() in `Batches`.
593       BG->Batches.push_front(TB);
594       BG->PushedBlocks = 0;
595       BG->BytesInBGAtLastCheckpoint = 0;
596       BG->MaxCachedPerBatch =
597           TransferBatch::getMaxCached(getSizeByClassId(ClassId));
598 
599       return BG;
600     };
601 
602     auto InsertBlocks = [&](BatchGroup *BG, CompactPtrT *Array, u32 Size) {
603       SinglyLinkedList<TransferBatch> &Batches = BG->Batches;
604       TransferBatch *CurBatch = Batches.front();
605       DCHECK_NE(CurBatch, nullptr);
606 
607       for (u32 I = 0; I < Size;) {
608         DCHECK_GE(BG->MaxCachedPerBatch, CurBatch->getCount());
609         u16 UnusedSlots =
610             static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount());
611         if (UnusedSlots == 0) {
612           CurBatch = C->createBatch(
613               ClassId,
614               reinterpret_cast<void *>(decompactPtr(ClassId, Array[I])));
615           CurBatch->clear();
616           Batches.push_front(CurBatch);
617           UnusedSlots = BG->MaxCachedPerBatch;
618         }
619         // `UnusedSlots` is u16 so the result will be also fit in u16.
620         u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I));
621         CurBatch->appendFromArray(&Array[I], AppendSize);
622         I += AppendSize;
623       }
624 
625       BG->PushedBlocks += Size;
626     };
627 
628     BatchGroup *Cur = Region->FreeList.front();
629 
630     if (ClassId == SizeClassMap::BatchClassId) {
631       if (Cur == nullptr) {
632         // Don't need to classify BatchClassId.
633         Cur = CreateGroup(/*CompactPtrGroupBase=*/0);
634         Region->FreeList.push_front(Cur);
635       }
636       InsertBlocks(Cur, Array, Size);
637       return;
638     }
639 
640     // In the following, `Cur` always points to the BatchGroup for blocks that
641     // will be pushed next. `Prev` is the element right before `Cur`.
642     BatchGroup *Prev = nullptr;
643 
644     while (Cur != nullptr &&
645            compactPtrGroup(Array[0]) > Cur->CompactPtrGroupBase) {
646       Prev = Cur;
647       Cur = Cur->Next;
648     }
649 
650     if (Cur == nullptr ||
651         compactPtrGroup(Array[0]) != Cur->CompactPtrGroupBase) {
652       Cur = CreateGroup(compactPtrGroup(Array[0]));
653       if (Prev == nullptr)
654         Region->FreeList.push_front(Cur);
655       else
656         Region->FreeList.insert(Prev, Cur);
657     }
658 
659     // All the blocks are from the same group, just push without checking group
660     // id.
661     if (SameGroup) {
662       for (u32 I = 0; I < Size; ++I)
663         DCHECK_EQ(compactPtrGroup(Array[I]), Cur->CompactPtrGroupBase);
664 
665       InsertBlocks(Cur, Array, Size);
666       return;
667     }
668 
669     // The blocks are sorted by group id. Determine the segment of group and
670     // push them to their group together.
671     u32 Count = 1;
672     for (u32 I = 1; I < Size; ++I) {
673       if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I])) {
674         DCHECK_EQ(compactPtrGroup(Array[I - 1]), Cur->CompactPtrGroupBase);
675         InsertBlocks(Cur, Array + I - Count, Count);
676 
677         while (Cur != nullptr &&
678                compactPtrGroup(Array[I]) > Cur->CompactPtrGroupBase) {
679           Prev = Cur;
680           Cur = Cur->Next;
681         }
682 
683         if (Cur == nullptr ||
684             compactPtrGroup(Array[I]) != Cur->CompactPtrGroupBase) {
685           Cur = CreateGroup(compactPtrGroup(Array[I]));
686           DCHECK_NE(Prev, nullptr);
687           Region->FreeList.insert(Prev, Cur);
688         }
689 
690         Count = 1;
691       } else {
692         ++Count;
693       }
694     }
695 
696     InsertBlocks(Cur, Array + Size - Count, Count);
697   }
698 
699   // Pop one TransferBatch from a BatchGroup. The BatchGroup with the smallest
700   // group id will be considered first.
701   //
702   // The region mutex needs to be held while calling this method.
popBatchImpl(CacheT * C,uptr ClassId,RegionInfo * Region)703   TransferBatch *popBatchImpl(CacheT *C, uptr ClassId, RegionInfo *Region)
704       REQUIRES(Region->Mutex) {
705     if (Region->FreeList.empty())
706       return nullptr;
707 
708     SinglyLinkedList<TransferBatch> &Batches =
709         Region->FreeList.front()->Batches;
710     DCHECK(!Batches.empty());
711 
712     TransferBatch *B = Batches.front();
713     Batches.pop_front();
714     DCHECK_NE(B, nullptr);
715     DCHECK_GT(B->getCount(), 0U);
716 
717     if (Batches.empty()) {
718       BatchGroup *BG = Region->FreeList.front();
719       Region->FreeList.pop_front();
720 
721       // We don't keep BatchGroup with zero blocks to avoid empty-checking while
722       // allocating. Note that block used by constructing BatchGroup is recorded
723       // as free blocks in the last element of BatchGroup::Batches. Which means,
724       // once we pop the last TransferBatch, the block is implicitly
725       // deallocated.
726       if (ClassId != SizeClassMap::BatchClassId)
727         C->deallocate(SizeClassMap::BatchClassId, BG);
728     }
729 
730     return B;
731   }
732 
populateFreeList(CacheT * C,uptr ClassId,RegionInfo * Region)733   NOINLINE bool populateFreeList(CacheT *C, uptr ClassId, RegionInfo *Region)
734       REQUIRES(Region->Mutex) {
735     const uptr Size = getSizeByClassId(ClassId);
736     const u16 MaxCount = TransferBatch::getMaxCached(Size);
737 
738     const uptr RegionBeg = Region->RegionBeg;
739     const uptr MappedUser = Region->MappedUser;
740     const uptr TotalUserBytes = Region->AllocatedUser + MaxCount * Size;
741     // Map more space for blocks, if necessary.
742     if (TotalUserBytes > MappedUser) {
743       // Do the mmap for the user memory.
744       const uptr MapSize =
745           roundUp(TotalUserBytes - MappedUser, MapSizeIncrement);
746       const uptr RegionBase = RegionBeg - getRegionBaseByClassId(ClassId);
747       if (UNLIKELY(RegionBase + MappedUser + MapSize > RegionSize)) {
748         Region->Exhausted = true;
749         return false;
750       }
751       // TODO: Consider allocating MemMap in init().
752       if (!Region->MemMap.isAllocated()) {
753         // TODO: Ideally, a region should reserve RegionSize because the memory
754         // between `RegionBeg` and region base is still belong to a region and
755         // it's just not used. In order to make it work on every platform (some
756         // of them don't support `remap()` across the unused range), dispatch
757         // from `RegionBeg` for now.
758         const uptr ReserveSize =
759             RegionSize - (RegionBeg - getRegionBaseByClassId(ClassId));
760         Region->MemMap = ReservedMemory.dispatch(RegionBeg, ReserveSize);
761       }
762       DCHECK(Region->MemMap.isAllocated());
763 
764       if (UNLIKELY(!Region->MemMap.remap(
765               RegionBeg + MappedUser, MapSize, "scudo:primary",
766               MAP_ALLOWNOMEM | MAP_RESIZABLE |
767                   (useMemoryTagging<Config>(Options.load()) ? MAP_MEMTAG
768                                                             : 0)))) {
769         return false;
770       }
771       Region->MappedUser += MapSize;
772       C->getStats().add(StatMapped, MapSize);
773     }
774 
775     const u32 NumberOfBlocks = Min(
776         MaxNumBatches * MaxCount,
777         static_cast<u32>((Region->MappedUser - Region->AllocatedUser) / Size));
778     DCHECK_GT(NumberOfBlocks, 0);
779 
780     constexpr u32 ShuffleArraySize =
781         MaxNumBatches * TransferBatch::MaxNumCached;
782     CompactPtrT ShuffleArray[ShuffleArraySize];
783     DCHECK_LE(NumberOfBlocks, ShuffleArraySize);
784 
785     const uptr CompactPtrBase = getCompactPtrBaseByClassId(ClassId);
786     uptr P = RegionBeg + Region->AllocatedUser;
787     for (u32 I = 0; I < NumberOfBlocks; I++, P += Size)
788       ShuffleArray[I] = compactPtrInternal(CompactPtrBase, P);
789 
790     if (ClassId != SizeClassMap::BatchClassId) {
791       u32 N = 1;
792       uptr CurGroup = compactPtrGroup(ShuffleArray[0]);
793       for (u32 I = 1; I < NumberOfBlocks; I++) {
794         if (UNLIKELY(compactPtrGroup(ShuffleArray[I]) != CurGroup)) {
795           shuffle(ShuffleArray + I - N, N, &Region->RandState);
796           pushBlocksImpl(C, ClassId, Region, ShuffleArray + I - N, N,
797                          /*SameGroup=*/true);
798           N = 1;
799           CurGroup = compactPtrGroup(ShuffleArray[I]);
800         } else {
801           ++N;
802         }
803       }
804 
805       shuffle(ShuffleArray + NumberOfBlocks - N, N, &Region->RandState);
806       pushBlocksImpl(C, ClassId, Region, &ShuffleArray[NumberOfBlocks - N], N,
807                      /*SameGroup=*/true);
808     } else {
809       pushBlocksImpl(C, ClassId, Region, ShuffleArray, NumberOfBlocks,
810                      /*SameGroup=*/true);
811     }
812 
813     const uptr AllocatedUser = Size * NumberOfBlocks;
814     C->getStats().add(StatFree, AllocatedUser);
815     Region->AllocatedUser += AllocatedUser;
816 
817     return true;
818   }
819 
getStats(ScopedString * Str,uptr ClassId,RegionInfo * Region,uptr Rss)820   void getStats(ScopedString *Str, uptr ClassId, RegionInfo *Region, uptr Rss)
821       REQUIRES(Region->Mutex) {
822     if (Region->MappedUser == 0)
823       return;
824     const uptr InUse = Region->Stats.PoppedBlocks - Region->Stats.PushedBlocks;
825     const uptr TotalChunks = Region->AllocatedUser / getSizeByClassId(ClassId);
826     Str->append("%s %02zu (%6zu): mapped: %6zuK popped: %7zu pushed: %7zu "
827                 "inuse: %6zu total: %6zu rss: %6zuK releases: %6zu last "
828                 "released: %6zuK region: 0x%zx (0x%zx)\n",
829                 Region->Exhausted ? "F" : " ", ClassId,
830                 getSizeByClassId(ClassId), Region->MappedUser >> 10,
831                 Region->Stats.PoppedBlocks, Region->Stats.PushedBlocks, InUse,
832                 TotalChunks, Rss >> 10, Region->ReleaseInfo.RangesReleased,
833                 Region->ReleaseInfo.LastReleasedBytes >> 10, Region->RegionBeg,
834                 getRegionBaseByClassId(ClassId));
835   }
836 
837   NOINLINE uptr releaseToOSMaybe(RegionInfo *Region, uptr ClassId,
838                                  ReleaseToOS ReleaseType = ReleaseToOS::Normal)
839       REQUIRES(Region->Mutex) {
840     const uptr BlockSize = getSizeByClassId(ClassId);
841     const uptr PageSize = getPageSizeCached();
842 
843     DCHECK_GE(Region->Stats.PoppedBlocks, Region->Stats.PushedBlocks);
844     const uptr BytesInFreeList =
845         Region->AllocatedUser -
846         (Region->Stats.PoppedBlocks - Region->Stats.PushedBlocks) * BlockSize;
847 
848     if (UNLIKELY(BytesInFreeList == 0))
849       return 0;
850 
851     bool MaySkip = false;
852 
853     // Always update `BytesInFreeListAtLastCheckpoint` with the smallest value
854     // so that we won't underestimate the releasable pages. For example, the
855     // following is the region usage,
856     //
857     //  BytesInFreeListAtLastCheckpoint   AllocatedUser
858     //                v                         v
859     //  |--------------------------------------->
860     //         ^                   ^
861     //  BytesInFreeList     ReleaseThreshold
862     //
863     // In general, if we have collected enough bytes and the amount of free
864     // bytes meets the ReleaseThreshold, we will try to do page release. If we
865     // don't update `BytesInFreeListAtLastCheckpoint` when the current
866     // `BytesInFreeList` is smaller, we may take longer time to wait for enough
867     // freed blocks because we miss the bytes between
868     // (BytesInFreeListAtLastCheckpoint - BytesInFreeList).
869     if (BytesInFreeList <=
870         Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint) {
871       Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;
872       MaySkip = true;
873     }
874 
875     const uptr RegionPushedBytesDelta =
876         BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint;
877     if (RegionPushedBytesDelta < PageSize)
878       MaySkip = true;
879 
880     const bool CheckDensity = isSmallBlock(BlockSize);
881     // Releasing smaller blocks is expensive, so we want to make sure that a
882     // significant amount of bytes are free, and that there has been a good
883     // amount of batches pushed to the freelist before attempting to release.
884     if (CheckDensity) {
885       if (ReleaseType == ReleaseToOS::Normal &&
886           RegionPushedBytesDelta < Region->TryReleaseThreshold) {
887         MaySkip = true;
888       }
889     }
890 
891     if (MaySkip && ReleaseType != ReleaseToOS::ForceAll)
892       return 0;
893 
894     if (ReleaseType == ReleaseToOS::Normal) {
895       const s32 IntervalMs = atomic_load_relaxed(&ReleaseToOsIntervalMs);
896       if (IntervalMs < 0)
897         return 0;
898       if (Region->ReleaseInfo.LastReleaseAtNs +
899               static_cast<u64>(IntervalMs) * 1000000 >
900           getMonotonicTimeFast()) {
901         return 0; // Memory was returned recently.
902       }
903     }
904 
905     const uptr GroupSize = (1U << GroupSizeLog);
906     const uptr AllocatedUserEnd = Region->AllocatedUser + Region->RegionBeg;
907     const uptr CompactPtrBase = getCompactPtrBaseByClassId(ClassId);
908     auto DecompactPtr = [CompactPtrBase](CompactPtrT CompactPtr) {
909       return decompactPtrInternal(CompactPtrBase, CompactPtr);
910     };
911 
912     // Instead of always preparing PageMap for the entire region, we only do it
913     // for the range of releasing groups. To do that, the free-block marking
914     // process includes visiting BlockGroups twice.
915 
916     // The first visit is to determine the range of BatchGroups we are going to
917     // release. And we will extract those BatchGroups out and push into
918     // `GroupToRelease`.
919     SinglyLinkedList<BatchGroup> GroupToRelease;
920     GroupToRelease.clear();
921 
922     // This is only used for debugging to ensure the consistency of the number
923     // of groups.
924     uptr NumberOfBatchGroups = Region->FreeList.size();
925 
926     // We are examining each group and will take the minimum distance to the
927     // release threshold as the next Region::TryReleaseThreshold(). Note that if
928     // the size of free blocks has reached the release threshold, the distance
929     // to the next release will be PageSize * SmallerBlockReleasePageDelta. See
930     // the comment on `SmallerBlockReleasePageDelta` for more details.
931     uptr MinDistToThreshold = GroupSize;
932 
933     for (BatchGroup *BG = Region->FreeList.front(), *Prev = nullptr;
934          BG != nullptr;) {
935       // Group boundary is always GroupSize-aligned from CompactPtr base. The
936       // layout of memory groups is like,
937       //
938       //     (CompactPtrBase)
939       // #1 CompactPtrGroupBase   #2 CompactPtrGroupBase            ...
940       //           |                       |                       |
941       //           v                       v                       v
942       //           +-----------------------+-----------------------+
943       //            \                     / \                     /
944       //             ---   GroupSize   ---   ---   GroupSize   ---
945       //
946       // After decompacting the CompactPtrGroupBase, we expect the alignment
947       // property is held as well.
948       const uptr BatchGroupBase =
949           decompactGroupBase(CompactPtrBase, BG->CompactPtrGroupBase);
950       DCHECK_LE(Region->RegionBeg, BatchGroupBase);
951       DCHECK_GE(AllocatedUserEnd, BatchGroupBase);
952       DCHECK_EQ((Region->RegionBeg - BatchGroupBase) % GroupSize, 0U);
953       const uptr BatchGroupEnd = BatchGroupBase + GroupSize;
954       const uptr AllocatedGroupSize = AllocatedUserEnd >= BatchGroupEnd
955                                           ? GroupSize
956                                           : AllocatedUserEnd - BatchGroupBase;
957       if (AllocatedGroupSize == 0) {
958         Prev = BG;
959         BG = BG->Next;
960         continue;
961       }
962 
963       // TransferBatches are pushed in front of BG.Batches. The first one may
964       // not have all caches used.
965       const uptr NumBlocks = (BG->Batches.size() - 1) * BG->MaxCachedPerBatch +
966                              BG->Batches.front()->getCount();
967       const uptr BytesInBG = NumBlocks * BlockSize;
968 
969       if (ReleaseType != ReleaseToOS::ForceAll &&
970           BytesInBG <= BG->BytesInBGAtLastCheckpoint) {
971         BG->BytesInBGAtLastCheckpoint = BytesInBG;
972         Prev = BG;
973         BG = BG->Next;
974         continue;
975       }
976 
977       const uptr PushedBytesDelta = BG->BytesInBGAtLastCheckpoint - BytesInBG;
978 
979       // Given the randomness property, we try to release the pages only if the
980       // bytes used by free blocks exceed certain proportion of group size. Note
981       // that this heuristic only applies when all the spaces in a BatchGroup
982       // are allocated.
983       if (CheckDensity) {
984         const uptr ReleaseThreshold =
985             (AllocatedGroupSize * (100 - 1U - BlockSize / 16U)) / 100U;
986         const bool HighDensity = BytesInBG >= ReleaseThreshold;
987         const bool MayHaveReleasedAll = NumBlocks >= (GroupSize / BlockSize);
988         // If all blocks in the group are released, we will do range marking
989         // which is fast. Otherwise, we will wait until we have accumulated
990         // a certain amount of free memory.
991         const bool ReachReleaseDelta =
992             MayHaveReleasedAll
993                 ? true
994                 : PushedBytesDelta >= PageSize * SmallerBlockReleasePageDelta;
995 
996         if (!HighDensity) {
997           DCHECK_LE(BytesInBG, ReleaseThreshold);
998           // The following is the usage of a memroy group,
999           //
1000           //     BytesInBG             ReleaseThreshold
1001           //  /             \                 v
1002           //  +---+---------------------------+-----+
1003           //  |   |         |                 |     |
1004           //  +---+---------------------------+-----+
1005           //       \        /                       ^
1006           //    PushedBytesDelta                 GroupEnd
1007           MinDistToThreshold =
1008               Min(MinDistToThreshold,
1009                   ReleaseThreshold - BytesInBG + PushedBytesDelta);
1010         } else {
1011           // If it reaches high density at this round, the next time we will try
1012           // to release is based on SmallerBlockReleasePageDelta
1013           MinDistToThreshold =
1014               Min(MinDistToThreshold, PageSize * SmallerBlockReleasePageDelta);
1015         }
1016 
1017         if (!HighDensity || !ReachReleaseDelta) {
1018           Prev = BG;
1019           BG = BG->Next;
1020           continue;
1021         }
1022       }
1023 
1024       // If `BG` is the first BatchGroup in the list, we only need to advance
1025       // `BG` and call FreeList::pop_front(). No update is needed for `Prev`.
1026       //
1027       //         (BG)   (BG->Next)
1028       // Prev     Cur      BG
1029       //   |       |       |
1030       //   v       v       v
1031       //  nil     +--+    +--+
1032       //          |X | -> |  | -> ...
1033       //          +--+    +--+
1034       //
1035       // Otherwise, `Prev` will be used to extract the `Cur` from the
1036       // `FreeList`.
1037       //
1038       //         (BG)   (BG->Next)
1039       // Prev     Cur      BG
1040       //   |       |       |
1041       //   v       v       v
1042       //  +--+    +--+    +--+
1043       //  |  | -> |X | -> |  | -> ...
1044       //  +--+    +--+    +--+
1045       //
1046       // After FreeList::extract(),
1047       //
1048       // Prev     Cur       BG
1049       //   |       |        |
1050       //   v       v        v
1051       //  +--+    +--+     +--+
1052       //  |  |-+  |X |  +->|  | -> ...
1053       //  +--+ |  +--+  |  +--+
1054       //       +--------+
1055       //
1056       // Note that we need to advance before pushing this BatchGroup to
1057       // GroupToRelease because it's a destructive operation.
1058 
1059       BatchGroup *Cur = BG;
1060       BG = BG->Next;
1061 
1062       // Ideally, we may want to update this only after successful release.
1063       // However, for smaller blocks, each block marking is a costly operation.
1064       // Therefore, we update it earlier.
1065       // TODO: Consider updating this after page release if `ReleaseRecorder`
1066       // can tell the releasd bytes in each group.
1067       Cur->BytesInBGAtLastCheckpoint = BytesInBG;
1068 
1069       if (Prev != nullptr)
1070         Region->FreeList.extract(Prev, Cur);
1071       else
1072         Region->FreeList.pop_front();
1073       GroupToRelease.push_back(Cur);
1074     }
1075 
1076     // Only small blocks have the adaptive `TryReleaseThreshold`.
1077     if (isSmallBlock(BlockSize)) {
1078       // If the MinDistToThreshold is not updated, that means each memory group
1079       // may have only pushed less than a page size. In that case, just set it
1080       // back to normal.
1081       if (MinDistToThreshold == GroupSize)
1082         MinDistToThreshold = PageSize * SmallerBlockReleasePageDelta;
1083       Region->TryReleaseThreshold = MinDistToThreshold;
1084     }
1085 
1086     if (GroupToRelease.empty())
1087       return 0;
1088 
1089     const uptr ReleaseBase = decompactGroupBase(
1090         CompactPtrBase, GroupToRelease.front()->CompactPtrGroupBase);
1091     const uptr LastGroupEnd =
1092         Min(decompactGroupBase(CompactPtrBase,
1093                                GroupToRelease.back()->CompactPtrGroupBase) +
1094                 GroupSize,
1095             AllocatedUserEnd);
1096     // The last block may straddle the group boundary. Rounding up to BlockSize
1097     // to get the exact range.
1098     const uptr ReleaseEnd =
1099         roundUpSlow(LastGroupEnd - Region->RegionBeg, BlockSize) +
1100         Region->RegionBeg;
1101     const uptr ReleaseRangeSize = ReleaseEnd - ReleaseBase;
1102     const uptr ReleaseOffset = ReleaseBase - Region->RegionBeg;
1103 
1104     RegionReleaseRecorder<MemMapT> Recorder(&Region->MemMap, Region->RegionBeg,
1105                                             ReleaseOffset);
1106     PageReleaseContext Context(BlockSize, /*NumberOfRegions=*/1U,
1107                                ReleaseRangeSize, ReleaseOffset);
1108     // We may not be able to do the page release in a rare case that we may
1109     // fail on PageMap allocation.
1110     if (UNLIKELY(!Context.ensurePageMapAllocated()))
1111       return 0;
1112 
1113     for (BatchGroup &BG : GroupToRelease) {
1114       const uptr BatchGroupBase =
1115           decompactGroupBase(CompactPtrBase, BG.CompactPtrGroupBase);
1116       const uptr BatchGroupEnd = BatchGroupBase + GroupSize;
1117       const uptr AllocatedGroupSize = AllocatedUserEnd >= BatchGroupEnd
1118                                           ? GroupSize
1119                                           : AllocatedUserEnd - BatchGroupBase;
1120       const uptr BatchGroupUsedEnd = BatchGroupBase + AllocatedGroupSize;
1121       const bool MayContainLastBlockInRegion =
1122           BatchGroupUsedEnd == AllocatedUserEnd;
1123       const bool BlockAlignedWithUsedEnd =
1124           (BatchGroupUsedEnd - Region->RegionBeg) % BlockSize == 0;
1125 
1126       uptr MaxContainedBlocks = AllocatedGroupSize / BlockSize;
1127       if (!BlockAlignedWithUsedEnd)
1128         ++MaxContainedBlocks;
1129 
1130       const uptr NumBlocks = (BG.Batches.size() - 1) * BG.MaxCachedPerBatch +
1131                              BG.Batches.front()->getCount();
1132 
1133       if (NumBlocks == MaxContainedBlocks) {
1134         for (const auto &It : BG.Batches)
1135           for (u16 I = 0; I < It.getCount(); ++I)
1136             DCHECK_EQ(compactPtrGroup(It.get(I)), BG.CompactPtrGroupBase);
1137 
1138         Context.markRangeAsAllCounted(BatchGroupBase, BatchGroupUsedEnd,
1139                                       Region->RegionBeg, /*RegionIndex=*/0,
1140                                       Region->AllocatedUser);
1141       } else {
1142         DCHECK_LT(NumBlocks, MaxContainedBlocks);
1143         // Note that we don't always visit blocks in each BatchGroup so that we
1144         // may miss the chance of releasing certain pages that cross
1145         // BatchGroups.
1146         Context.markFreeBlocksInRegion(
1147             BG.Batches, DecompactPtr, Region->RegionBeg, /*RegionIndex=*/0,
1148             Region->AllocatedUser, MayContainLastBlockInRegion);
1149       }
1150     }
1151 
1152     DCHECK(Context.hasBlockMarked());
1153 
1154     auto SkipRegion = [](UNUSED uptr RegionIndex) { return false; };
1155     releaseFreeMemoryToOS(Context, Recorder, SkipRegion);
1156 
1157     if (Recorder.getReleasedRangesCount() > 0) {
1158       Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;
1159       Region->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount();
1160       Region->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes();
1161     }
1162     Region->ReleaseInfo.LastReleaseAtNs = getMonotonicTimeFast();
1163 
1164     // Merge GroupToRelease back to the Region::FreeList. Note that both
1165     // `Region->FreeList` and `GroupToRelease` are sorted.
1166     for (BatchGroup *BG = Region->FreeList.front(), *Prev = nullptr;;) {
1167       if (BG == nullptr || GroupToRelease.empty()) {
1168         if (!GroupToRelease.empty())
1169           Region->FreeList.append_back(&GroupToRelease);
1170         break;
1171       }
1172 
1173       DCHECK_NE(BG->CompactPtrGroupBase,
1174                 GroupToRelease.front()->CompactPtrGroupBase);
1175 
1176       if (BG->CompactPtrGroupBase <
1177           GroupToRelease.front()->CompactPtrGroupBase) {
1178         Prev = BG;
1179         BG = BG->Next;
1180         continue;
1181       }
1182 
1183       // At here, the `BG` is the first BatchGroup with CompactPtrGroupBase
1184       // larger than the first element in `GroupToRelease`. We need to insert
1185       // `GroupToRelease::front()` (which is `Cur` below)  before `BG`.
1186       //
1187       //   1. If `Prev` is nullptr, we simply push `Cur` to the front of
1188       //      FreeList.
1189       //   2. Otherwise, use `insert()` which inserts an element next to `Prev`.
1190       //
1191       // Afterwards, we don't need to advance `BG` because the order between
1192       // `BG` and the new `GroupToRelease::front()` hasn't been checked.
1193       BatchGroup *Cur = GroupToRelease.front();
1194       GroupToRelease.pop_front();
1195       if (Prev == nullptr)
1196         Region->FreeList.push_front(Cur);
1197       else
1198         Region->FreeList.insert(Prev, Cur);
1199       DCHECK_EQ(Cur->Next, BG);
1200       Prev = Cur;
1201     }
1202 
1203     DCHECK_EQ(Region->FreeList.size(), NumberOfBatchGroups);
1204     (void)NumberOfBatchGroups;
1205 
1206     if (SCUDO_DEBUG) {
1207       BatchGroup *Prev = Region->FreeList.front();
1208       for (BatchGroup *Cur = Prev->Next; Cur != nullptr;
1209            Prev = Cur, Cur = Cur->Next) {
1210         CHECK_LT(Prev->CompactPtrGroupBase, Cur->CompactPtrGroupBase);
1211       }
1212     }
1213 
1214     return Recorder.getReleasedBytes();
1215   }
1216 };
1217 
1218 } // namespace scudo
1219 
1220 #endif // SCUDO_PRIMARY64_H_
1221