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