1 //===-- quarantine.h --------------------------------------------*- C++ -*-===//
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
7 //===----------------------------------------------------------------------===//
9 #ifndef SCUDO_QUARANTINE_H_
10 #define SCUDO_QUARANTINE_H_
14 #include "string_utils.h"
18 struct QuarantineBatch {
19 // With the following count, a batch (and the header that protects it) occupy
20 // 4096 bytes on 32-bit platforms, and 8192 bytes on 64-bit.
21 static const u32 MaxCount = 1019;
22 QuarantineBatch *Next;
25 void *Batch[MaxCount];
27 void init(void *Ptr, uptr Size) {
30 this->Size = Size + sizeof(QuarantineBatch); // Account for the Batch Size.
33 // The total size of quarantined nodes recorded in this batch.
34 uptr getQuarantinedSize() const { return Size - sizeof(QuarantineBatch); }
36 void push_back(void *Ptr, uptr Size) {
37 DCHECK_LT(Count, MaxCount);
42 bool canMerge(const QuarantineBatch *const From) const {
43 return Count + From->Count <= MaxCount;
46 void merge(QuarantineBatch *const From) {
47 DCHECK_LE(Count + From->Count, MaxCount);
48 DCHECK_GE(Size, sizeof(QuarantineBatch));
50 for (uptr I = 0; I < From->Count; ++I)
51 Batch[Count + I] = From->Batch[I];
53 Size += From->getQuarantinedSize();
56 From->Size = sizeof(QuarantineBatch);
59 void shuffle(u32 State) { ::scudo::shuffle(Batch, Count, &State); }
62 COMPILER_CHECK(sizeof(QuarantineBatch) <= (1U << 13)); // 8Kb.
64 // Per-thread cache of memory blocks.
65 template <typename Callback> class QuarantineCache {
67 void initLinkerInitialized() {}
69 memset(this, 0, sizeof(*this));
70 initLinkerInitialized();
73 // Total memory used, including internal accounting.
74 uptr getSize() const { return atomic_load_relaxed(&Size); }
75 // Memory used for internal accounting.
76 uptr getOverheadSize() const { return List.size() * sizeof(QuarantineBatch); }
78 void enqueue(Callback Cb, void *Ptr, uptr Size) {
79 if (List.empty() || List.back()->Count == QuarantineBatch::MaxCount) {
81 reinterpret_cast<QuarantineBatch *>(Cb.allocate(sizeof(*B)));
86 List.back()->push_back(Ptr, Size);
91 void transfer(QuarantineCache *From) {
92 List.append_back(&From->List);
93 addToSize(From->getSize());
94 atomic_store_relaxed(&From->Size, 0);
97 void enqueueBatch(QuarantineBatch *B) {
102 QuarantineBatch *dequeueBatch() {
105 QuarantineBatch *B = List.front();
107 subFromSize(B->Size);
111 void mergeBatches(QuarantineCache *ToDeallocate) {
112 uptr ExtractedSize = 0;
113 QuarantineBatch *Current = List.front();
114 while (Current && Current->Next) {
115 if (Current->canMerge(Current->Next)) {
116 QuarantineBatch *Extracted = Current->Next;
117 // Move all the chunks into the current batch.
118 Current->merge(Extracted);
119 DCHECK_EQ(Extracted->Count, 0);
120 DCHECK_EQ(Extracted->Size, sizeof(QuarantineBatch));
121 // Remove the next batch From the list and account for its Size.
122 List.extract(Current, Extracted);
123 ExtractedSize += Extracted->Size;
124 // Add it to deallocation list.
125 ToDeallocate->enqueueBatch(Extracted);
127 Current = Current->Next;
130 subFromSize(ExtractedSize);
133 void printStats() const {
135 uptr TotalOverheadBytes = 0;
137 uptr TotalQuarantineChunks = 0;
138 for (const QuarantineBatch &Batch : List) {
140 TotalBytes += Batch.Size;
141 TotalOverheadBytes += Batch.Size - Batch.getQuarantinedSize();
142 TotalQuarantineChunks += Batch.Count;
144 const uptr QuarantineChunksCapacity =
145 BatchCount * QuarantineBatch::MaxCount;
146 const uptr ChunksUsagePercent =
147 (QuarantineChunksCapacity == 0)
149 : TotalQuarantineChunks * 100 / QuarantineChunksCapacity;
150 const uptr TotalQuarantinedBytes = TotalBytes - TotalOverheadBytes;
151 const uptr MemoryOverheadPercent =
152 (TotalQuarantinedBytes == 0)
154 : TotalOverheadBytes * 100 / TotalQuarantinedBytes;
155 Printf("Global quarantine stats: batches: %zd; bytes: %zd (user: %zd); "
156 "chunks: %zd (capacity: %zd); %zd%% chunks used; %zd%% memory "
158 BatchCount, TotalBytes, TotalQuarantinedBytes, TotalQuarantineChunks,
159 QuarantineChunksCapacity, ChunksUsagePercent, MemoryOverheadPercent);
163 IntrusiveList<QuarantineBatch> List;
166 void addToSize(uptr add) { atomic_store_relaxed(&Size, getSize() + add); }
167 void subFromSize(uptr sub) { atomic_store_relaxed(&Size, getSize() - sub); }
170 // The callback interface is:
171 // void Callback::recycle(Node *Ptr);
172 // void *Callback::allocate(uptr Size);
173 // void Callback::deallocate(void *Ptr);
174 template <typename Callback, typename Node> class GlobalQuarantine {
176 typedef QuarantineCache<Callback> CacheT;
178 void initLinkerInitialized(uptr Size, uptr CacheSize) {
179 // Thread local quarantine size can be zero only when global quarantine size
180 // is zero (it allows us to perform just one atomic read per put() call).
181 CHECK((Size == 0 && CacheSize == 0) || CacheSize != 0);
183 atomic_store_relaxed(&MaxSize, Size);
184 atomic_store_relaxed(&MinSize, Size / 10 * 9); // 90% of max size.
185 atomic_store_relaxed(&MaxCacheSize, CacheSize);
187 Cache.initLinkerInitialized();
189 void init(uptr Size, uptr CacheSize) {
190 memset(this, 0, sizeof(*this));
191 initLinkerInitialized(Size, CacheSize);
194 uptr getMaxSize() const { return atomic_load_relaxed(&MaxSize); }
195 uptr getCacheSize() const { return atomic_load_relaxed(&MaxCacheSize); }
197 void put(CacheT *C, Callback Cb, Node *Ptr, uptr Size) {
198 C->enqueue(Cb, Ptr, Size);
199 if (C->getSize() > getCacheSize())
203 void NOINLINE drain(CacheT *C, Callback Cb) {
205 ScopedLock L(CacheMutex);
208 if (Cache.getSize() > getMaxSize() && RecyleMutex.tryLock())
209 recycle(atomic_load_relaxed(&MinSize), Cb);
212 void NOINLINE drainAndRecycle(CacheT *C, Callback Cb) {
214 ScopedLock L(CacheMutex);
221 void printStats() const {
222 // It assumes that the world is stopped, just as the allocator's printStats.
223 Printf("Quarantine limits: global: %zdM; thread local: %zdK\n",
224 getMaxSize() >> 20, getCacheSize() >> 10);
230 alignas(SCUDO_CACHE_LINE_SIZE) HybridMutex CacheMutex;
232 alignas(SCUDO_CACHE_LINE_SIZE) HybridMutex RecyleMutex;
235 alignas(SCUDO_CACHE_LINE_SIZE) atomic_uptr MaxCacheSize;
237 void NOINLINE recycle(uptr MinSize, Callback Cb) {
241 ScopedLock L(CacheMutex);
242 // Go over the batches and merge partially filled ones to
243 // save some memory, otherwise batches themselves (since the memory used
244 // by them is counted against quarantine limit) can overcome the actual
245 // user's quarantined chunks, which diminishes the purpose of the
247 const uptr CacheSize = Cache.getSize();
248 const uptr OverheadSize = Cache.getOverheadSize();
249 DCHECK_GE(CacheSize, OverheadSize);
250 // Do the merge only when overhead exceeds this predefined limit (might
251 // require some tuning). It saves us merge attempt when the batch list
252 // quarantine is unlikely to contain batches suitable for merge.
253 constexpr uptr OverheadThresholdPercents = 100;
254 if (CacheSize > OverheadSize &&
255 OverheadSize * (100 + OverheadThresholdPercents) >
256 CacheSize * OverheadThresholdPercents) {
257 Cache.mergeBatches(&Tmp);
259 // Extract enough chunks from the quarantine to get below the max
260 // quarantine size and leave some leeway for the newly quarantined chunks.
261 while (Cache.getSize() > MinSize)
262 Tmp.enqueueBatch(Cache.dequeueBatch());
264 RecyleMutex.unlock();
268 void NOINLINE doRecycle(CacheT *C, Callback Cb) {
269 while (QuarantineBatch *B = C->dequeueBatch()) {
270 const u32 Seed = static_cast<u32>(
271 (reinterpret_cast<uptr>(B) ^ reinterpret_cast<uptr>(C)) >> 4);
273 constexpr uptr NumberOfPrefetch = 8UL;
274 CHECK(NumberOfPrefetch <= ARRAY_SIZE(B->Batch));
275 for (uptr I = 0; I < NumberOfPrefetch; I++)
276 PREFETCH(B->Batch[I]);
277 for (uptr I = 0, Count = B->Count; I < Count; I++) {
278 if (I + NumberOfPrefetch < Count)
279 PREFETCH(B->Batch[I + NumberOfPrefetch]);
280 Cb.recycle(reinterpret_cast<Node *>(B->Batch[I]));
289 #endif // SCUDO_QUARANTINE_H_