xref: /freebsd/contrib/llvm-project/compiler-rt/lib/scudo/standalone/quarantine.h (revision a03411e84728e9b267056fd31c7d1d9d1dc1b01e)
1 //===-- quarantine.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_QUARANTINE_H_
10 #define SCUDO_QUARANTINE_H_
11 
12 #include "list.h"
13 #include "mutex.h"
14 #include "string_utils.h"
15 #include "thread_annotations.h"
16 
17 namespace scudo {
18 
19 struct QuarantineBatch {
20   // With the following count, a batch (and the header that protects it) occupy
21   // 4096 bytes on 32-bit platforms, and 8192 bytes on 64-bit.
22   static const u32 MaxCount = 1019;
23   QuarantineBatch *Next;
24   uptr Size;
25   u32 Count;
26   void *Batch[MaxCount];
27 
28   void init(void *Ptr, uptr Size) {
29     Count = 1;
30     Batch[0] = Ptr;
31     this->Size = Size + sizeof(QuarantineBatch); // Account for the Batch Size.
32   }
33 
34   // The total size of quarantined nodes recorded in this batch.
35   uptr getQuarantinedSize() const { return Size - sizeof(QuarantineBatch); }
36 
37   void push_back(void *Ptr, uptr Size) {
38     DCHECK_LT(Count, MaxCount);
39     Batch[Count++] = Ptr;
40     this->Size += Size;
41   }
42 
43   bool canMerge(const QuarantineBatch *const From) const {
44     return Count + From->Count <= MaxCount;
45   }
46 
47   void merge(QuarantineBatch *const From) {
48     DCHECK_LE(Count + From->Count, MaxCount);
49     DCHECK_GE(Size, sizeof(QuarantineBatch));
50 
51     for (uptr I = 0; I < From->Count; ++I)
52       Batch[Count + I] = From->Batch[I];
53     Count += From->Count;
54     Size += From->getQuarantinedSize();
55 
56     From->Count = 0;
57     From->Size = sizeof(QuarantineBatch);
58   }
59 
60   void shuffle(u32 State) { ::scudo::shuffle(Batch, Count, &State); }
61 };
62 
63 static_assert(sizeof(QuarantineBatch) <= (1U << 13), ""); // 8Kb.
64 
65 // Per-thread cache of memory blocks.
66 template <typename Callback> class QuarantineCache {
67 public:
68   void init() { DCHECK_EQ(atomic_load_relaxed(&Size), 0U); }
69 
70   // Total memory used, including internal accounting.
71   uptr getSize() const { return atomic_load_relaxed(&Size); }
72   // Memory used for internal accounting.
73   uptr getOverheadSize() const { return List.size() * sizeof(QuarantineBatch); }
74 
75   void enqueue(Callback Cb, void *Ptr, uptr Size) {
76     if (List.empty() || List.back()->Count == QuarantineBatch::MaxCount) {
77       QuarantineBatch *B =
78           reinterpret_cast<QuarantineBatch *>(Cb.allocate(sizeof(*B)));
79       DCHECK(B);
80       B->init(Ptr, Size);
81       enqueueBatch(B);
82     } else {
83       List.back()->push_back(Ptr, Size);
84       addToSize(Size);
85     }
86   }
87 
88   void transfer(QuarantineCache *From) {
89     List.append_back(&From->List);
90     addToSize(From->getSize());
91     atomic_store_relaxed(&From->Size, 0);
92   }
93 
94   void enqueueBatch(QuarantineBatch *B) {
95     List.push_back(B);
96     addToSize(B->Size);
97   }
98 
99   QuarantineBatch *dequeueBatch() {
100     if (List.empty())
101       return nullptr;
102     QuarantineBatch *B = List.front();
103     List.pop_front();
104     subFromSize(B->Size);
105     return B;
106   }
107 
108   void mergeBatches(QuarantineCache *ToDeallocate) {
109     uptr ExtractedSize = 0;
110     QuarantineBatch *Current = List.front();
111     while (Current && Current->Next) {
112       if (Current->canMerge(Current->Next)) {
113         QuarantineBatch *Extracted = Current->Next;
114         // Move all the chunks into the current batch.
115         Current->merge(Extracted);
116         DCHECK_EQ(Extracted->Count, 0);
117         DCHECK_EQ(Extracted->Size, sizeof(QuarantineBatch));
118         // Remove the next batch From the list and account for its Size.
119         List.extract(Current, Extracted);
120         ExtractedSize += Extracted->Size;
121         // Add it to deallocation list.
122         ToDeallocate->enqueueBatch(Extracted);
123       } else {
124         Current = Current->Next;
125       }
126     }
127     subFromSize(ExtractedSize);
128   }
129 
130   void getStats(ScopedString *Str) const {
131     uptr BatchCount = 0;
132     uptr TotalOverheadBytes = 0;
133     uptr TotalBytes = 0;
134     uptr TotalQuarantineChunks = 0;
135     for (const QuarantineBatch &Batch : List) {
136       BatchCount++;
137       TotalBytes += Batch.Size;
138       TotalOverheadBytes += Batch.Size - Batch.getQuarantinedSize();
139       TotalQuarantineChunks += Batch.Count;
140     }
141     const uptr QuarantineChunksCapacity =
142         BatchCount * QuarantineBatch::MaxCount;
143     const uptr ChunksUsagePercent =
144         (QuarantineChunksCapacity == 0)
145             ? 0
146             : TotalQuarantineChunks * 100 / QuarantineChunksCapacity;
147     const uptr TotalQuarantinedBytes = TotalBytes - TotalOverheadBytes;
148     const uptr MemoryOverheadPercent =
149         (TotalQuarantinedBytes == 0)
150             ? 0
151             : TotalOverheadBytes * 100 / TotalQuarantinedBytes;
152     Str->append(
153         "Stats: Quarantine: batches: %zu; bytes: %zu (user: %zu); chunks: %zu "
154         "(capacity: %zu); %zu%% chunks used; %zu%% memory overhead\n",
155         BatchCount, TotalBytes, TotalQuarantinedBytes, TotalQuarantineChunks,
156         QuarantineChunksCapacity, ChunksUsagePercent, MemoryOverheadPercent);
157   }
158 
159 private:
160   SinglyLinkedList<QuarantineBatch> List;
161   atomic_uptr Size = {};
162 
163   void addToSize(uptr add) { atomic_store_relaxed(&Size, getSize() + add); }
164   void subFromSize(uptr sub) { atomic_store_relaxed(&Size, getSize() - sub); }
165 };
166 
167 // The callback interface is:
168 // void Callback::recycle(Node *Ptr);
169 // void *Callback::allocate(uptr Size);
170 // void Callback::deallocate(void *Ptr);
171 template <typename Callback, typename Node> class GlobalQuarantine {
172 public:
173   typedef QuarantineCache<Callback> CacheT;
174   using ThisT = GlobalQuarantine<Callback, Node>;
175 
176   void init(uptr Size, uptr CacheSize) NO_THREAD_SAFETY_ANALYSIS {
177     DCHECK(isAligned(reinterpret_cast<uptr>(this), alignof(ThisT)));
178     DCHECK_EQ(atomic_load_relaxed(&MaxSize), 0U);
179     DCHECK_EQ(atomic_load_relaxed(&MinSize), 0U);
180     DCHECK_EQ(atomic_load_relaxed(&MaxCacheSize), 0U);
181     // Thread local quarantine size can be zero only when global quarantine size
182     // is zero (it allows us to perform just one atomic read per put() call).
183     CHECK((Size == 0 && CacheSize == 0) || CacheSize != 0);
184 
185     atomic_store_relaxed(&MaxSize, Size);
186     atomic_store_relaxed(&MinSize, Size / 10 * 9); // 90% of max size.
187     atomic_store_relaxed(&MaxCacheSize, CacheSize);
188 
189     Cache.init();
190   }
191 
192   uptr getMaxSize() const { return atomic_load_relaxed(&MaxSize); }
193   uptr getCacheSize() const { return atomic_load_relaxed(&MaxCacheSize); }
194 
195   // This is supposed to be used in test only.
196   bool isEmpty() {
197     ScopedLock L(CacheMutex);
198     return Cache.getSize() == 0U;
199   }
200 
201   void put(CacheT *C, Callback Cb, Node *Ptr, uptr Size) {
202     C->enqueue(Cb, Ptr, Size);
203     if (C->getSize() > getCacheSize())
204       drain(C, Cb);
205   }
206 
207   void NOINLINE drain(CacheT *C, Callback Cb) EXCLUDES(CacheMutex) {
208     bool needRecycle = false;
209     {
210       ScopedLock L(CacheMutex);
211       Cache.transfer(C);
212       needRecycle = Cache.getSize() > getMaxSize();
213     }
214 
215     if (needRecycle && RecycleMutex.tryLock())
216       recycle(atomic_load_relaxed(&MinSize), Cb);
217   }
218 
219   void NOINLINE drainAndRecycle(CacheT *C, Callback Cb) EXCLUDES(CacheMutex) {
220     {
221       ScopedLock L(CacheMutex);
222       Cache.transfer(C);
223     }
224     RecycleMutex.lock();
225     recycle(0, Cb);
226   }
227 
228   void getStats(ScopedString *Str) EXCLUDES(CacheMutex) {
229     ScopedLock L(CacheMutex);
230     // It assumes that the world is stopped, just as the allocator's printStats.
231     Cache.getStats(Str);
232     Str->append("Quarantine limits: global: %zuK; thread local: %zuK\n",
233                 getMaxSize() >> 10, getCacheSize() >> 10);
234   }
235 
236   void disable() NO_THREAD_SAFETY_ANALYSIS {
237     // RecycleMutex must be locked 1st since we grab CacheMutex within recycle.
238     RecycleMutex.lock();
239     CacheMutex.lock();
240   }
241 
242   void enable() NO_THREAD_SAFETY_ANALYSIS {
243     CacheMutex.unlock();
244     RecycleMutex.unlock();
245   }
246 
247 private:
248   // Read-only data.
249   alignas(SCUDO_CACHE_LINE_SIZE) HybridMutex CacheMutex;
250   CacheT Cache GUARDED_BY(CacheMutex);
251   alignas(SCUDO_CACHE_LINE_SIZE) HybridMutex RecycleMutex;
252   atomic_uptr MinSize = {};
253   atomic_uptr MaxSize = {};
254   alignas(SCUDO_CACHE_LINE_SIZE) atomic_uptr MaxCacheSize = {};
255 
256   void NOINLINE recycle(uptr MinSize, Callback Cb) RELEASE(RecycleMutex)
257       EXCLUDES(CacheMutex) {
258     CacheT Tmp;
259     Tmp.init();
260     {
261       ScopedLock L(CacheMutex);
262       // Go over the batches and merge partially filled ones to
263       // save some memory, otherwise batches themselves (since the memory used
264       // by them is counted against quarantine limit) can overcome the actual
265       // user's quarantined chunks, which diminishes the purpose of the
266       // quarantine.
267       const uptr CacheSize = Cache.getSize();
268       const uptr OverheadSize = Cache.getOverheadSize();
269       DCHECK_GE(CacheSize, OverheadSize);
270       // Do the merge only when overhead exceeds this predefined limit (might
271       // require some tuning). It saves us merge attempt when the batch list
272       // quarantine is unlikely to contain batches suitable for merge.
273       constexpr uptr OverheadThresholdPercents = 100;
274       if (CacheSize > OverheadSize &&
275           OverheadSize * (100 + OverheadThresholdPercents) >
276               CacheSize * OverheadThresholdPercents) {
277         Cache.mergeBatches(&Tmp);
278       }
279       // Extract enough chunks from the quarantine to get below the max
280       // quarantine size and leave some leeway for the newly quarantined chunks.
281       while (Cache.getSize() > MinSize)
282         Tmp.enqueueBatch(Cache.dequeueBatch());
283     }
284     RecycleMutex.unlock();
285     doRecycle(&Tmp, Cb);
286   }
287 
288   void NOINLINE doRecycle(CacheT *C, Callback Cb) {
289     while (QuarantineBatch *B = C->dequeueBatch()) {
290       const u32 Seed = static_cast<u32>(
291           (reinterpret_cast<uptr>(B) ^ reinterpret_cast<uptr>(C)) >> 4);
292       B->shuffle(Seed);
293       constexpr uptr NumberOfPrefetch = 8UL;
294       CHECK(NumberOfPrefetch <= ARRAY_SIZE(B->Batch));
295       for (uptr I = 0; I < NumberOfPrefetch; I++)
296         PREFETCH(B->Batch[I]);
297       for (uptr I = 0, Count = B->Count; I < Count; I++) {
298         if (I + NumberOfPrefetch < Count)
299           PREFETCH(B->Batch[I + NumberOfPrefetch]);
300         Cb.recycle(reinterpret_cast<Node *>(B->Batch[I]));
301       }
302       Cb.deallocate(B);
303     }
304   }
305 };
306 
307 } // namespace scudo
308 
309 #endif // SCUDO_QUARANTINE_H_
310