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, 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.
TEST(ScudoPrimaryTest,Primary64OOM)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
testIteratePrimary()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
TEST(ScudoPrimaryTest,PrimaryIterate)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;
153
performAllocations(Primary * Allocator)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
testPrimaryThreaded()178 template <typename Primary> static void testPrimaryThreaded() {
179 Ready = false;
180 auto Deleter = [](Primary *P) {
181 P->unmapTestOnly();
182 delete P;
183 };
184 std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
185 Allocator->init(/*ReleaseToOsInterval=*/-1);
186 std::thread Threads[32];
187 for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++)
188 Threads[I] = std::thread(performAllocations<Primary>, Allocator.get());
189 {
190 std::unique_lock<std::mutex> Lock(Mutex);
191 Ready = true;
192 Cv.notify_all();
193 }
194 for (auto &T : Threads)
195 T.join();
196 Allocator->releaseToOS();
197 scudo::ScopedString Str(1024);
198 Allocator->getStats(&Str);
199 Str.output();
200 }
201
TEST(ScudoPrimaryTest,PrimaryThreaded)202 TEST(ScudoPrimaryTest, PrimaryThreaded) {
203 using SizeClassMap = scudo::SvelteSizeClassMap;
204 #if !SCUDO_FUCHSIA
205 testPrimaryThreaded<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
206 #endif
207 testPrimaryThreaded<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
208 testPrimaryThreaded<scudo::SizeClassAllocator64<SizeClassMap, 24U, true>>();
209 }
210
211 // Through a simple allocation that spans two pages, verify that releaseToOS
212 // actually releases some bytes (at least one page worth). This is a regression
213 // test for an error in how the release criteria were computed.
testReleaseToOS()214 template <typename Primary> static void testReleaseToOS() {
215 auto Deleter = [](Primary *P) {
216 P->unmapTestOnly();
217 delete P;
218 };
219 std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
220 Allocator->init(/*ReleaseToOsInterval=*/-1);
221 typename Primary::CacheT Cache;
222 Cache.init(nullptr, Allocator.get());
223 const scudo::uptr Size = scudo::getPageSizeCached() * 2;
224 EXPECT_TRUE(Primary::canAllocate(Size));
225 const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
226 void *P = Cache.allocate(ClassId);
227 EXPECT_NE(P, nullptr);
228 Cache.deallocate(ClassId, P);
229 Cache.destroy(nullptr);
230 EXPECT_GT(Allocator->releaseToOS(), 0U);
231 }
232
TEST(ScudoPrimaryTest,ReleaseToOS)233 TEST(ScudoPrimaryTest, ReleaseToOS) {
234 using SizeClassMap = scudo::DefaultSizeClassMap;
235 #if !SCUDO_FUCHSIA
236 testReleaseToOS<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
237 #endif
238 testReleaseToOS<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
239 testReleaseToOS<scudo::SizeClassAllocator64<SizeClassMap, 24U, true>>();
240 }
241