1 //===-- release_test.cpp ----------------------------------------*- 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 #include "tests/scudo_unit_test.h"
10 
11 #include "list.h"
12 #include "release.h"
13 #include "size_class_map.h"
14 
15 #include <string.h>
16 
17 #include <algorithm>
18 #include <random>
19 #include <set>
20 
21 TEST(ScudoReleaseTest, RegionPageMap) {
22   for (scudo::uptr I = 0; I < SCUDO_WORDSIZE; I++) {
23     // Various valid counter's max values packed into one word.
24     scudo::RegionPageMap PageMap2N(1U, 1U, 1UL << I);
25     EXPECT_EQ(sizeof(scudo::uptr), PageMap2N.getBufferSize());
26     // Check the "all bit set" values too.
27     scudo::RegionPageMap PageMap2N1_1(1U, 1U, ~0UL >> I);
28     EXPECT_EQ(sizeof(scudo::uptr), PageMap2N1_1.getBufferSize());
29     // Verify the packing ratio, the counter is Expected to be packed into the
30     // closest power of 2 bits.
31     scudo::RegionPageMap PageMap(1U, SCUDO_WORDSIZE, 1UL << I);
32     EXPECT_EQ(sizeof(scudo::uptr) * scudo::roundUpToPowerOfTwo(I + 1),
33               PageMap.getBufferSize());
34   }
35 
36   // Go through 1, 2, 4, 8, .. {32,64} bits per counter.
37   for (scudo::uptr I = 0; (SCUDO_WORDSIZE >> I) != 0; I++) {
38     // Make sure counters request one memory page for the buffer.
39     const scudo::uptr NumCounters =
40         (scudo::getPageSizeCached() / 8) * (SCUDO_WORDSIZE >> I);
41     scudo::RegionPageMap PageMap(1U, NumCounters,
42                                        1UL << ((1UL << I) - 1));
43     PageMap.inc(0U, 0U);
44     for (scudo::uptr C = 1; C < NumCounters - 1; C++) {
45       EXPECT_EQ(0UL, PageMap.get(0U, C));
46       PageMap.inc(0U, C);
47       EXPECT_EQ(1UL, PageMap.get(0U, C - 1));
48     }
49     EXPECT_EQ(0UL, PageMap.get(0U, NumCounters - 1));
50     PageMap.inc(0U, NumCounters - 1);
51     if (I > 0) {
52       PageMap.incRange(0u, 0U, NumCounters - 1);
53       for (scudo::uptr C = 0; C < NumCounters; C++)
54         EXPECT_EQ(2UL, PageMap.get(0U, C));
55     }
56   }
57 }
58 
59 class StringRangeRecorder {
60 public:
61   std::string ReportedPages;
62 
63   StringRangeRecorder()
64       : PageSizeScaledLog(scudo::getLog2(scudo::getPageSizeCached())) {}
65 
66   void releasePageRangeToOS(scudo::uptr From, scudo::uptr To) {
67     From >>= PageSizeScaledLog;
68     To >>= PageSizeScaledLog;
69     EXPECT_LT(From, To);
70     if (!ReportedPages.empty())
71       EXPECT_LT(LastPageReported, From);
72     ReportedPages.append(From - LastPageReported, '.');
73     ReportedPages.append(To - From, 'x');
74     LastPageReported = To;
75   }
76 
77 private:
78   const scudo::uptr PageSizeScaledLog;
79   scudo::uptr LastPageReported = 0;
80 };
81 
82 TEST(ScudoReleaseTest, FreePagesRangeTracker) {
83   // 'x' denotes a page to be released, '.' denotes a page to be kept around.
84   const char *TestCases[] = {
85       "",
86       ".",
87       "x",
88       "........",
89       "xxxxxxxxxxx",
90       "..............xxxxx",
91       "xxxxxxxxxxxxxxxxxx.....",
92       "......xxxxxxxx........",
93       "xxx..........xxxxxxxxxxxxxxx",
94       "......xxxx....xxxx........",
95       "xxx..........xxxxxxxx....xxxxxxx",
96       "x.x.x.x.x.x.x.x.x.x.x.x.",
97       ".x.x.x.x.x.x.x.x.x.x.x.x",
98       ".x.x.x.x.x.x.x.x.x.x.x.x.",
99       "x.x.x.x.x.x.x.x.x.x.x.x.x",
100   };
101   typedef scudo::FreePagesRangeTracker<StringRangeRecorder> RangeTracker;
102 
103   for (auto TestCase : TestCases) {
104     StringRangeRecorder Recorder;
105     RangeTracker Tracker(Recorder);
106     for (scudo::uptr I = 0; TestCase[I] != 0; I++)
107       Tracker.processNextPage(TestCase[I] == 'x');
108     Tracker.finish();
109     // Strip trailing '.'-pages before comparing the results as they are not
110     // going to be reported to range_recorder anyway.
111     const char *LastX = strrchr(TestCase, 'x');
112     std::string Expected(TestCase,
113                          LastX == nullptr ? 0 : (LastX - TestCase + 1));
114     EXPECT_STREQ(Expected.c_str(), Recorder.ReportedPages.c_str());
115   }
116 }
117 
118 class ReleasedPagesRecorder {
119 public:
120   std::set<scudo::uptr> ReportedPages;
121 
122   void releasePageRangeToOS(scudo::uptr From, scudo::uptr To) {
123     const scudo::uptr PageSize = scudo::getPageSizeCached();
124     for (scudo::uptr I = From; I < To; I += PageSize)
125       ReportedPages.insert(I);
126   }
127 
128   scudo::uptr getBase() const { return 0; }
129 };
130 
131 // Simplified version of a TransferBatch.
132 template <class SizeClassMap> struct FreeBatch {
133   static const scudo::u16 MaxCount = SizeClassMap::MaxNumCachedHint;
134   void clear() { Count = 0; }
135   void add(scudo::uptr P) {
136     DCHECK_LT(Count, MaxCount);
137     Batch[Count++] = P;
138   }
139   scudo::u16 getCount() const { return Count; }
140   scudo::uptr get(scudo::u16 I) const {
141     DCHECK_LE(I, Count);
142     return Batch[I];
143   }
144   FreeBatch *Next;
145 
146 private:
147   scudo::uptr Batch[MaxCount];
148   scudo::u16 Count;
149 };
150 
151 template <class SizeClassMap> void testReleaseFreeMemoryToOS() {
152   typedef FreeBatch<SizeClassMap> Batch;
153   const scudo::uptr PagesCount = 1024;
154   const scudo::uptr PageSize = scudo::getPageSizeCached();
155   const scudo::uptr PageSizeLog = scudo::getLog2(PageSize);
156   std::mt19937 R;
157   scudo::u32 RandState = 42;
158 
159   for (scudo::uptr I = 1; I <= SizeClassMap::LargestClassId; I++) {
160     const scudo::uptr BlockSize = SizeClassMap::getSizeByClassId(I);
161     const scudo::uptr MaxBlocks = PagesCount * PageSize / BlockSize;
162 
163     // Generate the random free list.
164     std::vector<scudo::uptr> FreeArray;
165     bool InFreeRange = false;
166     scudo::uptr CurrentRangeEnd = 0;
167     for (scudo::uptr I = 0; I < MaxBlocks; I++) {
168       if (I == CurrentRangeEnd) {
169         InFreeRange = (scudo::getRandomU32(&RandState) & 1U) == 1;
170         CurrentRangeEnd += (scudo::getRandomU32(&RandState) & 0x7f) + 1;
171       }
172       if (InFreeRange)
173         FreeArray.push_back(I * BlockSize);
174     }
175     if (FreeArray.empty())
176       continue;
177     // Shuffle the array to ensure that the order is irrelevant.
178     std::shuffle(FreeArray.begin(), FreeArray.end(), R);
179 
180     // Build the FreeList from the FreeArray.
181     scudo::SinglyLinkedList<Batch> FreeList;
182     FreeList.clear();
183     Batch *CurrentBatch = nullptr;
184     for (auto const &Block : FreeArray) {
185       if (!CurrentBatch) {
186         CurrentBatch = new Batch;
187         CurrentBatch->clear();
188         FreeList.push_back(CurrentBatch);
189       }
190       CurrentBatch->add(Block);
191       if (CurrentBatch->getCount() == Batch::MaxCount)
192         CurrentBatch = nullptr;
193     }
194 
195     // Release the memory.
196     auto SkipRegion = [](UNUSED scudo::uptr RegionIndex) { return false; };
197     auto DecompactPtr = [](scudo::uptr P) { return P; };
198     ReleasedPagesRecorder Recorder;
199     scudo::PageReleaseContext Context(BlockSize,
200                                       /*RegionSize=*/MaxBlocks * BlockSize,
201                                       /*NumberOfRegions=*/1U);
202     ASSERT_FALSE(Context.hasBlockMarked());
203     Context.markFreeBlocks(FreeList, DecompactPtr, Recorder.getBase());
204     ASSERT_TRUE(Context.hasBlockMarked());
205     releaseFreeMemoryToOS(Context, Recorder, SkipRegion);
206     scudo::RegionPageMap &PageMap = Context.PageMap;
207 
208     // Verify that there are no released pages touched by used chunks and all
209     // ranges of free chunks big enough to contain the entire memory pages had
210     // these pages released.
211     scudo::uptr VerifiedReleasedPages = 0;
212     std::set<scudo::uptr> FreeBlocks(FreeArray.begin(), FreeArray.end());
213 
214     scudo::uptr CurrentBlock = 0;
215     InFreeRange = false;
216     scudo::uptr CurrentFreeRangeStart = 0;
217     for (scudo::uptr I = 0; I < MaxBlocks; I++) {
218       const bool IsFreeBlock =
219           FreeBlocks.find(CurrentBlock) != FreeBlocks.end();
220       if (IsFreeBlock) {
221         if (!InFreeRange) {
222           InFreeRange = true;
223           CurrentFreeRangeStart = CurrentBlock;
224         }
225       } else {
226         // Verify that this used chunk does not touch any released page.
227         const scudo::uptr StartPage = CurrentBlock / PageSize;
228         const scudo::uptr EndPage = (CurrentBlock + BlockSize - 1) / PageSize;
229         for (scudo::uptr J = StartPage; J <= EndPage; J++) {
230           const bool PageReleased = Recorder.ReportedPages.find(J * PageSize) !=
231                                     Recorder.ReportedPages.end();
232           EXPECT_EQ(false, PageReleased);
233           EXPECT_EQ(false,
234                     PageMap.isAllCounted(0, (J * PageSize) >> PageSizeLog));
235         }
236 
237         if (InFreeRange) {
238           InFreeRange = false;
239           // Verify that all entire memory pages covered by this range of free
240           // chunks were released.
241           scudo::uptr P = scudo::roundUpTo(CurrentFreeRangeStart, PageSize);
242           while (P + PageSize <= CurrentBlock) {
243             const bool PageReleased =
244                 Recorder.ReportedPages.find(P) != Recorder.ReportedPages.end();
245             EXPECT_EQ(true, PageReleased);
246             EXPECT_EQ(true, PageMap.isAllCounted(0, P >> PageSizeLog));
247             VerifiedReleasedPages++;
248             P += PageSize;
249           }
250         }
251       }
252 
253       CurrentBlock += BlockSize;
254     }
255 
256     if (InFreeRange) {
257       scudo::uptr P = scudo::roundUpTo(CurrentFreeRangeStart, PageSize);
258       const scudo::uptr EndPage =
259           scudo::roundUpTo(MaxBlocks * BlockSize, PageSize);
260       while (P + PageSize <= EndPage) {
261         const bool PageReleased =
262             Recorder.ReportedPages.find(P) != Recorder.ReportedPages.end();
263         EXPECT_EQ(true, PageReleased);
264         EXPECT_EQ(true, PageMap.isAllCounted(0, P >> PageSizeLog));
265         VerifiedReleasedPages++;
266         P += PageSize;
267       }
268     }
269 
270     EXPECT_EQ(Recorder.ReportedPages.size(), VerifiedReleasedPages);
271 
272     while (!FreeList.empty()) {
273       CurrentBatch = FreeList.front();
274       FreeList.pop_front();
275       delete CurrentBatch;
276     }
277   }
278 }
279 
280 TEST(ScudoReleaseTest, ReleaseFreeMemoryToOSDefault) {
281   testReleaseFreeMemoryToOS<scudo::DefaultSizeClassMap>();
282 }
283 
284 TEST(ScudoReleaseTest, ReleaseFreeMemoryToOSAndroid) {
285   testReleaseFreeMemoryToOS<scudo::AndroidSizeClassMap>();
286 }
287 
288 TEST(ScudoReleaseTest, ReleaseFreeMemoryToOSSvelte) {
289   testReleaseFreeMemoryToOS<scudo::SvelteSizeClassMap>();
290 }
291