| 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 "mem_map.h" |
| 15 | #include "mutex.h" |
| 16 | #include "thread_annotations.h" |
| 17 | |
| 18 | namespace scudo { |
| 19 | |
| 20 | template <typename MemMapT> class RegionReleaseRecorder { |
| 21 | public: |
| 22 | RegionReleaseRecorder(MemMapT *RegionMemMap, uptr Base, uptr Offset = 0) |
| 23 | : RegionMemMap(RegionMemMap), Base(Base), Offset(Offset) {} |
| 24 | |
| 25 | uptr getReleasedBytes() const { return ReleasedBytes; } |
| 26 | |
| 27 | uptr getBase() const { return Base; } |
| 28 | |
| 29 | // Releases [From, To) range of pages back to OS. Note that `From` and `To` |
| 30 | // are offseted from `Base` + Offset. |
| 31 | void (uptr From, uptr To) { |
| 32 | const uptr Size = To - From; |
| 33 | RegionMemMap->releasePagesToOS(getBase() + Offset + From, Size); |
| 34 | ReleasedBytes += Size; |
| 35 | } |
| 36 | |
| 37 | private: |
| 38 | uptr ReleasedBytes = 0; |
| 39 | MemMapT *RegionMemMap = nullptr; |
| 40 | uptr Base = 0; |
| 41 | // The release offset from Base. This is used when we know a given range after |
| 42 | // Base will not be released. |
| 43 | uptr Offset = 0; |
| 44 | }; |
| 45 | |
| 46 | class ReleaseRecorder { |
| 47 | public: |
| 48 | ReleaseRecorder(uptr Base, uptr Offset = 0, MapPlatformData *Data = nullptr) |
| 49 | : Base(Base), Offset(Offset), Data(Data) {} |
| 50 | |
| 51 | uptr getReleasedBytes() const { return ReleasedBytes; } |
| 52 | |
| 53 | uptr getBase() const { return Base; } |
| 54 | |
| 55 | // Releases [From, To) range of pages back to OS. |
| 56 | void (uptr From, uptr To) { |
| 57 | const uptr Size = To - From; |
| 58 | releasePagesToOS(BaseAddress: Base, Offset: From + Offset, Size, Data); |
| 59 | ReleasedBytes += Size; |
| 60 | } |
| 61 | |
| 62 | private: |
| 63 | uptr ReleasedBytes = 0; |
| 64 | // The starting address to release. Note that we may want to combine (Base + |
| 65 | // Offset) as a new Base. However, the Base is retrieved from |
| 66 | // `MapPlatformData` on Fuchsia, which means the offset won't be aware. |
| 67 | // Therefore, store them separately to make it work on all the platforms. |
| 68 | uptr Base = 0; |
| 69 | // The release offset from Base. This is used when we know a given range after |
| 70 | // Base will not be released. |
| 71 | uptr Offset = 0; |
| 72 | MapPlatformData *Data = nullptr; |
| 73 | }; |
| 74 | |
| 75 | class FragmentationRecorder { |
| 76 | public: |
| 77 | FragmentationRecorder() = default; |
| 78 | |
| 79 | uptr getReleasedPagesCount() const { return ReleasedPagesCount; } |
| 80 | |
| 81 | void (uptr From, uptr To) { |
| 82 | DCHECK_EQ((To - From) % getPageSizeCached(), 0U); |
| 83 | ReleasedPagesCount += (To - From) >> getPageSizeLogCached(); |
| 84 | } |
| 85 | |
| 86 | private: |
| 87 | uptr ReleasedPagesCount = 0; |
| 88 | }; |
| 89 | |
| 90 | template <uptr GroupSize, uptr NumGroups> |
| 91 | class MemoryGroupFragmentationRecorder { |
| 92 | public: |
| 93 | const uptr NumPagesInOneGroup = GroupSize / getPageSizeCached(); |
| 94 | |
| 95 | void (uptr From, uptr To) { |
| 96 | for (uptr I = From / getPageSizeCached(); I < To / getPageSizeCached(); ++I) |
| 97 | ++FreePagesCount[I / NumPagesInOneGroup]; |
| 98 | } |
| 99 | |
| 100 | uptr getNumFreePages(uptr GroupId) { return FreePagesCount[GroupId]; } |
| 101 | |
| 102 | private: |
| 103 | uptr FreePagesCount[NumGroups] = {}; |
| 104 | }; |
| 105 | |
| 106 | // A buffer pool which holds a fixed number of static buffers of `uptr` elements |
| 107 | // for fast buffer allocation. If the request size is greater than |
| 108 | // `StaticBufferNumElements` or if all the static buffers are in use, it'll |
| 109 | // delegate the allocation to map(). |
| 110 | template <uptr StaticBufferCount, uptr StaticBufferNumElements> |
| 111 | class BufferPool { |
| 112 | public: |
| 113 | // Preserve 1 bit in the `Mask` so that we don't need to do zero-check while |
| 114 | // extracting the least significant bit from the `Mask`. |
| 115 | static_assert(StaticBufferCount < SCUDO_WORDSIZE, "" ); |
| 116 | static_assert(isAligned(X: StaticBufferNumElements * sizeof(uptr), |
| 117 | SCUDO_CACHE_LINE_SIZE), |
| 118 | "" ); |
| 119 | |
| 120 | struct Buffer { |
| 121 | // Pointer to the buffer's memory, or nullptr if no buffer was allocated. |
| 122 | uptr *Data = nullptr; |
| 123 | |
| 124 | // The index of the underlying static buffer, or StaticBufferCount if this |
| 125 | // buffer was dynamically allocated. This value is initially set to a poison |
| 126 | // value to aid debugging. |
| 127 | uptr BufferIndex = ~static_cast<uptr>(0); |
| 128 | |
| 129 | // Only valid if BufferIndex == StaticBufferCount. |
| 130 | MemMapT MemMap = {}; |
| 131 | }; |
| 132 | |
| 133 | // Return a zero-initialized buffer which can contain at least the given |
| 134 | // number of elements, or nullptr on failure. |
| 135 | Buffer getBuffer(const uptr NumElements) { |
| 136 | if (UNLIKELY(NumElements > StaticBufferNumElements)) |
| 137 | return getDynamicBuffer(NumElements); |
| 138 | |
| 139 | uptr index; |
| 140 | { |
| 141 | // TODO: In general, we expect this operation should be fast so the |
| 142 | // waiting thread won't be put into sleep. The HybridMutex does implement |
| 143 | // the busy-waiting but we may want to review the performance and see if |
| 144 | // we need an explict spin lock here. |
| 145 | ScopedLock L(Mutex); |
| 146 | index = getLeastSignificantSetBitIndex(X: Mask); |
| 147 | if (index < StaticBufferCount) |
| 148 | Mask ^= static_cast<uptr>(1) << index; |
| 149 | } |
| 150 | |
| 151 | if (index >= StaticBufferCount) |
| 152 | return getDynamicBuffer(NumElements); |
| 153 | |
| 154 | Buffer Buf; |
| 155 | Buf.Data = &RawBuffer[index * StaticBufferNumElements]; |
| 156 | Buf.BufferIndex = index; |
| 157 | memset(Buf.Data, 0, StaticBufferNumElements * sizeof(uptr)); |
| 158 | return Buf; |
| 159 | } |
| 160 | |
| 161 | void releaseBuffer(Buffer Buf) { |
| 162 | DCHECK_NE(Buf.Data, nullptr); |
| 163 | DCHECK_LE(Buf.BufferIndex, StaticBufferCount); |
| 164 | if (Buf.BufferIndex != StaticBufferCount) { |
| 165 | ScopedLock L(Mutex); |
| 166 | DCHECK_EQ((Mask & (static_cast<uptr>(1) << Buf.BufferIndex)), 0U); |
| 167 | Mask |= static_cast<uptr>(1) << Buf.BufferIndex; |
| 168 | } else { |
| 169 | Buf.MemMap.unmap(); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | bool isStaticBufferTestOnly(const Buffer &Buf) { |
| 174 | DCHECK_NE(Buf.Data, nullptr); |
| 175 | DCHECK_LE(Buf.BufferIndex, StaticBufferCount); |
| 176 | return Buf.BufferIndex != StaticBufferCount; |
| 177 | } |
| 178 | |
| 179 | private: |
| 180 | Buffer getDynamicBuffer(const uptr NumElements) { |
| 181 | // When using a heap-based buffer, precommit the pages backing the |
| 182 | // Vmar by passing |MAP_PRECOMMIT| flag. This allows an optimization |
| 183 | // where page fault exceptions are skipped as the allocated memory |
| 184 | // is accessed. So far, this is only enabled on Fuchsia. It hasn't proven a |
| 185 | // performance benefit on other platforms. |
| 186 | const uptr MmapFlags = MAP_ALLOWNOMEM | (SCUDO_FUCHSIA ? MAP_PRECOMMIT : 0); |
| 187 | const uptr MappedSize = |
| 188 | roundUp(X: NumElements * sizeof(uptr), Boundary: getPageSizeCached()); |
| 189 | Buffer Buf; |
| 190 | if (Buf.MemMap.map(/*Addr=*/0, MappedSize, "scudo:counters" , MmapFlags)) { |
| 191 | Buf.Data = reinterpret_cast<uptr *>(Buf.MemMap.getBase()); |
| 192 | Buf.BufferIndex = StaticBufferCount; |
| 193 | } |
| 194 | return Buf; |
| 195 | } |
| 196 | |
| 197 | HybridMutex Mutex; |
| 198 | // '1' means that buffer index is not used. '0' means the buffer is in use. |
| 199 | uptr Mask GUARDED_BY(Mutex) = ~static_cast<uptr>(0); |
| 200 | uptr RawBuffer[StaticBufferCount * StaticBufferNumElements] GUARDED_BY(Mutex); |
| 201 | }; |
| 202 | |
| 203 | // A Region page map is used to record the usage of pages in the regions. It |
| 204 | // implements a packed array of Counters. Each counter occupies 2^N bits, enough |
| 205 | // to store counter's MaxValue. Ctor will try to use a static buffer first, and |
| 206 | // if that fails (the buffer is too small or already locked), will allocate the |
| 207 | // required Buffer via map(). The caller is expected to check whether the |
| 208 | // initialization was successful by checking isAllocated() result. For |
| 209 | // performance sake, none of the accessors check the validity of the arguments, |
| 210 | // It is assumed that Index is always in [0, N) range and the value is not |
| 211 | // incremented past MaxValue. |
| 212 | class RegionPageMap { |
| 213 | public: |
| 214 | RegionPageMap() |
| 215 | : Regions(0), NumCounters(0), CounterSizeBitsLog(0), CounterMask(0), |
| 216 | PackingRatioLog(0), BitOffsetMask(0), SizePerRegion(0), |
| 217 | BufferNumElements(0) {} |
| 218 | RegionPageMap(uptr NumberOfRegions, uptr CountersPerRegion, uptr MaxValue) { |
| 219 | reset(NumberOfRegion: NumberOfRegions, CountersPerRegion, MaxValue); |
| 220 | } |
| 221 | ~RegionPageMap() { |
| 222 | if (!isAllocated()) |
| 223 | return; |
| 224 | Buffers.releaseBuffer(Buf: Buffer); |
| 225 | Buffer = {}; |
| 226 | } |
| 227 | |
| 228 | // Lock of `StaticBuffer` is acquired conditionally and there's no easy way to |
| 229 | // specify the thread-safety attribute properly in current code structure. |
| 230 | // Besides, it's the only place we may want to check thread safety. Therefore, |
| 231 | // it's fine to bypass the thread-safety analysis now. |
| 232 | void reset(uptr NumberOfRegion, uptr CountersPerRegion, uptr MaxValue) { |
| 233 | DCHECK_GT(NumberOfRegion, 0); |
| 234 | DCHECK_GT(CountersPerRegion, 0); |
| 235 | DCHECK_GT(MaxValue, 0); |
| 236 | |
| 237 | Regions = NumberOfRegion; |
| 238 | NumCounters = CountersPerRegion; |
| 239 | |
| 240 | constexpr uptr MaxCounterBits = sizeof(*Buffer.Data) * 8UL; |
| 241 | // Rounding counter storage size up to the power of two allows for using |
| 242 | // bit shifts calculating particular counter's Index and offset. |
| 243 | const uptr CounterSizeBits = |
| 244 | roundUpPowerOfTwo(Size: getMostSignificantSetBitIndex(X: MaxValue) + 1); |
| 245 | DCHECK_LE(CounterSizeBits, MaxCounterBits); |
| 246 | CounterSizeBitsLog = getLog2(X: CounterSizeBits); |
| 247 | CounterMask = ~(static_cast<uptr>(0)) >> (MaxCounterBits - CounterSizeBits); |
| 248 | |
| 249 | const uptr PackingRatio = MaxCounterBits >> CounterSizeBitsLog; |
| 250 | DCHECK_GT(PackingRatio, 0); |
| 251 | PackingRatioLog = getLog2(X: PackingRatio); |
| 252 | BitOffsetMask = PackingRatio - 1; |
| 253 | |
| 254 | SizePerRegion = |
| 255 | roundUp(X: NumCounters, Boundary: static_cast<uptr>(1U) << PackingRatioLog) >> |
| 256 | PackingRatioLog; |
| 257 | BufferNumElements = SizePerRegion * Regions; |
| 258 | Buffer = Buffers.getBuffer(NumElements: BufferNumElements); |
| 259 | } |
| 260 | |
| 261 | bool isAllocated() const { return Buffer.Data != nullptr; } |
| 262 | |
| 263 | uptr getCount() const { return NumCounters; } |
| 264 | |
| 265 | uptr get(uptr Region, uptr I) const { |
| 266 | DCHECK_LT(Region, Regions); |
| 267 | DCHECK_LT(I, NumCounters); |
| 268 | const uptr Index = I >> PackingRatioLog; |
| 269 | const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; |
| 270 | return (Buffer.Data[Region * SizePerRegion + Index] >> BitOffset) & |
| 271 | CounterMask; |
| 272 | } |
| 273 | |
| 274 | void inc(uptr Region, uptr I) const { |
| 275 | DCHECK_LT(get(Region, I), CounterMask); |
| 276 | const uptr Index = I >> PackingRatioLog; |
| 277 | const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; |
| 278 | DCHECK_LT(BitOffset, SCUDO_WORDSIZE); |
| 279 | DCHECK_EQ(isAllCounted(Region, I), false); |
| 280 | Buffer.Data[Region * SizePerRegion + Index] += static_cast<uptr>(1U) |
| 281 | << BitOffset; |
| 282 | } |
| 283 | |
| 284 | void incN(uptr Region, uptr I, uptr N) const { |
| 285 | DCHECK_GT(N, 0U); |
| 286 | DCHECK_LE(N, CounterMask); |
| 287 | DCHECK_LE(get(Region, I), CounterMask - N); |
| 288 | const uptr Index = I >> PackingRatioLog; |
| 289 | const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; |
| 290 | DCHECK_LT(BitOffset, SCUDO_WORDSIZE); |
| 291 | DCHECK_EQ(isAllCounted(Region, I), false); |
| 292 | Buffer.Data[Region * SizePerRegion + Index] += N << BitOffset; |
| 293 | } |
| 294 | |
| 295 | void incRange(uptr Region, uptr From, uptr To) const { |
| 296 | DCHECK_LE(From, To); |
| 297 | const uptr Top = Min(A: To + 1, B: NumCounters); |
| 298 | for (uptr I = From; I < Top; I++) |
| 299 | inc(Region, I); |
| 300 | } |
| 301 | |
| 302 | // Set the counter to the max value. Note that the max number of blocks in a |
| 303 | // page may vary. To provide an easier way to tell if all the blocks are |
| 304 | // counted for different pages, set to the same max value to denote the |
| 305 | // all-counted status. |
| 306 | void setAsAllCounted(uptr Region, uptr I) const { |
| 307 | DCHECK_LE(get(Region, I), CounterMask); |
| 308 | const uptr Index = I >> PackingRatioLog; |
| 309 | const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; |
| 310 | DCHECK_LT(BitOffset, SCUDO_WORDSIZE); |
| 311 | Buffer.Data[Region * SizePerRegion + Index] |= CounterMask << BitOffset; |
| 312 | } |
| 313 | void setAsAllCountedRange(uptr Region, uptr From, uptr To) const { |
| 314 | DCHECK_LE(From, To); |
| 315 | const uptr Top = Min(A: To + 1, B: NumCounters); |
| 316 | for (uptr I = From; I < Top; I++) |
| 317 | setAsAllCounted(Region, I); |
| 318 | } |
| 319 | |
| 320 | bool updateAsAllCountedIf(uptr Region, uptr I, uptr MaxCount) { |
| 321 | const uptr Count = get(Region, I); |
| 322 | if (Count == CounterMask) |
| 323 | return true; |
| 324 | if (Count == MaxCount) { |
| 325 | setAsAllCounted(Region, I); |
| 326 | return true; |
| 327 | } |
| 328 | return false; |
| 329 | } |
| 330 | bool isAllCounted(uptr Region, uptr I) const { |
| 331 | return get(Region, I) == CounterMask; |
| 332 | } |
| 333 | |
| 334 | uptr getBufferNumElements() const { return BufferNumElements; } |
| 335 | |
| 336 | private: |
| 337 | // We may consider making this configurable if there are cases which may |
| 338 | // benefit from this. |
| 339 | static const uptr StaticBufferCount = 2U; |
| 340 | static const uptr StaticBufferNumElements = 512U; |
| 341 | using BufferPoolT = BufferPool<StaticBufferCount, StaticBufferNumElements>; |
| 342 | static BufferPoolT Buffers; |
| 343 | |
| 344 | uptr Regions; |
| 345 | uptr NumCounters; |
| 346 | uptr CounterSizeBitsLog; |
| 347 | uptr CounterMask; |
| 348 | uptr PackingRatioLog; |
| 349 | uptr BitOffsetMask; |
| 350 | |
| 351 | uptr SizePerRegion; |
| 352 | uptr BufferNumElements; |
| 353 | BufferPoolT::Buffer Buffer; |
| 354 | }; |
| 355 | |
| 356 | template <class ReleaseRecorderT> class FreePagesRangeTracker { |
| 357 | public: |
| 358 | explicit FreePagesRangeTracker(ReleaseRecorderT &Recorder) |
| 359 | : Recorder(Recorder) {} |
| 360 | |
| 361 | void processNextPage(bool Released) { |
| 362 | if (Released) { |
| 363 | if (!InRange) { |
| 364 | CurrentRangeStatePage = CurrentPage; |
| 365 | InRange = true; |
| 366 | } |
| 367 | } else { |
| 368 | closeOpenedRange(); |
| 369 | } |
| 370 | CurrentPage++; |
| 371 | } |
| 372 | |
| 373 | void skipPages(uptr N) { |
| 374 | closeOpenedRange(); |
| 375 | CurrentPage += N; |
| 376 | } |
| 377 | |
| 378 | void finish() { closeOpenedRange(); } |
| 379 | |
| 380 | private: |
| 381 | void closeOpenedRange() { |
| 382 | if (InRange) { |
| 383 | const uptr PageSizeLog = getPageSizeLogCached(); |
| 384 | Recorder.releasePageRangeToOS((CurrentRangeStatePage << PageSizeLog), |
| 385 | (CurrentPage << PageSizeLog)); |
| 386 | InRange = false; |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | ReleaseRecorderT &Recorder; |
| 391 | bool InRange = false; |
| 392 | uptr CurrentPage = 0; |
| 393 | uptr CurrentRangeStatePage = 0; |
| 394 | }; |
| 395 | |
| 396 | struct { |
| 397 | (uptr BlockSize, uptr NumberOfRegions, uptr ReleaseSize, |
| 398 | uptr ReleaseOffset = 0) |
| 399 | : BlockSize(BlockSize), NumberOfRegions(NumberOfRegions) { |
| 400 | const uptr PageSize = getPageSizeCached(); |
| 401 | if (BlockSize <= PageSize) { |
| 402 | if (PageSize % BlockSize == 0) { |
| 403 | // Same number of chunks per page, no cross overs. |
| 404 | FullPagesBlockCountMax = PageSize / BlockSize; |
| 405 | SameBlockCountPerPage = true; |
| 406 | } else if (BlockSize % (PageSize % BlockSize) == 0) { |
| 407 | // Some chunks are crossing page boundaries, which means that the page |
| 408 | // contains one or two partial chunks, but all pages contain the same |
| 409 | // number of chunks. |
| 410 | FullPagesBlockCountMax = PageSize / BlockSize + 1; |
| 411 | SameBlockCountPerPage = true; |
| 412 | } else { |
| 413 | // Some chunks are crossing page boundaries, which means that the page |
| 414 | // contains one or two partial chunks. |
| 415 | FullPagesBlockCountMax = PageSize / BlockSize + 2; |
| 416 | SameBlockCountPerPage = false; |
| 417 | } |
| 418 | } else { |
| 419 | if ((BlockSize & (PageSize - 1)) == 0) { |
| 420 | // One chunk covers multiple pages, no cross overs. |
| 421 | FullPagesBlockCountMax = 1; |
| 422 | SameBlockCountPerPage = true; |
| 423 | } else { |
| 424 | // One chunk covers multiple pages, Some chunks are crossing page |
| 425 | // boundaries. Some pages contain one chunk, some contain two. |
| 426 | FullPagesBlockCountMax = 2; |
| 427 | SameBlockCountPerPage = false; |
| 428 | } |
| 429 | } |
| 430 | |
| 431 | // TODO: For multiple regions, it's more complicated to support partial |
| 432 | // region marking (which includes the complexity of how to handle the last |
| 433 | // block in a region). We may consider this after markFreeBlocks() accepts |
| 434 | // only free blocks from the same region. |
| 435 | if (NumberOfRegions != 1) |
| 436 | DCHECK_EQ(ReleaseOffset, 0U); |
| 437 | |
| 438 | const uptr PageSizeLog = getPageSizeLogCached(); |
| 439 | PagesCount = roundUp(X: ReleaseSize, Boundary: PageSize) >> PageSizeLog; |
| 440 | ReleasePageOffset = ReleaseOffset >> PageSizeLog; |
| 441 | } |
| 442 | |
| 443 | // PageMap is lazily allocated when markFreeBlocks() is invoked. |
| 444 | bool () const { |
| 445 | return PageMap.isAllocated(); |
| 446 | } |
| 447 | |
| 448 | bool () { |
| 449 | if (PageMap.isAllocated()) |
| 450 | return true; |
| 451 | PageMap.reset(NumberOfRegion: NumberOfRegions, CountersPerRegion: PagesCount, MaxValue: FullPagesBlockCountMax); |
| 452 | // TODO: Log some message when we fail on PageMap allocation. |
| 453 | return PageMap.isAllocated(); |
| 454 | } |
| 455 | |
| 456 | // Mark all the blocks in the given range [From, to). Instead of visiting all |
| 457 | // the blocks, we will just mark the page as all counted. Note the `From` and |
| 458 | // `To` has to be page aligned but with one exception, if `To` is equal to the |
| 459 | // RegionSize, it's not necessary to be aligned with page size. |
| 460 | bool (uptr From, uptr To, uptr Base, |
| 461 | const uptr RegionIndex, const uptr RegionSize) { |
| 462 | const uptr PageSize = getPageSizeCached(); |
| 463 | DCHECK_LT(From, To); |
| 464 | DCHECK_LE(To, Base + RegionSize); |
| 465 | DCHECK_EQ(From % PageSize, 0U); |
| 466 | DCHECK_LE(To - From, RegionSize); |
| 467 | |
| 468 | if (!ensurePageMapAllocated()) |
| 469 | return false; |
| 470 | |
| 471 | uptr FromInRegion = From - Base; |
| 472 | uptr ToInRegion = To - Base; |
| 473 | uptr FirstBlockInRange = roundUpSlow(X: FromInRegion, Boundary: BlockSize); |
| 474 | |
| 475 | // The straddling block sits across entire range. |
| 476 | if (FirstBlockInRange >= ToInRegion) |
| 477 | return true; |
| 478 | |
| 479 | // First block may not sit at the first pape in the range, move |
| 480 | // `FromInRegion` to the first block page. |
| 481 | FromInRegion = roundDown(X: FirstBlockInRange, Boundary: PageSize); |
| 482 | |
| 483 | // When The first block is not aligned to the range boundary, which means |
| 484 | // there is a block sitting acorss `From`, that looks like, |
| 485 | // |
| 486 | // From To |
| 487 | // V V |
| 488 | // +-----------------------------------------------+ |
| 489 | // +-----+-----+-----+-----+ |
| 490 | // | | | | | ... |
| 491 | // +-----+-----+-----+-----+ |
| 492 | // |- first page -||- second page -||- ... |
| 493 | // |
| 494 | // Therefore, we can't just mark the first page as all counted. Instead, we |
| 495 | // increment the number of blocks in the first page in the page map and |
| 496 | // then round up the `From` to the next page. |
| 497 | if (FirstBlockInRange != FromInRegion) { |
| 498 | DCHECK_GT(FromInRegion + PageSize, FirstBlockInRange); |
| 499 | uptr NumBlocksInFirstPage = |
| 500 | (FromInRegion + PageSize - FirstBlockInRange + BlockSize - 1) / |
| 501 | BlockSize; |
| 502 | PageMap.incN(Region: RegionIndex, I: getPageIndex(P: FromInRegion), |
| 503 | N: NumBlocksInFirstPage); |
| 504 | FromInRegion = roundUp(X: FromInRegion + 1, Boundary: PageSize); |
| 505 | } |
| 506 | |
| 507 | uptr LastBlockInRange = roundDownSlow(X: ToInRegion - 1, Boundary: BlockSize); |
| 508 | |
| 509 | // Note that LastBlockInRange may be smaller than `FromInRegion` at this |
| 510 | // point because it may contain only one block in the range. |
| 511 | |
| 512 | // When the last block sits across `To`, we can't just mark the pages |
| 513 | // occupied by the last block as all counted. Instead, we increment the |
| 514 | // counters of those pages by 1. The exception is that if it's the last |
| 515 | // block in the region, it's fine to mark those pages as all counted. |
| 516 | if (LastBlockInRange + BlockSize != RegionSize) { |
| 517 | DCHECK_EQ(ToInRegion % PageSize, 0U); |
| 518 | // The case below is like, |
| 519 | // |
| 520 | // From To |
| 521 | // V V |
| 522 | // +----------------------------------------+ |
| 523 | // +-----+-----+-----+-----+ |
| 524 | // | | | | | ... |
| 525 | // +-----+-----+-----+-----+ |
| 526 | // ... -||- last page -||- next page -| |
| 527 | // |
| 528 | // The last block is not aligned to `To`, we need to increment the |
| 529 | // counter of `next page` by 1. |
| 530 | if (LastBlockInRange + BlockSize != ToInRegion) { |
| 531 | PageMap.incRange(Region: RegionIndex, From: getPageIndex(P: ToInRegion), |
| 532 | To: getPageIndex(P: LastBlockInRange + BlockSize - 1)); |
| 533 | } |
| 534 | } else { |
| 535 | ToInRegion = RegionSize; |
| 536 | } |
| 537 | |
| 538 | // After handling the first page and the last block, it's safe to mark any |
| 539 | // page in between the range [From, To). |
| 540 | if (FromInRegion < ToInRegion) { |
| 541 | PageMap.setAsAllCountedRange(Region: RegionIndex, From: getPageIndex(P: FromInRegion), |
| 542 | To: getPageIndex(P: ToInRegion - 1)); |
| 543 | } |
| 544 | |
| 545 | return true; |
| 546 | } |
| 547 | |
| 548 | template <class TransferBatchT, typename DecompactPtrT> |
| 549 | bool (const IntrusiveList<TransferBatchT> &FreeList, |
| 550 | DecompactPtrT DecompactPtr, const uptr Base, |
| 551 | const uptr RegionIndex, const uptr RegionSize, |
| 552 | bool MayContainLastBlockInRegion) { |
| 553 | if (!ensurePageMapAllocated()) |
| 554 | return false; |
| 555 | |
| 556 | const uptr PageSize = getPageSizeCached(); |
| 557 | if (MayContainLastBlockInRegion) { |
| 558 | const uptr LastBlockInRegion = |
| 559 | ((RegionSize / BlockSize) - 1U) * BlockSize; |
| 560 | // The last block in a region may not use the entire page, we mark the |
| 561 | // following "pretend" memory block(s) as free in advance. |
| 562 | // |
| 563 | // Region Boundary |
| 564 | // v |
| 565 | // -----+-----------------------+ |
| 566 | // | Last Page | <- Rounded Region Boundary |
| 567 | // -----+-----------------------+ |
| 568 | // |-----||- trailing blocks -| |
| 569 | // ^ |
| 570 | // last block |
| 571 | const uptr RoundedRegionSize = roundUp(X: RegionSize, Boundary: PageSize); |
| 572 | const uptr TrailingBlockBase = LastBlockInRegion + BlockSize; |
| 573 | // If the difference between `RoundedRegionSize` and |
| 574 | // `TrailingBlockBase` is larger than a page, that implies the reported |
| 575 | // `RegionSize` may not be accurate. |
| 576 | DCHECK_LT(RoundedRegionSize - TrailingBlockBase, PageSize); |
| 577 | |
| 578 | // Only the last page touched by the last block needs to mark the trailing |
| 579 | // blocks. Note that if the last "pretend" block straddles the boundary, |
| 580 | // we still have to count it in so that the logic of counting the number |
| 581 | // of blocks on a page is consistent. |
| 582 | uptr NumTrailingBlocks = |
| 583 | (roundUpSlow(X: RoundedRegionSize - TrailingBlockBase, Boundary: BlockSize) + |
| 584 | BlockSize - 1) / |
| 585 | BlockSize; |
| 586 | if (NumTrailingBlocks > 0) { |
| 587 | PageMap.incN(Region: RegionIndex, I: getPageIndex(P: TrailingBlockBase), |
| 588 | N: NumTrailingBlocks); |
| 589 | } |
| 590 | } |
| 591 | |
| 592 | // Iterate over free chunks and count how many free chunks affect each |
| 593 | // allocated page. |
| 594 | if (BlockSize <= PageSize && PageSize % BlockSize == 0) { |
| 595 | // Each chunk affects one page only. |
| 596 | for (const auto &It : FreeList) { |
| 597 | for (u16 I = 0; I < It.getCount(); I++) { |
| 598 | const uptr PInRegion = DecompactPtr(It.get(I)) - Base; |
| 599 | DCHECK_LT(PInRegion, RegionSize); |
| 600 | PageMap.inc(Region: RegionIndex, I: getPageIndex(P: PInRegion)); |
| 601 | } |
| 602 | } |
| 603 | } else { |
| 604 | // In all other cases chunks might affect more than one page. |
| 605 | DCHECK_GE(RegionSize, BlockSize); |
| 606 | for (const auto &It : FreeList) { |
| 607 | for (u16 I = 0; I < It.getCount(); I++) { |
| 608 | const uptr PInRegion = DecompactPtr(It.get(I)) - Base; |
| 609 | PageMap.incRange(Region: RegionIndex, From: getPageIndex(P: PInRegion), |
| 610 | To: getPageIndex(P: PInRegion + BlockSize - 1)); |
| 611 | } |
| 612 | } |
| 613 | } |
| 614 | |
| 615 | return true; |
| 616 | } |
| 617 | |
| 618 | uptr (uptr P) { |
| 619 | return (P >> getPageSizeLogCached()) - ReleasePageOffset; |
| 620 | } |
| 621 | uptr () { |
| 622 | return ReleasePageOffset << getPageSizeLogCached(); |
| 623 | } |
| 624 | |
| 625 | uptr ; |
| 626 | uptr ; |
| 627 | // For partial region marking, some pages in front are not needed to be |
| 628 | // counted. |
| 629 | uptr ; |
| 630 | uptr ; |
| 631 | uptr ; |
| 632 | bool ; |
| 633 | RegionPageMap ; |
| 634 | }; |
| 635 | |
| 636 | // Try to release the page which doesn't have any in-used block, i.e., they are |
| 637 | // all free blocks. The `PageMap` will record the number of free blocks in each |
| 638 | // page. |
| 639 | template <class ReleaseRecorderT, typename SkipRegionT> |
| 640 | NOINLINE void |
| 641 | (PageReleaseContext &Context, |
| 642 | ReleaseRecorderT &Recorder, SkipRegionT SkipRegion) { |
| 643 | const uptr PageSize = getPageSizeCached(); |
| 644 | const uptr BlockSize = Context.BlockSize; |
| 645 | const uptr PagesCount = Context.PagesCount; |
| 646 | const uptr NumberOfRegions = Context.NumberOfRegions; |
| 647 | const uptr ReleasePageOffset = Context.ReleasePageOffset; |
| 648 | const uptr FullPagesBlockCountMax = Context.FullPagesBlockCountMax; |
| 649 | const bool SameBlockCountPerPage = Context.SameBlockCountPerPage; |
| 650 | RegionPageMap &PageMap = Context.PageMap; |
| 651 | |
| 652 | // Iterate over pages detecting ranges of pages with chunk Counters equal |
| 653 | // to the expected number of chunks for the particular page. |
| 654 | FreePagesRangeTracker<ReleaseRecorderT> RangeTracker(Recorder); |
| 655 | if (SameBlockCountPerPage) { |
| 656 | // Fast path, every page has the same number of chunks affecting it. |
| 657 | for (uptr I = 0; I < NumberOfRegions; I++) { |
| 658 | if (SkipRegion(I)) { |
| 659 | RangeTracker.skipPages(PagesCount); |
| 660 | continue; |
| 661 | } |
| 662 | for (uptr J = 0; J < PagesCount; J++) { |
| 663 | const bool CanRelease = |
| 664 | PageMap.updateAsAllCountedIf(Region: I, I: J, MaxCount: FullPagesBlockCountMax); |
| 665 | RangeTracker.processNextPage(CanRelease); |
| 666 | } |
| 667 | } |
| 668 | } else { |
| 669 | // Slow path, go through the pages keeping count how many chunks affect |
| 670 | // each page. |
| 671 | const uptr Pn = BlockSize < PageSize ? PageSize / BlockSize : 1; |
| 672 | const uptr Pnc = Pn * BlockSize; |
| 673 | // The idea is to increment the current page pointer by the first chunk |
| 674 | // size, middle portion size (the portion of the page covered by chunks |
| 675 | // except the first and the last one) and then the last chunk size, adding |
| 676 | // up the number of chunks on the current page and checking on every step |
| 677 | // whether the page boundary was crossed. |
| 678 | for (uptr I = 0; I < NumberOfRegions; I++) { |
| 679 | if (SkipRegion(I)) { |
| 680 | RangeTracker.skipPages(PagesCount); |
| 681 | continue; |
| 682 | } |
| 683 | uptr PrevPageBoundary = 0; |
| 684 | uptr CurrentBoundary = 0; |
| 685 | if (ReleasePageOffset > 0) { |
| 686 | PrevPageBoundary = ReleasePageOffset << getPageSizeLogCached(); |
| 687 | CurrentBoundary = roundUpSlow(X: PrevPageBoundary, Boundary: BlockSize); |
| 688 | } |
| 689 | for (uptr J = 0; J < PagesCount; J++) { |
| 690 | const uptr PageBoundary = PrevPageBoundary + PageSize; |
| 691 | uptr BlocksPerPage = Pn; |
| 692 | if (CurrentBoundary < PageBoundary) { |
| 693 | if (CurrentBoundary > PrevPageBoundary) |
| 694 | BlocksPerPage++; |
| 695 | CurrentBoundary += Pnc; |
| 696 | if (CurrentBoundary < PageBoundary) { |
| 697 | BlocksPerPage++; |
| 698 | CurrentBoundary += BlockSize; |
| 699 | } |
| 700 | } |
| 701 | PrevPageBoundary = PageBoundary; |
| 702 | const bool CanRelease = |
| 703 | PageMap.updateAsAllCountedIf(Region: I, I: J, MaxCount: BlocksPerPage); |
| 704 | RangeTracker.processNextPage(CanRelease); |
| 705 | } |
| 706 | } |
| 707 | } |
| 708 | RangeTracker.finish(); |
| 709 | } |
| 710 | |
| 711 | } // namespace scudo |
| 712 | |
| 713 | #endif // SCUDO_RELEASE_H_ |
| 714 | |