1 //===-- primary_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 "primary32.h"
12 #include "primary64.h"
13 #include "size_class_map.h"
14 
15 #include <condition_variable>
16 #include <mutex>
17 #include <thread>
18 #include <vector>
19 
20 // Note that with small enough regions, the SizeClassAllocator64 also works on
21 // 32-bit architectures. It's not something we want to encourage, but we still
22 // should ensure the tests pass.
23 
24 template <typename Primary> static void testPrimary() {
25   const scudo::uptr NumberOfAllocations = 32U;
26   auto Deleter = [](Primary *P) {
27     P->unmapTestOnly();
28     delete P;
29   };
30   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
31   Allocator->init(/*ReleaseToOsInterval=*/-1);
32   typename Primary::CacheT Cache;
33   Cache.init(nullptr, Allocator.get());
34   for (scudo::uptr I = 0; I <= 16U; I++) {
35     const scudo::uptr Size = 1UL << I;
36     if (!Primary::canAllocate(Size))
37       continue;
38     const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
39     void *Pointers[NumberOfAllocations];
40     for (scudo::uptr J = 0; J < NumberOfAllocations; J++) {
41       void *P = Cache.allocate(ClassId);
42       memset(P, 'B', Size);
43       Pointers[J] = P;
44     }
45     for (scudo::uptr J = 0; J < NumberOfAllocations; J++)
46       Cache.deallocate(ClassId, Pointers[J]);
47   }
48   Cache.destroy(nullptr);
49   Allocator->releaseToOS();
50   scudo::ScopedString Str(1024);
51   Allocator->getStats(&Str);
52   Str.output();
53 }
54 
55 TEST(ScudoPrimaryTest, BasicPrimary) {
56   using SizeClassMap = scudo::DefaultSizeClassMap;
57 #if !SCUDO_FUCHSIA
58   testPrimary<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
59 #endif
60   testPrimary<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
61   testPrimary<scudo::SizeClassAllocator64<SizeClassMap, 24U, true>>();
62 }
63 
64 // The 64-bit SizeClassAllocator can be easily OOM'd with small region sizes.
65 // For the 32-bit one, it requires actually exhausting memory, so we skip it.
66 TEST(ScudoPrimaryTest, Primary64OOM) {
67   using Primary = scudo::SizeClassAllocator64<scudo::DefaultSizeClassMap, 20U>;
68   using TransferBatch = Primary::CacheT::TransferBatch;
69   Primary Allocator;
70   Allocator.init(/*ReleaseToOsInterval=*/-1);
71   typename Primary::CacheT Cache;
72   scudo::GlobalStats Stats;
73   Stats.init();
74   Cache.init(&Stats, &Allocator);
75   bool AllocationFailed = false;
76   std::vector<TransferBatch *> Batches;
77   const scudo::uptr ClassId = Primary::SizeClassMap::LargestClassId;
78   const scudo::uptr Size = Primary::getSizeByClassId(ClassId);
79   for (scudo::uptr I = 0; I < 10000U; I++) {
80     TransferBatch *B = Allocator.popBatch(&Cache, ClassId);
81     if (!B) {
82       AllocationFailed = true;
83       break;
84     }
85     for (scudo::u32 J = 0; J < B->getCount(); J++)
86       memset(B->get(J), 'B', Size);
87     Batches.push_back(B);
88   }
89   while (!Batches.empty()) {
90     Allocator.pushBatch(ClassId, Batches.back());
91     Batches.pop_back();
92   }
93   Cache.destroy(nullptr);
94   Allocator.releaseToOS();
95   scudo::ScopedString Str(1024);
96   Allocator.getStats(&Str);
97   Str.output();
98   EXPECT_EQ(AllocationFailed, true);
99   Allocator.unmapTestOnly();
100 }
101 
102 template <typename Primary> static void testIteratePrimary() {
103   auto Deleter = [](Primary *P) {
104     P->unmapTestOnly();
105     delete P;
106   };
107   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
108   Allocator->init(/*ReleaseToOsInterval=*/-1);
109   typename Primary::CacheT Cache;
110   Cache.init(nullptr, Allocator.get());
111   std::vector<std::pair<scudo::uptr, void *>> V;
112   for (scudo::uptr I = 0; I < 64U; I++) {
113     const scudo::uptr Size = std::rand() % Primary::SizeClassMap::MaxSize;
114     const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
115     void *P = Cache.allocate(ClassId);
116     V.push_back(std::make_pair(ClassId, P));
117   }
118   scudo::uptr Found = 0;
119   auto Lambda = [V, &Found](scudo::uptr Block) {
120     for (const auto &Pair : V) {
121       if (Pair.second == reinterpret_cast<void *>(Block))
122         Found++;
123     }
124   };
125   Allocator->disable();
126   Allocator->iterateOverBlocks(Lambda);
127   Allocator->enable();
128   EXPECT_EQ(Found, V.size());
129   while (!V.empty()) {
130     auto Pair = V.back();
131     Cache.deallocate(Pair.first, Pair.second);
132     V.pop_back();
133   }
134   Cache.destroy(nullptr);
135   Allocator->releaseToOS();
136   scudo::ScopedString Str(1024);
137   Allocator->getStats(&Str);
138   Str.output();
139 }
140 
141 TEST(ScudoPrimaryTest, PrimaryIterate) {
142   using SizeClassMap = scudo::DefaultSizeClassMap;
143 #if !SCUDO_FUCHSIA
144   testIteratePrimary<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
145 #endif
146   testIteratePrimary<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
147   testIteratePrimary<scudo::SizeClassAllocator64<SizeClassMap, 24U, true>>();
148 }
149 
150 static std::mutex Mutex;
151 static std::condition_variable Cv;
152 static bool Ready = false;
153 
154 template <typename Primary> static void performAllocations(Primary *Allocator) {
155   static THREADLOCAL typename Primary::CacheT Cache;
156   Cache.init(nullptr, Allocator);
157   std::vector<std::pair<scudo::uptr, void *>> V;
158   {
159     std::unique_lock<std::mutex> Lock(Mutex);
160     while (!Ready)
161       Cv.wait(Lock);
162   }
163   for (scudo::uptr I = 0; I < 256U; I++) {
164     const scudo::uptr Size = std::rand() % Primary::SizeClassMap::MaxSize / 4;
165     const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
166     void *P = Cache.allocate(ClassId);
167     if (P)
168       V.push_back(std::make_pair(ClassId, P));
169   }
170   while (!V.empty()) {
171     auto Pair = V.back();
172     Cache.deallocate(Pair.first, Pair.second);
173     V.pop_back();
174   }
175   Cache.destroy(nullptr);
176 }
177 
178 template <typename Primary> static void testPrimaryThreaded() {
179   auto Deleter = [](Primary *P) {
180     P->unmapTestOnly();
181     delete P;
182   };
183   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
184   Allocator->init(/*ReleaseToOsInterval=*/-1);
185   std::thread Threads[32];
186   for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++)
187     Threads[I] = std::thread(performAllocations<Primary>, Allocator.get());
188   {
189     std::unique_lock<std::mutex> Lock(Mutex);
190     Ready = true;
191     Cv.notify_all();
192   }
193   for (auto &T : Threads)
194     T.join();
195   Allocator->releaseToOS();
196   scudo::ScopedString Str(1024);
197   Allocator->getStats(&Str);
198   Str.output();
199 }
200 
201 TEST(ScudoPrimaryTest, PrimaryThreaded) {
202   using SizeClassMap = scudo::SvelteSizeClassMap;
203 #if !SCUDO_FUCHSIA
204   testPrimaryThreaded<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
205 #endif
206   testPrimaryThreaded<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
207   testPrimaryThreaded<scudo::SizeClassAllocator64<SizeClassMap, 24U, true>>();
208 }
209 
210 // Through a simple allocation that spans two pages, verify that releaseToOS
211 // actually releases some bytes (at least one page worth). This is a regression
212 // test for an error in how the release criteria were computed.
213 template <typename Primary> static void testReleaseToOS() {
214   auto Deleter = [](Primary *P) {
215     P->unmapTestOnly();
216     delete P;
217   };
218   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
219   Allocator->init(/*ReleaseToOsInterval=*/-1);
220   typename Primary::CacheT Cache;
221   Cache.init(nullptr, Allocator.get());
222   const scudo::uptr Size = scudo::getPageSizeCached() * 2;
223   EXPECT_TRUE(Primary::canAllocate(Size));
224   const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
225   void *P = Cache.allocate(ClassId);
226   EXPECT_NE(P, nullptr);
227   Cache.deallocate(ClassId, P);
228   Cache.destroy(nullptr);
229   EXPECT_GT(Allocator->releaseToOS(), 0U);
230 }
231 
232 TEST(ScudoPrimaryTest, ReleaseToOS) {
233   using SizeClassMap = scudo::DefaultSizeClassMap;
234 #if !SCUDO_FUCHSIA
235   testReleaseToOS<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
236 #endif
237   testReleaseToOS<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
238   testReleaseToOS<scudo::SizeClassAllocator64<SizeClassMap, 24U, true>>();
239 }
240