1//===-- primary32.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_PRIMARY32_H_
10#define SCUDO_PRIMARY32_H_
11
12#include "bytemap.h"
13#include "common.h"
14#include "list.h"
15#include "local_cache.h"
16#include "options.h"
17#include "release.h"
18#include "report.h"
19#include "stats.h"
20#include "string_utils.h"
21
22namespace scudo {
23
24// SizeClassAllocator32 is an allocator for 32 or 64-bit address space.
25//
26// It maps Regions of 2^RegionSizeLog bytes aligned on a 2^RegionSizeLog bytes
27// boundary, and keeps a bytemap of the mappable address space to track the size
28// class they are associated with.
29//
30// Mapped regions are split into equally sized Blocks according to the size
31// class they belong to, and the associated pointers are shuffled to prevent any
32// predictable address pattern (the predictability increases with the block
33// size).
34//
35// Regions for size class 0 are special and used to hold TransferBatches, which
36// allow to transfer arrays of pointers from the global size class freelist to
37// the thread specific freelist for said class, and back.
38//
39// Memory used by this allocator is never unmapped but can be partially
40// reclaimed if the platform allows for it.
41
42template <typename Config> class SizeClassAllocator32 {
43public:
44  typedef typename Config::PrimaryCompactPtrT CompactPtrT;
45  typedef typename Config::SizeClassMap SizeClassMap;
46  static const uptr GroupSizeLog = Config::PrimaryGroupSizeLog;
47  // The bytemap can only track UINT8_MAX - 1 classes.
48  static_assert(SizeClassMap::LargestClassId <= (UINT8_MAX - 1), "");
49  // Regions should be large enough to hold the largest Block.
50  static_assert((1UL << Config::PrimaryRegionSizeLog) >= SizeClassMap::MaxSize,
51                "");
52  typedef SizeClassAllocator32<Config> ThisT;
53  typedef SizeClassAllocatorLocalCache<ThisT> CacheT;
54  typedef typename CacheT::TransferBatch TransferBatch;
55  typedef typename CacheT::BatchGroup BatchGroup;
56
57  static uptr getSizeByClassId(uptr ClassId) {
58    return (ClassId == SizeClassMap::BatchClassId)
59               ? sizeof(TransferBatch)
60               : SizeClassMap::getSizeByClassId(ClassId);
61  }
62
63  static bool canAllocate(uptr Size) { return Size <= SizeClassMap::MaxSize; }
64
65  void init(s32 ReleaseToOsInterval) {
66    if (SCUDO_FUCHSIA)
67      reportError("SizeClassAllocator32 is not supported on Fuchsia");
68
69    if (SCUDO_TRUSTY)
70      reportError("SizeClassAllocator32 is not supported on Trusty");
71
72    DCHECK(isAligned(reinterpret_cast<uptr>(this), alignof(ThisT)));
73    PossibleRegions.init();
74    u32 Seed;
75    const u64 Time = getMonotonicTime();
76    if (!getRandom(reinterpret_cast<void *>(&Seed), sizeof(Seed)))
77      Seed = static_cast<u32>(
78          Time ^ (reinterpret_cast<uptr>(SizeClassInfoArray) >> 6));
79    for (uptr I = 0; I < NumClasses; I++) {
80      SizeClassInfo *Sci = getSizeClassInfo(I);
81      Sci->RandState = getRandomU32(&Seed);
82      // Sci->MaxRegionIndex is already initialized to 0.
83      Sci->MinRegionIndex = NumRegions;
84      Sci->ReleaseInfo.LastReleaseAtNs = Time;
85    }
86    setOption(Option::ReleaseInterval, static_cast<sptr>(ReleaseToOsInterval));
87  }
88
89  void unmapTestOnly() {
90    while (NumberOfStashedRegions > 0)
91      unmap(reinterpret_cast<void *>(RegionsStash[--NumberOfStashedRegions]),
92            RegionSize);
93    uptr MinRegionIndex = NumRegions, MaxRegionIndex = 0;
94    for (uptr I = 0; I < NumClasses; I++) {
95      SizeClassInfo *Sci = getSizeClassInfo(I);
96      if (Sci->MinRegionIndex < MinRegionIndex)
97        MinRegionIndex = Sci->MinRegionIndex;
98      if (Sci->MaxRegionIndex > MaxRegionIndex)
99        MaxRegionIndex = Sci->MaxRegionIndex;
100      *Sci = {};
101    }
102    for (uptr I = MinRegionIndex; I < MaxRegionIndex; I++)
103      if (PossibleRegions[I])
104        unmap(reinterpret_cast<void *>(I * RegionSize), RegionSize);
105    PossibleRegions.unmapTestOnly();
106  }
107
108  CompactPtrT compactPtr(UNUSED uptr ClassId, uptr Ptr) const {
109    return static_cast<CompactPtrT>(Ptr);
110  }
111
112  void *decompactPtr(UNUSED uptr ClassId, CompactPtrT CompactPtr) const {
113    return reinterpret_cast<void *>(static_cast<uptr>(CompactPtr));
114  }
115
116  uptr compactPtrGroup(CompactPtrT CompactPtr) {
117    return CompactPtr >> GroupSizeLog;
118  }
119
120  TransferBatch *popBatch(CacheT *C, uptr ClassId) {
121    DCHECK_LT(ClassId, NumClasses);
122    SizeClassInfo *Sci = getSizeClassInfo(ClassId);
123    ScopedLock L(Sci->Mutex);
124    TransferBatch *B = popBatchImpl(C, ClassId);
125    if (UNLIKELY(!B)) {
126      if (UNLIKELY(!populateFreeList(C, ClassId, Sci)))
127        return nullptr;
128      B = popBatchImpl(C, ClassId);
129      // if `populateFreeList` succeeded, we are supposed to get free blocks.
130      DCHECK_NE(B, nullptr);
131    }
132    Sci->Stats.PoppedBlocks += B->getCount();
133    return B;
134  }
135
136  // Push the array of free blocks to the designated batch group.
137  void pushBlocks(CacheT *C, uptr ClassId, CompactPtrT *Array, u32 Size) {
138    DCHECK_LT(ClassId, NumClasses);
139    DCHECK_GT(Size, 0);
140
141    SizeClassInfo *Sci = getSizeClassInfo(ClassId);
142    if (ClassId == SizeClassMap::BatchClassId) {
143      ScopedLock L(Sci->Mutex);
144      // Constructing a batch group in the free list will use two blocks in
145      // BatchClassId. If we are pushing BatchClassId blocks, we will use the
146      // blocks in the array directly (can't delegate local cache which will
147      // cause a recursive allocation). However, The number of free blocks may
148      // be less than two. Therefore, populate the free list before inserting
149      // the blocks.
150      if (Size == 1 && !populateFreeList(C, ClassId, Sci))
151        return;
152      pushBlocksImpl(C, ClassId, Array, Size);
153      Sci->Stats.PushedBlocks += Size;
154      return;
155    }
156
157    // TODO(chiahungduan): Consider not doing grouping if the group size is not
158    // greater than the block size with a certain scale.
159
160    // Sort the blocks so that blocks belonging to the same group can be pushed
161    // together.
162    bool SameGroup = true;
163    for (u32 I = 1; I < Size; ++I) {
164      if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I]))
165        SameGroup = false;
166      CompactPtrT Cur = Array[I];
167      u32 J = I;
168      while (J > 0 && compactPtrGroup(Cur) < compactPtrGroup(Array[J - 1])) {
169        Array[J] = Array[J - 1];
170        --J;
171      }
172      Array[J] = Cur;
173    }
174
175    ScopedLock L(Sci->Mutex);
176    pushBlocksImpl(C, ClassId, Array, Size, SameGroup);
177
178    Sci->Stats.PushedBlocks += Size;
179    if (ClassId != SizeClassMap::BatchClassId)
180      releaseToOSMaybe(Sci, ClassId);
181  }
182
183  void disable() {
184    // The BatchClassId must be locked last since other classes can use it.
185    for (sptr I = static_cast<sptr>(NumClasses) - 1; I >= 0; I--) {
186      if (static_cast<uptr>(I) == SizeClassMap::BatchClassId)
187        continue;
188      getSizeClassInfo(static_cast<uptr>(I))->Mutex.lock();
189    }
190    getSizeClassInfo(SizeClassMap::BatchClassId)->Mutex.lock();
191    RegionsStashMutex.lock();
192    PossibleRegions.disable();
193  }
194
195  void enable() {
196    PossibleRegions.enable();
197    RegionsStashMutex.unlock();
198    getSizeClassInfo(SizeClassMap::BatchClassId)->Mutex.unlock();
199    for (uptr I = 0; I < NumClasses; I++) {
200      if (I == SizeClassMap::BatchClassId)
201        continue;
202      getSizeClassInfo(I)->Mutex.unlock();
203    }
204  }
205
206  template <typename F> void iterateOverBlocks(F Callback) {
207    uptr MinRegionIndex = NumRegions, MaxRegionIndex = 0;
208    for (uptr I = 0; I < NumClasses; I++) {
209      SizeClassInfo *Sci = getSizeClassInfo(I);
210      if (Sci->MinRegionIndex < MinRegionIndex)
211        MinRegionIndex = Sci->MinRegionIndex;
212      if (Sci->MaxRegionIndex > MaxRegionIndex)
213        MaxRegionIndex = Sci->MaxRegionIndex;
214    }
215    for (uptr I = MinRegionIndex; I <= MaxRegionIndex; I++)
216      if (PossibleRegions[I] &&
217          (PossibleRegions[I] - 1U) != SizeClassMap::BatchClassId) {
218        const uptr BlockSize = getSizeByClassId(PossibleRegions[I] - 1U);
219        const uptr From = I * RegionSize;
220        const uptr To = From + (RegionSize / BlockSize) * BlockSize;
221        for (uptr Block = From; Block < To; Block += BlockSize)
222          Callback(Block);
223      }
224  }
225
226  void getStats(ScopedString *Str) {
227    // TODO(kostyak): get the RSS per region.
228    uptr TotalMapped = 0;
229    uptr PoppedBlocks = 0;
230    uptr PushedBlocks = 0;
231    for (uptr I = 0; I < NumClasses; I++) {
232      SizeClassInfo *Sci = getSizeClassInfo(I);
233      TotalMapped += Sci->AllocatedUser;
234      PoppedBlocks += Sci->Stats.PoppedBlocks;
235      PushedBlocks += Sci->Stats.PushedBlocks;
236    }
237    Str->append("Stats: SizeClassAllocator32: %zuM mapped in %zu allocations; "
238                "remains %zu\n",
239                TotalMapped >> 20, PoppedBlocks, PoppedBlocks - PushedBlocks);
240    for (uptr I = 0; I < NumClasses; I++)
241      getStats(Str, I, 0);
242  }
243
244  bool setOption(Option O, sptr Value) {
245    if (O == Option::ReleaseInterval) {
246      const s32 Interval = Max(
247          Min(static_cast<s32>(Value), Config::PrimaryMaxReleaseToOsIntervalMs),
248          Config::PrimaryMinReleaseToOsIntervalMs);
249      atomic_store_relaxed(&ReleaseToOsIntervalMs, Interval);
250      return true;
251    }
252    // Not supported by the Primary, but not an error either.
253    return true;
254  }
255
256  uptr releaseToOS() {
257    uptr TotalReleasedBytes = 0;
258    for (uptr I = 0; I < NumClasses; I++) {
259      if (I == SizeClassMap::BatchClassId)
260        continue;
261      SizeClassInfo *Sci = getSizeClassInfo(I);
262      ScopedLock L(Sci->Mutex);
263      TotalReleasedBytes += releaseToOSMaybe(Sci, I, /*Force=*/true);
264    }
265    return TotalReleasedBytes;
266  }
267
268  const char *getRegionInfoArrayAddress() const { return nullptr; }
269  static uptr getRegionInfoArraySize() { return 0; }
270
271  static BlockInfo findNearestBlock(UNUSED const char *RegionInfoData,
272                                    UNUSED uptr Ptr) {
273    return {};
274  }
275
276  AtomicOptions Options;
277
278private:
279  static const uptr NumClasses = SizeClassMap::NumClasses;
280  static const uptr RegionSize = 1UL << Config::PrimaryRegionSizeLog;
281  static const uptr NumRegions =
282      SCUDO_MMAP_RANGE_SIZE >> Config::PrimaryRegionSizeLog;
283  static const u32 MaxNumBatches = SCUDO_ANDROID ? 4U : 8U;
284  typedef FlatByteMap<NumRegions> ByteMap;
285
286  struct SizeClassStats {
287    uptr PoppedBlocks;
288    uptr PushedBlocks;
289  };
290
291  struct ReleaseToOsInfo {
292    uptr PushedBlocksAtLastRelease;
293    uptr RangesReleased;
294    uptr LastReleasedBytes;
295    u64 LastReleaseAtNs;
296  };
297
298  struct alignas(SCUDO_CACHE_LINE_SIZE) SizeClassInfo {
299    HybridMutex Mutex;
300    SinglyLinkedList<BatchGroup> FreeList;
301    uptr CurrentRegion;
302    uptr CurrentRegionAllocated;
303    SizeClassStats Stats;
304    u32 RandState;
305    uptr AllocatedUser;
306    // Lowest & highest region index allocated for this size class, to avoid
307    // looping through the whole NumRegions.
308    uptr MinRegionIndex;
309    uptr MaxRegionIndex;
310    ReleaseToOsInfo ReleaseInfo;
311  };
312  static_assert(sizeof(SizeClassInfo) % SCUDO_CACHE_LINE_SIZE == 0, "");
313
314  uptr computeRegionId(uptr Mem) {
315    const uptr Id = Mem >> Config::PrimaryRegionSizeLog;
316    CHECK_LT(Id, NumRegions);
317    return Id;
318  }
319
320  uptr allocateRegionSlow() {
321    uptr MapSize = 2 * RegionSize;
322    const uptr MapBase = reinterpret_cast<uptr>(
323        map(nullptr, MapSize, "scudo:primary", MAP_ALLOWNOMEM));
324    if (!MapBase)
325      return 0;
326    const uptr MapEnd = MapBase + MapSize;
327    uptr Region = MapBase;
328    if (isAligned(Region, RegionSize)) {
329      ScopedLock L(RegionsStashMutex);
330      if (NumberOfStashedRegions < MaxStashedRegions)
331        RegionsStash[NumberOfStashedRegions++] = MapBase + RegionSize;
332      else
333        MapSize = RegionSize;
334    } else {
335      Region = roundUpTo(MapBase, RegionSize);
336      unmap(reinterpret_cast<void *>(MapBase), Region - MapBase);
337      MapSize = RegionSize;
338    }
339    const uptr End = Region + MapSize;
340    if (End != MapEnd)
341      unmap(reinterpret_cast<void *>(End), MapEnd - End);
342    return Region;
343  }
344
345  uptr allocateRegion(SizeClassInfo *Sci, uptr ClassId) {
346    DCHECK_LT(ClassId, NumClasses);
347    uptr Region = 0;
348    {
349      ScopedLock L(RegionsStashMutex);
350      if (NumberOfStashedRegions > 0)
351        Region = RegionsStash[--NumberOfStashedRegions];
352    }
353    if (!Region)
354      Region = allocateRegionSlow();
355    if (LIKELY(Region)) {
356      // Sci->Mutex is held by the caller, updating the Min/Max is safe.
357      const uptr RegionIndex = computeRegionId(Region);
358      if (RegionIndex < Sci->MinRegionIndex)
359        Sci->MinRegionIndex = RegionIndex;
360      if (RegionIndex > Sci->MaxRegionIndex)
361        Sci->MaxRegionIndex = RegionIndex;
362      PossibleRegions.set(RegionIndex, static_cast<u8>(ClassId + 1U));
363    }
364    return Region;
365  }
366
367  SizeClassInfo *getSizeClassInfo(uptr ClassId) {
368    DCHECK_LT(ClassId, NumClasses);
369    return &SizeClassInfoArray[ClassId];
370  }
371
372  // Push the blocks to their batch group. The layout will be like,
373  //
374  // FreeList - > BG -> BG -> BG
375  //              |     |     |
376  //              v     v     v
377  //              TB    TB    TB
378  //              |
379  //              v
380  //              TB
381  //
382  // Each BlockGroup(BG) will associate with unique group id and the free blocks
383  // are managed by a list of TransferBatch(TB). To reduce the time of inserting
384  // blocks, BGs are sorted and the input `Array` are supposed to be sorted so
385  // that we can get better performance of maintaining sorted property.
386  // Use `SameGroup=true` to indicate that all blocks in the array are from the
387  // same group then we will skip checking the group id of each block.
388  //
389  // Note that this aims to have a better management of dirty pages, i.e., the
390  // RSS usage won't grow indefinitely. There's an exception that we may not put
391  // a block to its associated group. While populating new blocks, we may have
392  // blocks cross different groups. However, most cases will fall into same
393  // group and they are supposed to be popped soon. In that case, it's not worth
394  // sorting the array with the almost-sorted property. Therefore, we use
395  // `SameGroup=true` instead.
396  //
397  // The region mutex needs to be held while calling this method.
398  void pushBlocksImpl(CacheT *C, uptr ClassId, CompactPtrT *Array, u32 Size,
399                      bool SameGroup = false) {
400    DCHECK_GT(Size, 0U);
401    SizeClassInfo *Sci = getSizeClassInfo(ClassId);
402
403    auto CreateGroup = [&](uptr GroupId) {
404      BatchGroup *BG = nullptr;
405      TransferBatch *TB = nullptr;
406      if (ClassId == SizeClassMap::BatchClassId) {
407        DCHECK_GE(Size, 2U);
408        BG = reinterpret_cast<BatchGroup *>(
409            decompactPtr(ClassId, Array[Size - 1]));
410        BG->Batches.clear();
411
412        TB = reinterpret_cast<TransferBatch *>(
413            decompactPtr(ClassId, Array[Size - 2]));
414        TB->clear();
415      } else {
416        BG = C->createGroup();
417        BG->Batches.clear();
418
419        TB = C->createBatch(ClassId, nullptr);
420        TB->clear();
421      }
422
423      BG->GroupId = GroupId;
424      BG->Batches.push_front(TB);
425      BG->PushedBlocks = 0;
426      BG->PushedBlocksAtLastCheckpoint = 0;
427      BG->MaxCachedPerBatch =
428          TransferBatch::getMaxCached(getSizeByClassId(ClassId));
429
430      return BG;
431    };
432
433    auto InsertBlocks = [&](BatchGroup *BG, CompactPtrT *Array, u32 Size) {
434      SinglyLinkedList<TransferBatch> &Batches = BG->Batches;
435      TransferBatch *CurBatch = Batches.front();
436      DCHECK_NE(CurBatch, nullptr);
437
438      for (u32 I = 0; I < Size;) {
439        DCHECK_GE(BG->MaxCachedPerBatch, CurBatch->getCount());
440        u16 UnusedSlots =
441            static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount());
442        if (UnusedSlots == 0) {
443          CurBatch = C->createBatch(
444              ClassId,
445              reinterpret_cast<void *>(decompactPtr(ClassId, Array[I])));
446          CurBatch->clear();
447          Batches.push_front(CurBatch);
448          UnusedSlots = BG->MaxCachedPerBatch;
449        }
450        // `UnusedSlots` is u16 so the result will be also fit in u16.
451        u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I));
452        CurBatch->appendFromArray(&Array[I], AppendSize);
453        I += AppendSize;
454      }
455
456      BG->PushedBlocks += Size;
457    };
458
459    BatchGroup *Cur = Sci->FreeList.front();
460
461    if (ClassId == SizeClassMap::BatchClassId) {
462      if (Cur == nullptr) {
463        // Don't need to classify BatchClassId.
464        Cur = CreateGroup(/*GroupId=*/0);
465        Sci->FreeList.push_front(Cur);
466      }
467      InsertBlocks(Cur, Array, Size);
468      return;
469    }
470
471    // In the following, `Cur` always points to the BatchGroup for blocks that
472    // will be pushed next. `Prev` is the element right before `Cur`.
473    BatchGroup *Prev = nullptr;
474
475    while (Cur != nullptr && compactPtrGroup(Array[0]) > Cur->GroupId) {
476      Prev = Cur;
477      Cur = Cur->Next;
478    }
479
480    if (Cur == nullptr || compactPtrGroup(Array[0]) != Cur->GroupId) {
481      Cur = CreateGroup(compactPtrGroup(Array[0]));
482      if (Prev == nullptr)
483        Sci->FreeList.push_front(Cur);
484      else
485        Sci->FreeList.insert(Prev, Cur);
486    }
487
488    // All the blocks are from the same group, just push without checking group
489    // id.
490    if (SameGroup) {
491      InsertBlocks(Cur, Array, Size);
492      return;
493    }
494
495    // The blocks are sorted by group id. Determine the segment of group and
496    // push them to their group together.
497    u32 Count = 1;
498    for (u32 I = 1; I < Size; ++I) {
499      if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I])) {
500        DCHECK_EQ(compactPtrGroup(Array[I - 1]), Cur->GroupId);
501        InsertBlocks(Cur, Array + I - Count, Count);
502
503        while (Cur != nullptr && compactPtrGroup(Array[I]) > Cur->GroupId) {
504          Prev = Cur;
505          Cur = Cur->Next;
506        }
507
508        if (Cur == nullptr || compactPtrGroup(Array[I]) != Cur->GroupId) {
509          Cur = CreateGroup(compactPtrGroup(Array[I]));
510          DCHECK_NE(Prev, nullptr);
511          Sci->FreeList.insert(Prev, Cur);
512        }
513
514        Count = 1;
515      } else {
516        ++Count;
517      }
518    }
519
520    InsertBlocks(Cur, Array + Size - Count, Count);
521  }
522
523  // Pop one TransferBatch from a BatchGroup. The BatchGroup with the smallest
524  // group id will be considered first.
525  //
526  // The region mutex needs to be held while calling this method.
527  TransferBatch *popBatchImpl(CacheT *C, uptr ClassId) {
528    SizeClassInfo *Sci = getSizeClassInfo(ClassId);
529    if (Sci->FreeList.empty())
530      return nullptr;
531
532    SinglyLinkedList<TransferBatch> &Batches = Sci->FreeList.front()->Batches;
533    DCHECK(!Batches.empty());
534
535    TransferBatch *B = Batches.front();
536    Batches.pop_front();
537    DCHECK_NE(B, nullptr);
538    DCHECK_GT(B->getCount(), 0U);
539
540    if (Batches.empty()) {
541      BatchGroup *BG = Sci->FreeList.front();
542      Sci->FreeList.pop_front();
543
544      // We don't keep BatchGroup with zero blocks to avoid empty-checking while
545      // allocating. Note that block used by constructing BatchGroup is recorded
546      // as free blocks in the last element of BatchGroup::Batches. Which means,
547      // once we pop the last TransferBatch, the block is implicitly
548      // deallocated.
549      if (ClassId != SizeClassMap::BatchClassId)
550        C->deallocate(SizeClassMap::BatchClassId, BG);
551    }
552
553    return B;
554  }
555
556  NOINLINE bool populateFreeList(CacheT *C, uptr ClassId, SizeClassInfo *Sci) {
557    uptr Region;
558    uptr Offset;
559    // If the size-class currently has a region associated to it, use it. The
560    // newly created blocks will be located after the currently allocated memory
561    // for that region (up to RegionSize). Otherwise, create a new region, where
562    // the new blocks will be carved from the beginning.
563    if (Sci->CurrentRegion) {
564      Region = Sci->CurrentRegion;
565      DCHECK_GT(Sci->CurrentRegionAllocated, 0U);
566      Offset = Sci->CurrentRegionAllocated;
567    } else {
568      DCHECK_EQ(Sci->CurrentRegionAllocated, 0U);
569      Region = allocateRegion(Sci, ClassId);
570      if (UNLIKELY(!Region))
571        return false;
572      C->getStats().add(StatMapped, RegionSize);
573      Sci->CurrentRegion = Region;
574      Offset = 0;
575    }
576
577    const uptr Size = getSizeByClassId(ClassId);
578    const u16 MaxCount = TransferBatch::getMaxCached(Size);
579    DCHECK_GT(MaxCount, 0U);
580    // The maximum number of blocks we should carve in the region is dictated
581    // by the maximum number of batches we want to fill, and the amount of
582    // memory left in the current region (we use the lowest of the two). This
583    // will not be 0 as we ensure that a region can at least hold one block (via
584    // static_assert and at the end of this function).
585    const u32 NumberOfBlocks =
586        Min(MaxNumBatches * MaxCount,
587            static_cast<u32>((RegionSize - Offset) / Size));
588    DCHECK_GT(NumberOfBlocks, 0U);
589
590    constexpr u32 ShuffleArraySize =
591        MaxNumBatches * TransferBatch::MaxNumCached;
592    // Fill the transfer batches and put them in the size-class freelist. We
593    // need to randomize the blocks for security purposes, so we first fill a
594    // local array that we then shuffle before populating the batches.
595    CompactPtrT ShuffleArray[ShuffleArraySize];
596    DCHECK_LE(NumberOfBlocks, ShuffleArraySize);
597
598    uptr P = Region + Offset;
599    for (u32 I = 0; I < NumberOfBlocks; I++, P += Size)
600      ShuffleArray[I] = reinterpret_cast<CompactPtrT>(P);
601    // No need to shuffle the batches size class.
602    if (ClassId != SizeClassMap::BatchClassId)
603      shuffle(ShuffleArray, NumberOfBlocks, &Sci->RandState);
604    for (u32 I = 0; I < NumberOfBlocks;) {
605      // `MaxCount` is u16 so the result will also fit in u16.
606      const u16 N = static_cast<u16>(Min<u32>(MaxCount, NumberOfBlocks - I));
607      // Note that the N blocks here may have different group ids. Given that
608      // it only happens when it crosses the group size boundary. Instead of
609      // sorting them, treat them as same group here to avoid sorting the
610      // almost-sorted blocks.
611      pushBlocksImpl(C, ClassId, &ShuffleArray[I], N, /*SameGroup=*/true);
612      I += N;
613    }
614
615    const uptr AllocatedUser = Size * NumberOfBlocks;
616    C->getStats().add(StatFree, AllocatedUser);
617    DCHECK_LE(Sci->CurrentRegionAllocated + AllocatedUser, RegionSize);
618    // If there is not enough room in the region currently associated to fit
619    // more blocks, we deassociate the region by resetting CurrentRegion and
620    // CurrentRegionAllocated. Otherwise, update the allocated amount.
621    if (RegionSize - (Sci->CurrentRegionAllocated + AllocatedUser) < Size) {
622      Sci->CurrentRegion = 0;
623      Sci->CurrentRegionAllocated = 0;
624    } else {
625      Sci->CurrentRegionAllocated += AllocatedUser;
626    }
627    Sci->AllocatedUser += AllocatedUser;
628
629    return true;
630  }
631
632  void getStats(ScopedString *Str, uptr ClassId, uptr Rss) {
633    SizeClassInfo *Sci = getSizeClassInfo(ClassId);
634    if (Sci->AllocatedUser == 0)
635      return;
636    const uptr InUse = Sci->Stats.PoppedBlocks - Sci->Stats.PushedBlocks;
637    const uptr AvailableChunks = Sci->AllocatedUser / getSizeByClassId(ClassId);
638    Str->append("  %02zu (%6zu): mapped: %6zuK popped: %7zu pushed: %7zu "
639                "inuse: %6zu avail: %6zu rss: %6zuK releases: %6zu\n",
640                ClassId, getSizeByClassId(ClassId), Sci->AllocatedUser >> 10,
641                Sci->Stats.PoppedBlocks, Sci->Stats.PushedBlocks, InUse,
642                AvailableChunks, Rss >> 10, Sci->ReleaseInfo.RangesReleased);
643  }
644
645  NOINLINE uptr releaseToOSMaybe(SizeClassInfo *Sci, uptr ClassId,
646                                 bool Force = false) {
647    const uptr BlockSize = getSizeByClassId(ClassId);
648    const uptr PageSize = getPageSizeCached();
649
650    DCHECK_GE(Sci->Stats.PoppedBlocks, Sci->Stats.PushedBlocks);
651    const uptr BytesInFreeList =
652        Sci->AllocatedUser -
653        (Sci->Stats.PoppedBlocks - Sci->Stats.PushedBlocks) * BlockSize;
654    if (BytesInFreeList < PageSize)
655      return 0; // No chance to release anything.
656    const uptr BytesPushed =
657        (Sci->Stats.PushedBlocks - Sci->ReleaseInfo.PushedBlocksAtLastRelease) *
658        BlockSize;
659    if (BytesPushed < PageSize)
660      return 0; // Nothing new to release.
661
662    const bool CheckDensity = BlockSize < PageSize / 16U;
663    // Releasing smaller blocks is expensive, so we want to make sure that a
664    // significant amount of bytes are free, and that there has been a good
665    // amount of batches pushed to the freelist before attempting to release.
666    if (CheckDensity) {
667      if (!Force && BytesPushed < Sci->AllocatedUser / 16U)
668        return 0;
669    }
670
671    if (!Force) {
672      const s32 IntervalMs = atomic_load_relaxed(&ReleaseToOsIntervalMs);
673      if (IntervalMs < 0)
674        return 0;
675      if (Sci->ReleaseInfo.LastReleaseAtNs +
676              static_cast<u64>(IntervalMs) * 1000000 >
677          getMonotonicTime()) {
678        return 0; // Memory was returned recently.
679      }
680    }
681
682    const uptr First = Sci->MinRegionIndex;
683    const uptr Last = Sci->MaxRegionIndex;
684    DCHECK_NE(Last, 0U);
685    DCHECK_LE(First, Last);
686    uptr TotalReleasedBytes = 0;
687    const uptr Base = First * RegionSize;
688    const uptr NumberOfRegions = Last - First + 1U;
689    const uptr GroupSize = (1U << GroupSizeLog);
690    const uptr CurRegionGroupId =
691        compactPtrGroup(compactPtr(ClassId, Sci->CurrentRegion));
692
693    ReleaseRecorder Recorder(Base);
694    PageReleaseContext Context(BlockSize, RegionSize, NumberOfRegions);
695
696    auto DecompactPtr = [](CompactPtrT CompactPtr) {
697      return reinterpret_cast<uptr>(CompactPtr);
698    };
699    for (BatchGroup &BG : Sci->FreeList) {
700      const uptr PushedBytesDelta =
701          BG.PushedBlocks - BG.PushedBlocksAtLastCheckpoint;
702      if (PushedBytesDelta * BlockSize < PageSize)
703        continue;
704
705      uptr AllocatedGroupSize = BG.GroupId == CurRegionGroupId
706                                    ? Sci->CurrentRegionAllocated
707                                    : GroupSize;
708      if (AllocatedGroupSize == 0)
709        continue;
710
711      // TransferBatches are pushed in front of BG.Batches. The first one may
712      // not have all caches used.
713      const uptr NumBlocks = (BG.Batches.size() - 1) * BG.MaxCachedPerBatch +
714                             BG.Batches.front()->getCount();
715      const uptr BytesInBG = NumBlocks * BlockSize;
716      // Given the randomness property, we try to release the pages only if the
717      // bytes used by free blocks exceed certain proportion of allocated
718      // spaces.
719      if (CheckDensity && (BytesInBG * 100U) / AllocatedGroupSize <
720                              (100U - 1U - BlockSize / 16U)) {
721        continue;
722      }
723
724      BG.PushedBlocksAtLastCheckpoint = BG.PushedBlocks;
725      // Note that we don't always visit blocks in each BatchGroup so that we
726      // may miss the chance of releasing certain pages that cross BatchGroups.
727      Context.markFreeBlocks(BG.Batches, DecompactPtr, Base);
728    }
729
730    if (!Context.hasBlockMarked())
731      return 0;
732
733    auto SkipRegion = [this, First, ClassId](uptr RegionIndex) {
734      return (PossibleRegions[First + RegionIndex] - 1U) != ClassId;
735    };
736    releaseFreeMemoryToOS(Context, Recorder, SkipRegion);
737
738    if (Recorder.getReleasedRangesCount() > 0) {
739      Sci->ReleaseInfo.PushedBlocksAtLastRelease = Sci->Stats.PushedBlocks;
740      Sci->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount();
741      Sci->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes();
742      TotalReleasedBytes += Sci->ReleaseInfo.LastReleasedBytes;
743    }
744    Sci->ReleaseInfo.LastReleaseAtNs = getMonotonicTime();
745
746    return TotalReleasedBytes;
747  }
748
749  SizeClassInfo SizeClassInfoArray[NumClasses] = {};
750
751  // Track the regions in use, 0 is unused, otherwise store ClassId + 1.
752  ByteMap PossibleRegions = {};
753  atomic_s32 ReleaseToOsIntervalMs = {};
754  // Unless several threads request regions simultaneously from different size
755  // classes, the stash rarely contains more than 1 entry.
756  static constexpr uptr MaxStashedRegions = 4;
757  HybridMutex RegionsStashMutex;
758  uptr NumberOfStashedRegions = 0;
759  uptr RegionsStash[MaxStashedRegions] = {};
760};
761
762} // namespace scudo
763
764#endif // SCUDO_PRIMARY32_H_
765