| 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(A: Batch, N: Count, RandState: &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(A: &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(add: Size); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | void transfer(QuarantineCache *From) { |
| 89 | List.append_back(L: &From->List); |
| 90 | addToSize(add: From->getSize()); |
| 91 | atomic_store_relaxed(&From->Size, 0); |
| 92 | } |
| 93 | |
| 94 | void enqueueBatch(QuarantineBatch *B) { |
| 95 | List.push_back(X: B); |
| 96 | addToSize(add: 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(sub: B->Size); |
| 105 | return B; |
| 106 | } |
| 107 | |
| 108 | void mergeBatches(QuarantineCache *ToDeallocate) { |
| 109 | uptr = 0; |
| 110 | QuarantineBatch *Current = List.front(); |
| 111 | while (Current && Current->Next) { |
| 112 | if (Current->canMerge(From: Current->Next)) { |
| 113 | QuarantineBatch * = Current->Next; |
| 114 | // Move all the chunks into the current batch. |
| 115 | Current->merge(From: 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(Prev: Current, X: 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(sub: 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 | Format: "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(A: &MaxSize, V: Size); |
| 186 | atomic_store_relaxed(A: &MinSize, V: Size / 10 * 9); // 90% of max size. |
| 187 | atomic_store_relaxed(A: &MaxCacheSize, V: CacheSize); |
| 188 | |
| 189 | Cache.init(); |
| 190 | } |
| 191 | |
| 192 | uptr getMaxSize() const { return atomic_load_relaxed(A: &MaxSize); } |
| 193 | uptr getCacheSize() const { return atomic_load_relaxed(A: &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(MinSize: atomic_load_relaxed(A: &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(MinSize: 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(Format: "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(C: &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(State: 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 | |