//===-- release.h -----------------------------------------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #ifndef SCUDO_RELEASE_H_ #define SCUDO_RELEASE_H_ #include "common.h" #include "list.h" #include "mem_map.h" #include "mutex.h" #include "thread_annotations.h" namespace scudo { template class RegionReleaseRecorder { public: RegionReleaseRecorder(MemMapT *RegionMemMap, uptr Base, uptr Offset = 0) : RegionMemMap(RegionMemMap), Base(Base), Offset(Offset) {} uptr getReleasedRangesCount() const { return ReleasedRangesCount; } uptr getReleasedBytes() const { return ReleasedBytes; } uptr getBase() const { return Base; } // Releases [From, To) range of pages back to OS. Note that `From` and `To` // are offseted from `Base` + Offset. void releasePageRangeToOS(uptr From, uptr To) { const uptr Size = To - From; RegionMemMap->releasePagesToOS(getBase() + Offset + From, Size); ReleasedRangesCount++; ReleasedBytes += Size; } private: uptr ReleasedRangesCount = 0; uptr ReleasedBytes = 0; MemMapT *RegionMemMap = nullptr; uptr Base = 0; // The release offset from Base. This is used when we know a given range after // Base will not be released. uptr Offset = 0; }; class ReleaseRecorder { public: ReleaseRecorder(uptr Base, uptr Offset = 0, MapPlatformData *Data = nullptr) : Base(Base), Offset(Offset), Data(Data) {} uptr getReleasedRangesCount() const { return ReleasedRangesCount; } uptr getReleasedBytes() const { return ReleasedBytes; } uptr getBase() const { return Base; } // Releases [From, To) range of pages back to OS. void releasePageRangeToOS(uptr From, uptr To) { const uptr Size = To - From; releasePagesToOS(Base, From + Offset, Size, Data); ReleasedRangesCount++; ReleasedBytes += Size; } private: uptr ReleasedRangesCount = 0; uptr ReleasedBytes = 0; // The starting address to release. Note that we may want to combine (Base + // Offset) as a new Base. However, the Base is retrieved from // `MapPlatformData` on Fuchsia, which means the offset won't be aware. // Therefore, store them separately to make it work on all the platforms. uptr Base = 0; // The release offset from Base. This is used when we know a given range after // Base will not be released. uptr Offset = 0; MapPlatformData *Data = nullptr; }; // A buffer pool which holds a fixed number of static buffers for fast buffer // allocation. If the request size is greater than `StaticBufferSize`, it'll // delegate the allocation to map(). template class BufferPool { public: // Preserve 1 bit in the `Mask` so that we don't need to do zero-check while // extracting the least significant bit from the `Mask`. static_assert(StaticBufferCount < SCUDO_WORDSIZE, ""); static_assert(isAligned(StaticBufferSize, SCUDO_CACHE_LINE_SIZE), ""); // Return a buffer which is at least `BufferSize`. uptr *getBuffer(const uptr BufferSize) { if (UNLIKELY(BufferSize > StaticBufferSize)) return getDynamicBuffer(BufferSize); uptr index; { // TODO: In general, we expect this operation should be fast so the // waiting thread won't be put into sleep. The HybridMutex does implement // the busy-waiting but we may want to review the performance and see if // we need an explict spin lock here. ScopedLock L(Mutex); index = getLeastSignificantSetBitIndex(Mask); if (index < StaticBufferCount) Mask ^= static_cast(1) << index; } if (index >= StaticBufferCount) return getDynamicBuffer(BufferSize); const uptr Offset = index * StaticBufferSize; memset(&RawBuffer[Offset], 0, StaticBufferSize); return &RawBuffer[Offset]; } void releaseBuffer(uptr *Buffer, const uptr BufferSize) { const uptr index = getStaticBufferIndex(Buffer, BufferSize); if (index < StaticBufferCount) { ScopedLock L(Mutex); DCHECK_EQ((Mask & (static_cast(1) << index)), 0U); Mask |= static_cast(1) << index; } else { unmap(reinterpret_cast(Buffer), roundUp(BufferSize, getPageSizeCached())); } } bool isStaticBufferTestOnly(uptr *Buffer, uptr BufferSize) { return getStaticBufferIndex(Buffer, BufferSize) < StaticBufferCount; } private: uptr getStaticBufferIndex(uptr *Buffer, uptr BufferSize) { if (UNLIKELY(BufferSize > StaticBufferSize)) return StaticBufferCount; const uptr BufferBase = reinterpret_cast(Buffer); const uptr RawBufferBase = reinterpret_cast(RawBuffer); if (BufferBase < RawBufferBase || BufferBase >= RawBufferBase + sizeof(RawBuffer)) { return StaticBufferCount; } DCHECK_LE(BufferSize, StaticBufferSize); DCHECK_LE(BufferBase + BufferSize, RawBufferBase + sizeof(RawBuffer)); DCHECK_EQ((BufferBase - RawBufferBase) % StaticBufferSize, 0U); const uptr index = (BufferBase - RawBufferBase) / (StaticBufferSize * sizeof(uptr)); DCHECK_LT(index, StaticBufferCount); return index; } uptr *getDynamicBuffer(const uptr BufferSize) { // When using a heap-based buffer, precommit the pages backing the // Vmar by passing |MAP_PRECOMMIT| flag. This allows an optimization // where page fault exceptions are skipped as the allocated memory // is accessed. So far, this is only enabled on Fuchsia. It hasn't proven a // performance benefit on other platforms. const uptr MmapFlags = MAP_ALLOWNOMEM | (SCUDO_FUCHSIA ? MAP_PRECOMMIT : 0); return reinterpret_cast( map(nullptr, roundUp(BufferSize, getPageSizeCached()), "scudo:counters", MmapFlags, &MapData)); } HybridMutex Mutex; // '1' means that buffer index is not used. '0' means the buffer is in use. uptr Mask GUARDED_BY(Mutex) = ~static_cast(0); uptr RawBuffer[StaticBufferCount * StaticBufferSize] GUARDED_BY(Mutex); [[no_unique_address]] MapPlatformData MapData = {}; }; // A Region page map is used to record the usage of pages in the regions. It // implements a packed array of Counters. Each counter occupies 2^N bits, enough // to store counter's MaxValue. Ctor will try to use a static buffer first, and // if that fails (the buffer is too small or already locked), will allocate the // required Buffer via map(). The caller is expected to check whether the // initialization was successful by checking isAllocated() result. For // performance sake, none of the accessors check the validity of the arguments, // It is assumed that Index is always in [0, N) range and the value is not // incremented past MaxValue. class RegionPageMap { public: RegionPageMap() : Regions(0), NumCounters(0), CounterSizeBitsLog(0), CounterMask(0), PackingRatioLog(0), BitOffsetMask(0), SizePerRegion(0), BufferSize(0), Buffer(nullptr) {} RegionPageMap(uptr NumberOfRegions, uptr CountersPerRegion, uptr MaxValue) { reset(NumberOfRegions, CountersPerRegion, MaxValue); } ~RegionPageMap() { if (!isAllocated()) return; Buffers.releaseBuffer(Buffer, BufferSize); Buffer = nullptr; } // Lock of `StaticBuffer` is acquired conditionally and there's no easy way to // specify the thread-safety attribute properly in current code structure. // Besides, it's the only place we may want to check thread safety. Therefore, // it's fine to bypass the thread-safety analysis now. void reset(uptr NumberOfRegion, uptr CountersPerRegion, uptr MaxValue) { DCHECK_GT(NumberOfRegion, 0); DCHECK_GT(CountersPerRegion, 0); DCHECK_GT(MaxValue, 0); Regions = NumberOfRegion; NumCounters = CountersPerRegion; constexpr uptr MaxCounterBits = sizeof(*Buffer) * 8UL; // Rounding counter storage size up to the power of two allows for using // bit shifts calculating particular counter's Index and offset. const uptr CounterSizeBits = roundUpPowerOfTwo(getMostSignificantSetBitIndex(MaxValue) + 1); DCHECK_LE(CounterSizeBits, MaxCounterBits); CounterSizeBitsLog = getLog2(CounterSizeBits); CounterMask = ~(static_cast(0)) >> (MaxCounterBits - CounterSizeBits); const uptr PackingRatio = MaxCounterBits >> CounterSizeBitsLog; DCHECK_GT(PackingRatio, 0); PackingRatioLog = getLog2(PackingRatio); BitOffsetMask = PackingRatio - 1; SizePerRegion = roundUp(NumCounters, static_cast(1U) << PackingRatioLog) >> PackingRatioLog; BufferSize = SizePerRegion * sizeof(*Buffer) * Regions; Buffer = Buffers.getBuffer(BufferSize); } bool isAllocated() const { return !!Buffer; } uptr getCount() const { return NumCounters; } uptr get(uptr Region, uptr I) const { DCHECK_LT(Region, Regions); DCHECK_LT(I, NumCounters); const uptr Index = I >> PackingRatioLog; const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; return (Buffer[Region * SizePerRegion + Index] >> BitOffset) & CounterMask; } void inc(uptr Region, uptr I) const { DCHECK_LT(get(Region, I), CounterMask); const uptr Index = I >> PackingRatioLog; const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; DCHECK_LT(BitOffset, SCUDO_WORDSIZE); DCHECK_EQ(isAllCounted(Region, I), false); Buffer[Region * SizePerRegion + Index] += static_cast(1U) << BitOffset; } void incN(uptr Region, uptr I, uptr N) const { DCHECK_GT(N, 0U); DCHECK_LE(N, CounterMask); DCHECK_LE(get(Region, I), CounterMask - N); const uptr Index = I >> PackingRatioLog; const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; DCHECK_LT(BitOffset, SCUDO_WORDSIZE); DCHECK_EQ(isAllCounted(Region, I), false); Buffer[Region * SizePerRegion + Index] += N << BitOffset; } void incRange(uptr Region, uptr From, uptr To) const { DCHECK_LE(From, To); const uptr Top = Min(To + 1, NumCounters); for (uptr I = From; I < Top; I++) inc(Region, I); } // Set the counter to the max value. Note that the max number of blocks in a // page may vary. To provide an easier way to tell if all the blocks are // counted for different pages, set to the same max value to denote the // all-counted status. void setAsAllCounted(uptr Region, uptr I) const { DCHECK_LE(get(Region, I), CounterMask); const uptr Index = I >> PackingRatioLog; const uptr BitOffset = (I & BitOffsetMask) << CounterSizeBitsLog; DCHECK_LT(BitOffset, SCUDO_WORDSIZE); Buffer[Region * SizePerRegion + Index] |= CounterMask << BitOffset; } void setAsAllCountedRange(uptr Region, uptr From, uptr To) const { DCHECK_LE(From, To); const uptr Top = Min(To + 1, NumCounters); for (uptr I = From; I < Top; I++) setAsAllCounted(Region, I); } bool updateAsAllCountedIf(uptr Region, uptr I, uptr MaxCount) { const uptr Count = get(Region, I); if (Count == CounterMask) return true; if (Count == MaxCount) { setAsAllCounted(Region, I); return true; } return false; } bool isAllCounted(uptr Region, uptr I) const { return get(Region, I) == CounterMask; } uptr getBufferSize() const { return BufferSize; } private: uptr Regions; uptr NumCounters; uptr CounterSizeBitsLog; uptr CounterMask; uptr PackingRatioLog; uptr BitOffsetMask; uptr SizePerRegion; uptr BufferSize; uptr *Buffer; // We may consider making this configurable if there are cases which may // benefit from this. static const uptr StaticBufferCount = 2U; static const uptr StaticBufferSize = 512U; static BufferPool Buffers; }; template class FreePagesRangeTracker { public: explicit FreePagesRangeTracker(ReleaseRecorderT &Recorder) : Recorder(Recorder), PageSizeLog(getLog2(getPageSizeCached())) {} void processNextPage(bool Released) { if (Released) { if (!InRange) { CurrentRangeStatePage = CurrentPage; InRange = true; } } else { closeOpenedRange(); } CurrentPage++; } void skipPages(uptr N) { closeOpenedRange(); CurrentPage += N; } void finish() { closeOpenedRange(); } private: void closeOpenedRange() { if (InRange) { Recorder.releasePageRangeToOS((CurrentRangeStatePage << PageSizeLog), (CurrentPage << PageSizeLog)); InRange = false; } } ReleaseRecorderT &Recorder; const uptr PageSizeLog; bool InRange = false; uptr CurrentPage = 0; uptr CurrentRangeStatePage = 0; }; struct PageReleaseContext { PageReleaseContext(uptr BlockSize, uptr NumberOfRegions, uptr ReleaseSize, uptr ReleaseOffset = 0) : BlockSize(BlockSize), NumberOfRegions(NumberOfRegions) { PageSize = getPageSizeCached(); if (BlockSize <= PageSize) { if (PageSize % BlockSize == 0) { // Same number of chunks per page, no cross overs. FullPagesBlockCountMax = PageSize / BlockSize; SameBlockCountPerPage = true; } else if (BlockSize % (PageSize % BlockSize) == 0) { // Some chunks are crossing page boundaries, which means that the page // contains one or two partial chunks, but all pages contain the same // number of chunks. FullPagesBlockCountMax = PageSize / BlockSize + 1; SameBlockCountPerPage = true; } else { // Some chunks are crossing page boundaries, which means that the page // contains one or two partial chunks. FullPagesBlockCountMax = PageSize / BlockSize + 2; SameBlockCountPerPage = false; } } else { if (BlockSize % PageSize == 0) { // One chunk covers multiple pages, no cross overs. FullPagesBlockCountMax = 1; SameBlockCountPerPage = true; } else { // One chunk covers multiple pages, Some chunks are crossing page // boundaries. Some pages contain one chunk, some contain two. FullPagesBlockCountMax = 2; SameBlockCountPerPage = false; } } // TODO: For multiple regions, it's more complicated to support partial // region marking (which includes the complexity of how to handle the last // block in a region). We may consider this after markFreeBlocks() accepts // only free blocks from the same region. if (NumberOfRegions != 1) DCHECK_EQ(ReleaseOffset, 0U); PagesCount = roundUp(ReleaseSize, PageSize) / PageSize; PageSizeLog = getLog2(PageSize); ReleasePageOffset = ReleaseOffset >> PageSizeLog; } // PageMap is lazily allocated when markFreeBlocks() is invoked. bool hasBlockMarked() const { return PageMap.isAllocated(); } bool ensurePageMapAllocated() { if (PageMap.isAllocated()) return true; PageMap.reset(NumberOfRegions, PagesCount, FullPagesBlockCountMax); // TODO: Log some message when we fail on PageMap allocation. return PageMap.isAllocated(); } // Mark all the blocks in the given range [From, to). Instead of visiting all // the blocks, we will just mark the page as all counted. Note the `From` and // `To` has to be page aligned but with one exception, if `To` is equal to the // RegionSize, it's not necessary to be aligned with page size. bool markRangeAsAllCounted(uptr From, uptr To, uptr Base, const uptr RegionIndex, const uptr RegionSize) { DCHECK_LT(From, To); DCHECK_LE(To, Base + RegionSize); DCHECK_EQ(From % PageSize, 0U); DCHECK_LE(To - From, RegionSize); if (!ensurePageMapAllocated()) return false; uptr FromInRegion = From - Base; uptr ToInRegion = To - Base; uptr FirstBlockInRange = roundUpSlow(FromInRegion, BlockSize); // The straddling block sits across entire range. if (FirstBlockInRange >= ToInRegion) return true; // First block may not sit at the first pape in the range, move // `FromInRegion` to the first block page. FromInRegion = roundDown(FirstBlockInRange, PageSize); // When The first block is not aligned to the range boundary, which means // there is a block sitting acorss `From`, that looks like, // // From To // V V // +-----------------------------------------------+ // +-----+-----+-----+-----+ // | | | | | ... // +-----+-----+-----+-----+ // |- first page -||- second page -||- ... // // Therefore, we can't just mark the first page as all counted. Instead, we // increment the number of blocks in the first page in the page map and // then round up the `From` to the next page. if (FirstBlockInRange != FromInRegion) { DCHECK_GT(FromInRegion + PageSize, FirstBlockInRange); uptr NumBlocksInFirstPage = (FromInRegion + PageSize - FirstBlockInRange + BlockSize - 1) / BlockSize; PageMap.incN(RegionIndex, getPageIndex(FromInRegion), NumBlocksInFirstPage); FromInRegion = roundUp(FromInRegion + 1, PageSize); } uptr LastBlockInRange = roundDownSlow(ToInRegion - 1, BlockSize); // Note that LastBlockInRange may be smaller than `FromInRegion` at this // point because it may contain only one block in the range. // When the last block sits across `To`, we can't just mark the pages // occupied by the last block as all counted. Instead, we increment the // counters of those pages by 1. The exception is that if it's the last // block in the region, it's fine to mark those pages as all counted. if (LastBlockInRange + BlockSize != RegionSize) { DCHECK_EQ(ToInRegion % PageSize, 0U); // The case below is like, // // From To // V V // +----------------------------------------+ // +-----+-----+-----+-----+ // | | | | | ... // +-----+-----+-----+-----+ // ... -||- last page -||- next page -| // // The last block is not aligned to `To`, we need to increment the // counter of `next page` by 1. if (LastBlockInRange + BlockSize != ToInRegion) { PageMap.incRange(RegionIndex, getPageIndex(ToInRegion), getPageIndex(LastBlockInRange + BlockSize - 1)); } } else { ToInRegion = RegionSize; } // After handling the first page and the last block, it's safe to mark any // page in between the range [From, To). if (FromInRegion < ToInRegion) { PageMap.setAsAllCountedRange(RegionIndex, getPageIndex(FromInRegion), getPageIndex(ToInRegion - 1)); } return true; } template bool markFreeBlocksInRegion(const IntrusiveList &FreeList, DecompactPtrT DecompactPtr, const uptr Base, const uptr RegionIndex, const uptr RegionSize, bool MayContainLastBlockInRegion) { if (!ensurePageMapAllocated()) return false; if (MayContainLastBlockInRegion) { const uptr LastBlockInRegion = ((RegionSize / BlockSize) - 1U) * BlockSize; // The last block in a region may not use the entire page, we mark the // following "pretend" memory block(s) as free in advance. // // Region Boundary // v // -----+-----------------------+ // | Last Page | <- Rounded Region Boundary // -----+-----------------------+ // |-----||- trailing blocks -| // ^ // last block const uptr RoundedRegionSize = roundUp(RegionSize, PageSize); const uptr TrailingBlockBase = LastBlockInRegion + BlockSize; // If the difference between `RoundedRegionSize` and // `TrailingBlockBase` is larger than a page, that implies the reported // `RegionSize` may not be accurate. DCHECK_LT(RoundedRegionSize - TrailingBlockBase, PageSize); // Only the last page touched by the last block needs to mark the trailing // blocks. Note that if the last "pretend" block straddles the boundary, // we still have to count it in so that the logic of counting the number // of blocks on a page is consistent. uptr NumTrailingBlocks = (roundUpSlow(RoundedRegionSize - TrailingBlockBase, BlockSize) + BlockSize - 1) / BlockSize; if (NumTrailingBlocks > 0) { PageMap.incN(RegionIndex, getPageIndex(TrailingBlockBase), NumTrailingBlocks); } } // Iterate over free chunks and count how many free chunks affect each // allocated page. if (BlockSize <= PageSize && PageSize % BlockSize == 0) { // Each chunk affects one page only. for (const auto &It : FreeList) { for (u16 I = 0; I < It.getCount(); I++) { const uptr PInRegion = DecompactPtr(It.get(I)) - Base; DCHECK_LT(PInRegion, RegionSize); PageMap.inc(RegionIndex, getPageIndex(PInRegion)); } } } else { // In all other cases chunks might affect more than one page. DCHECK_GE(RegionSize, BlockSize); for (const auto &It : FreeList) { for (u16 I = 0; I < It.getCount(); I++) { const uptr PInRegion = DecompactPtr(It.get(I)) - Base; PageMap.incRange(RegionIndex, getPageIndex(PInRegion), getPageIndex(PInRegion + BlockSize - 1)); } } } return true; } uptr getPageIndex(uptr P) { return (P >> PageSizeLog) - ReleasePageOffset; } uptr getReleaseOffset() { return ReleasePageOffset << PageSizeLog; } uptr BlockSize; uptr NumberOfRegions; // For partial region marking, some pages in front are not needed to be // counted. uptr ReleasePageOffset; uptr PageSize; uptr PagesCount; uptr PageSizeLog; uptr FullPagesBlockCountMax; bool SameBlockCountPerPage; RegionPageMap PageMap; }; // Try to release the page which doesn't have any in-used block, i.e., they are // all free blocks. The `PageMap` will record the number of free blocks in each // page. template NOINLINE void releaseFreeMemoryToOS(PageReleaseContext &Context, ReleaseRecorderT &Recorder, SkipRegionT SkipRegion) { const uptr PageSize = Context.PageSize; const uptr BlockSize = Context.BlockSize; const uptr PagesCount = Context.PagesCount; const uptr NumberOfRegions = Context.NumberOfRegions; const uptr ReleasePageOffset = Context.ReleasePageOffset; const uptr FullPagesBlockCountMax = Context.FullPagesBlockCountMax; const bool SameBlockCountPerPage = Context.SameBlockCountPerPage; RegionPageMap &PageMap = Context.PageMap; // Iterate over pages detecting ranges of pages with chunk Counters equal // to the expected number of chunks for the particular page. FreePagesRangeTracker RangeTracker(Recorder); if (SameBlockCountPerPage) { // Fast path, every page has the same number of chunks affecting it. for (uptr I = 0; I < NumberOfRegions; I++) { if (SkipRegion(I)) { RangeTracker.skipPages(PagesCount); continue; } for (uptr J = 0; J < PagesCount; J++) { const bool CanRelease = PageMap.updateAsAllCountedIf(I, J, FullPagesBlockCountMax); RangeTracker.processNextPage(CanRelease); } } } else { // Slow path, go through the pages keeping count how many chunks affect // each page. const uptr Pn = BlockSize < PageSize ? PageSize / BlockSize : 1; const uptr Pnc = Pn * BlockSize; // The idea is to increment the current page pointer by the first chunk // size, middle portion size (the portion of the page covered by chunks // except the first and the last one) and then the last chunk size, adding // up the number of chunks on the current page and checking on every step // whether the page boundary was crossed. for (uptr I = 0; I < NumberOfRegions; I++) { if (SkipRegion(I)) { RangeTracker.skipPages(PagesCount); continue; } uptr PrevPageBoundary = 0; uptr CurrentBoundary = 0; if (ReleasePageOffset > 0) { PrevPageBoundary = ReleasePageOffset * PageSize; CurrentBoundary = roundUpSlow(PrevPageBoundary, BlockSize); } for (uptr J = 0; J < PagesCount; J++) { const uptr PageBoundary = PrevPageBoundary + PageSize; uptr BlocksPerPage = Pn; if (CurrentBoundary < PageBoundary) { if (CurrentBoundary > PrevPageBoundary) BlocksPerPage++; CurrentBoundary += Pnc; if (CurrentBoundary < PageBoundary) { BlocksPerPage++; CurrentBoundary += BlockSize; } } PrevPageBoundary = PageBoundary; const bool CanRelease = PageMap.updateAsAllCountedIf(I, J, BlocksPerPage); RangeTracker.processNextPage(CanRelease); } } } RangeTracker.finish(); } } // namespace scudo #endif // SCUDO_RELEASE_H_