1 //===-- release.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_RELEASE_H_
10 #define SCUDO_RELEASE_H_
11
12 #include "common.h"
13 #include "list.h"
14 #include "mutex.h"
15
16 namespace scudo {
17
18 class ReleaseRecorder {
19 public:
20 ReleaseRecorder(uptr Base, MapPlatformData *Data = nullptr)
Base(Base)21 : Base(Base), Data(Data) {}
22
getReleasedRangesCount()23 uptr getReleasedRangesCount() const { return ReleasedRangesCount; }
24
getReleasedBytes()25 uptr getReleasedBytes() const { return ReleasedBytes; }
26
getBase()27 uptr getBase() const { return Base; }
28
29 // Releases [From, To) range of pages back to OS.
releasePageRangeToOS(uptr From,uptr To)30 void releasePageRangeToOS(uptr From, uptr To) {
31 const uptr Size = To - From;
32 releasePagesToOS(Base, From, Size, Data);
33 ReleasedRangesCount++;
34 ReleasedBytes += Size;
35 }
36
37 private:
38 uptr ReleasedRangesCount = 0;
39 uptr ReleasedBytes = 0;
40 uptr Base = 0;
41 MapPlatformData *Data = nullptr;
42 };
43
44 // A Region page map is used to record the usage of pages in the regions. It
45 // implements a packed array of Counters. Each counter occupies 2^N bits, enough
46 // to store counter's MaxValue. Ctor will try to use a static buffer first, and
47 // if that fails (the buffer is too small or already locked), will allocate the
48 // required Buffer via map(). The caller is expected to check whether the
49 // initialization was successful by checking isAllocated() result. For
50 // performance sake, none of the accessors check the validity of the arguments,
51 // It is assumed that Index is always in [0, N) range and the value is not
52 // incremented past MaxValue.
53 class RegionPageMap {
54 public:
RegionPageMap()55 RegionPageMap()
56 : Regions(0),
57 NumCounters(0),
58 CounterSizeBitsLog(0),
59 CounterMask(0),
60 PackingRatioLog(0),
61 BitOffsetMask(0),
62 SizePerRegion(0),
63 BufferSize(0),
64 Buffer(nullptr) {}
RegionPageMap(uptr NumberOfRegions,uptr CountersPerRegion,uptr MaxValue)65 RegionPageMap(uptr NumberOfRegions, uptr CountersPerRegion, uptr MaxValue) {
66 reset(NumberOfRegions, CountersPerRegion, MaxValue);
67 }
~RegionPageMap()68 ~RegionPageMap() {
69 if (!isAllocated())
70 return;
71 if (Buffer == &StaticBuffer[0])
72 Mutex.unlock();
73 else
74 unmap(reinterpret_cast<void *>(Buffer),
75 roundUpTo(BufferSize, getPageSizeCached()));
76 Buffer = nullptr;
77 }
78
reset(uptr NumberOfRegion,uptr CountersPerRegion,uptr MaxValue)79 void reset(uptr NumberOfRegion, uptr CountersPerRegion, uptr MaxValue) {
80 DCHECK_GT(NumberOfRegion, 0);
81 DCHECK_GT(CountersPerRegion, 0);
82 DCHECK_GT(MaxValue, 0);
83
84 Regions = NumberOfRegion;
85 NumCounters = CountersPerRegion;
86
87 constexpr uptr MaxCounterBits = sizeof(*Buffer) * 8UL;
88 // Rounding counter storage size up to the power of two allows for using
89 // bit shifts calculating particular counter's Index and offset.
90 const uptr CounterSizeBits =
91 roundUpToPowerOfTwo(getMostSignificantSetBitIndex(MaxValue) + 1);
92 DCHECK_LE(CounterSizeBits, MaxCounterBits);
93 CounterSizeBitsLog = getLog2(CounterSizeBits);
94 CounterMask = ~(static_cast<uptr>(0)) >> (MaxCounterBits - CounterSizeBits);
95
96 const uptr PackingRatio = MaxCounterBits >> CounterSizeBitsLog;
97 DCHECK_GT(PackingRatio, 0);
98 PackingRatioLog = getLog2(PackingRatio);
99 BitOffsetMask = PackingRatio - 1;
100
101 SizePerRegion =
102 roundUpTo(NumCounters, static_cast<uptr>(1U) << PackingRatioLog) >>
103 PackingRatioLog;
104 BufferSize = SizePerRegion * sizeof(*Buffer) * Regions;
105 if (BufferSize <= (StaticBufferCount * sizeof(Buffer[0])) &&
106 Mutex.tryLock()) {
107 Buffer = &StaticBuffer[0];
108 memset(Buffer, 0, BufferSize);
109 } else {
110 // When using a heap-based buffer, precommit the pages backing the
111 // Vmar by passing |MAP_PRECOMMIT| flag. This allows an optimization
112 // where page fault exceptions are skipped as the allocated memory
113 // is accessed.
114 const uptr MmapFlags =
115 MAP_ALLOWNOMEM | (SCUDO_FUCHSIA ? MAP_PRECOMMIT : 0);
116 Buffer = reinterpret_cast<uptr *>(
117 map(nullptr, roundUpTo(BufferSize, getPageSizeCached()),
118 "scudo:counters", MmapFlags, &MapData));
119 }
120 }
121
isAllocated()122 bool isAllocated() const { return !!Buffer; }
123
getCount()124 uptr getCount() const { return NumCounters; }
125
get(uptr Region,uptr I)126 uptr get(uptr Region, uptr I) const {
127 DCHECK_LT(Region, Regions);
128 DCHECK_LT(I, NumCounters);
129 const uptr Index = I >> PackingRatioLog;
130 const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog;
131 return (Buffer[Region * SizePerRegion + Index] >> BitOffset) & CounterMask;
132 }
133
inc(uptr Region,uptr I)134 void inc(uptr Region, uptr I) const {
135 DCHECK_LT(get(Region, I), CounterMask);
136 const uptr Index = I >> PackingRatioLog;
137 const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog;
138 DCHECK_LT(BitOffset, SCUDO_WORDSIZE);
139 DCHECK_EQ(isAllCounted(Region, I), false);
140 Buffer[Region * SizePerRegion + Index] += static_cast<uptr>(1U)
141 << BitOffset;
142 }
143
incRange(uptr Region,uptr From,uptr To)144 void incRange(uptr Region, uptr From, uptr To) const {
145 DCHECK_LE(From, To);
146 const uptr Top = Min(To + 1, NumCounters);
147 for (uptr I = From; I < Top; I++)
148 inc(Region, I);
149 }
150
151 // Set the counter to the max value. Note that the max number of blocks in a
152 // page may vary. To provide an easier way to tell if all the blocks are
153 // counted for different pages, set to the same max value to denote the
154 // all-counted status.
setAsAllCounted(uptr Region,uptr I)155 void setAsAllCounted(uptr Region, uptr I) const {
156 DCHECK_LE(get(Region, I), CounterMask);
157 const uptr Index = I >> PackingRatioLog;
158 const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog;
159 DCHECK_LT(BitOffset, SCUDO_WORDSIZE);
160 Buffer[Region * SizePerRegion + Index] |= CounterMask << BitOffset;
161 }
isAllCounted(uptr Region,uptr I)162 bool isAllCounted(uptr Region, uptr I) const {
163 return get(Region, I) == CounterMask;
164 }
165
getBufferSize()166 uptr getBufferSize() const { return BufferSize; }
167
168 static const uptr StaticBufferCount = 2048U;
169
170 private:
171 uptr Regions;
172 uptr NumCounters;
173 uptr CounterSizeBitsLog;
174 uptr CounterMask;
175 uptr PackingRatioLog;
176 uptr BitOffsetMask;
177
178 uptr SizePerRegion;
179 uptr BufferSize;
180 uptr *Buffer;
181 [[no_unique_address]] MapPlatformData MapData = {};
182
183 static HybridMutex Mutex;
184 static uptr StaticBuffer[StaticBufferCount];
185 };
186
187 template <class ReleaseRecorderT> class FreePagesRangeTracker {
188 public:
FreePagesRangeTracker(ReleaseRecorderT & Recorder)189 explicit FreePagesRangeTracker(ReleaseRecorderT &Recorder)
190 : Recorder(Recorder), PageSizeLog(getLog2(getPageSizeCached())) {}
191
processNextPage(bool Released)192 void processNextPage(bool Released) {
193 if (Released) {
194 if (!InRange) {
195 CurrentRangeStatePage = CurrentPage;
196 InRange = true;
197 }
198 } else {
199 closeOpenedRange();
200 }
201 CurrentPage++;
202 }
203
skipPages(uptr N)204 void skipPages(uptr N) {
205 closeOpenedRange();
206 CurrentPage += N;
207 }
208
finish()209 void finish() { closeOpenedRange(); }
210
211 private:
closeOpenedRange()212 void closeOpenedRange() {
213 if (InRange) {
214 Recorder.releasePageRangeToOS((CurrentRangeStatePage << PageSizeLog),
215 (CurrentPage << PageSizeLog));
216 InRange = false;
217 }
218 }
219
220 ReleaseRecorderT &Recorder;
221 const uptr PageSizeLog;
222 bool InRange = false;
223 uptr CurrentPage = 0;
224 uptr CurrentRangeStatePage = 0;
225 };
226
227 struct PageReleaseContext {
PageReleaseContextPageReleaseContext228 PageReleaseContext(uptr BlockSize, uptr RegionSize, uptr NumberOfRegions) :
229 BlockSize(BlockSize),
230 RegionSize(RegionSize),
231 NumberOfRegions(NumberOfRegions) {
232 PageSize = getPageSizeCached();
233 if (BlockSize <= PageSize) {
234 if (PageSize % BlockSize == 0) {
235 // Same number of chunks per page, no cross overs.
236 FullPagesBlockCountMax = PageSize / BlockSize;
237 SameBlockCountPerPage = true;
238 } else if (BlockSize % (PageSize % BlockSize) == 0) {
239 // Some chunks are crossing page boundaries, which means that the page
240 // contains one or two partial chunks, but all pages contain the same
241 // number of chunks.
242 FullPagesBlockCountMax = PageSize / BlockSize + 1;
243 SameBlockCountPerPage = true;
244 } else {
245 // Some chunks are crossing page boundaries, which means that the page
246 // contains one or two partial chunks.
247 FullPagesBlockCountMax = PageSize / BlockSize + 2;
248 SameBlockCountPerPage = false;
249 }
250 } else {
251 if (BlockSize % PageSize == 0) {
252 // One chunk covers multiple pages, no cross overs.
253 FullPagesBlockCountMax = 1;
254 SameBlockCountPerPage = true;
255 } else {
256 // One chunk covers multiple pages, Some chunks are crossing page
257 // boundaries. Some pages contain one chunk, some contain two.
258 FullPagesBlockCountMax = 2;
259 SameBlockCountPerPage = false;
260 }
261 }
262
263 PagesCount = roundUpTo(RegionSize, PageSize) / PageSize;
264 PageSizeLog = getLog2(PageSize);
265 RoundedRegionSize = PagesCount << PageSizeLog;
266 RoundedSize = NumberOfRegions * RoundedRegionSize;
267 }
268
269 // PageMap is lazily allocated when markFreeBlocks() is invoked.
hasBlockMarkedPageReleaseContext270 bool hasBlockMarked() const {
271 return PageMap.isAllocated();
272 }
273
ensurePageMapAllocatedPageReleaseContext274 void ensurePageMapAllocated() {
275 if (PageMap.isAllocated())
276 return;
277 PageMap.reset(NumberOfRegions, PagesCount, FullPagesBlockCountMax);
278 DCHECK(PageMap.isAllocated());
279 }
280
281 template<class TransferBatchT, typename DecompactPtrT>
markFreeBlocksPageReleaseContext282 void markFreeBlocks(const IntrusiveList<TransferBatchT> &FreeList,
283 DecompactPtrT DecompactPtr, uptr Base) {
284 ensurePageMapAllocated();
285
286 // Iterate over free chunks and count how many free chunks affect each
287 // allocated page.
288 if (BlockSize <= PageSize && PageSize % BlockSize == 0) {
289 // Each chunk affects one page only.
290 for (const auto &It : FreeList) {
291 for (u16 I = 0; I < It.getCount(); I++) {
292 const uptr P = DecompactPtr(It.get(I)) - Base;
293 if (P >= RoundedSize)
294 continue;
295 const uptr RegionIndex = NumberOfRegions == 1U ? 0 : P / RegionSize;
296 const uptr PInRegion = P - RegionIndex * RegionSize;
297 PageMap.inc(RegionIndex, PInRegion >> PageSizeLog);
298 }
299 }
300 } else {
301 // In all other cases chunks might affect more than one page.
302 DCHECK_GE(RegionSize, BlockSize);
303 const uptr LastBlockInRegion =
304 ((RegionSize / BlockSize) - 1U) * BlockSize;
305 for (const auto &It : FreeList) {
306 for (u16 I = 0; I < It.getCount(); I++) {
307 const uptr P = DecompactPtr(It.get(I)) - Base;
308 if (P >= RoundedSize)
309 continue;
310 const uptr RegionIndex = NumberOfRegions == 1U ? 0 : P / RegionSize;
311 uptr PInRegion = P - RegionIndex * RegionSize;
312 PageMap.incRange(RegionIndex, PInRegion >> PageSizeLog,
313 (PInRegion + BlockSize - 1) >> PageSizeLog);
314 // The last block in a region might straddle a page, so if it's
315 // free, we mark the following "pretend" memory block(s) as free.
316 if (PInRegion == LastBlockInRegion) {
317 PInRegion += BlockSize;
318 while (PInRegion < RoundedRegionSize) {
319 PageMap.incRange(RegionIndex, PInRegion >> PageSizeLog,
320 (PInRegion + BlockSize - 1) >> PageSizeLog);
321 PInRegion += BlockSize;
322 }
323 }
324 }
325 }
326 }
327 }
328
329 uptr BlockSize;
330 uptr RegionSize;
331 uptr NumberOfRegions;
332 uptr PageSize;
333 uptr PagesCount;
334 uptr PageSizeLog;
335 uptr RoundedRegionSize;
336 uptr RoundedSize;
337 uptr FullPagesBlockCountMax;
338 bool SameBlockCountPerPage;
339 RegionPageMap PageMap;
340 };
341
342 // Try to release the page which doesn't have any in-used block, i.e., they are
343 // all free blocks. The `PageMap` will record the number of free blocks in each
344 // page.
345 template <class ReleaseRecorderT, typename SkipRegionT>
346 NOINLINE void
releaseFreeMemoryToOS(PageReleaseContext & Context,ReleaseRecorderT & Recorder,SkipRegionT SkipRegion)347 releaseFreeMemoryToOS(PageReleaseContext &Context,
348 ReleaseRecorderT &Recorder, SkipRegionT SkipRegion) {
349 const uptr PageSize = Context.PageSize;
350 const uptr BlockSize = Context.BlockSize;
351 const uptr PagesCount = Context.PagesCount;
352 const uptr NumberOfRegions = Context.NumberOfRegions;
353 const uptr FullPagesBlockCountMax = Context.FullPagesBlockCountMax;
354 const bool SameBlockCountPerPage = Context.SameBlockCountPerPage;
355 RegionPageMap &PageMap = Context.PageMap;
356
357 // Iterate over pages detecting ranges of pages with chunk Counters equal
358 // to the expected number of chunks for the particular page.
359 FreePagesRangeTracker<ReleaseRecorderT> RangeTracker(Recorder);
360 if (SameBlockCountPerPage) {
361 // Fast path, every page has the same number of chunks affecting it.
362 for (uptr I = 0; I < NumberOfRegions; I++) {
363 if (SkipRegion(I)) {
364 RangeTracker.skipPages(PagesCount);
365 continue;
366 }
367 for (uptr J = 0; J < PagesCount; J++) {
368 const bool CanRelease = PageMap.get(I, J) == FullPagesBlockCountMax;
369 if (CanRelease)
370 PageMap.setAsAllCounted(I, J);
371 RangeTracker.processNextPage(CanRelease);
372 }
373 }
374 } else {
375 // Slow path, go through the pages keeping count how many chunks affect
376 // each page.
377 const uptr Pn = BlockSize < PageSize ? PageSize / BlockSize : 1;
378 const uptr Pnc = Pn * BlockSize;
379 // The idea is to increment the current page pointer by the first chunk
380 // size, middle portion size (the portion of the page covered by chunks
381 // except the first and the last one) and then the last chunk size, adding
382 // up the number of chunks on the current page and checking on every step
383 // whether the page boundary was crossed.
384 for (uptr I = 0; I < NumberOfRegions; I++) {
385 if (SkipRegion(I)) {
386 RangeTracker.skipPages(PagesCount);
387 continue;
388 }
389 uptr PrevPageBoundary = 0;
390 uptr CurrentBoundary = 0;
391 for (uptr J = 0; J < PagesCount; J++) {
392 const uptr PageBoundary = PrevPageBoundary + PageSize;
393 uptr BlocksPerPage = Pn;
394 if (CurrentBoundary < PageBoundary) {
395 if (CurrentBoundary > PrevPageBoundary)
396 BlocksPerPage++;
397 CurrentBoundary += Pnc;
398 if (CurrentBoundary < PageBoundary) {
399 BlocksPerPage++;
400 CurrentBoundary += BlockSize;
401 }
402 }
403 PrevPageBoundary = PageBoundary;
404 const bool CanRelease = PageMap.get(I, J) == BlocksPerPage;
405 if (CanRelease)
406 PageMap.setAsAllCounted(I, J);
407 RangeTracker.processNextPage(CanRelease);
408 }
409 }
410 }
411 RangeTracker.finish();
412 }
413
414 // An overload releaseFreeMemoryToOS which doesn't require the page usage
415 // information after releasing.
416 template <class TransferBatchT, class ReleaseRecorderT, typename DecompactPtrT,
417 typename SkipRegionT>
418 NOINLINE void
releaseFreeMemoryToOS(const IntrusiveList<TransferBatchT> & FreeList,uptr RegionSize,uptr NumberOfRegions,uptr BlockSize,ReleaseRecorderT & Recorder,DecompactPtrT DecompactPtr,SkipRegionT SkipRegion)419 releaseFreeMemoryToOS(const IntrusiveList<TransferBatchT> &FreeList,
420 uptr RegionSize, uptr NumberOfRegions, uptr BlockSize,
421 ReleaseRecorderT &Recorder, DecompactPtrT DecompactPtr,
422 SkipRegionT SkipRegion) {
423 PageReleaseContext Context(BlockSize, RegionSize, NumberOfRegions);
424 Context.markFreeBlocks(FreeList, DecompactPtr, Recorder.getBase());
425 releaseFreeMemoryToOS(Context, Recorder, SkipRegion);
426 }
427
428 } // namespace scudo
429
430 #endif // SCUDO_RELEASE_H_
431