1 | //===-- secondary_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 "memtag.h" |
10 | #include "tests/scudo_unit_test.h" |
11 | |
12 | #include "allocator_config.h" |
13 | #include "allocator_config_wrapper.h" |
14 | #include "secondary.h" |
15 | |
16 | #include <algorithm> |
17 | #include <condition_variable> |
18 | #include <memory> |
19 | #include <mutex> |
20 | #include <random> |
21 | #include <stdio.h> |
22 | #include <thread> |
23 | #include <vector> |
24 | |
25 | template <typename Config> static scudo::Options getOptionsForConfig() { |
26 | if (!Config::getMaySupportMemoryTagging() || |
27 | !scudo::archSupportsMemoryTagging() || |
28 | !scudo::systemSupportsMemoryTagging()) |
29 | return {}; |
30 | scudo::AtomicOptions AO; |
31 | AO.set(scudo::OptionBit::UseMemoryTagging); |
32 | return AO.load(); |
33 | } |
34 | |
35 | template <typename Config> static void testSecondaryBasic(void) { |
36 | using SecondaryT = scudo::MapAllocator<scudo::SecondaryConfig<Config>>; |
37 | scudo::Options Options = |
38 | getOptionsForConfig<scudo::SecondaryConfig<Config>>(); |
39 | |
40 | scudo::GlobalStats S; |
41 | S.init(); |
42 | std::unique_ptr<SecondaryT> L(new SecondaryT); |
43 | L->init(&S); |
44 | const scudo::uptr Size = 1U << 16; |
45 | void *P = L->allocate(Options, Size); |
46 | EXPECT_NE(P, nullptr); |
47 | memset(s: P, c: 'A', n: Size); |
48 | EXPECT_GE(SecondaryT::getBlockSize(P), Size); |
49 | L->deallocate(Options, P); |
50 | |
51 | // If the Secondary can't cache that pointer, it will be unmapped. |
52 | if (!L->canCache(Size)) { |
53 | EXPECT_DEATH( |
54 | { |
55 | // Repeat few time to avoid missing crash if it's mmaped by unrelated |
56 | // code. |
57 | for (int i = 0; i < 10; ++i) { |
58 | P = L->allocate(Options, Size); |
59 | L->deallocate(Options, P); |
60 | memset(P, 'A', Size); |
61 | } |
62 | }, |
63 | "" ); |
64 | } |
65 | |
66 | const scudo::uptr Align = 1U << 16; |
67 | P = L->allocate(Options, Size + Align, Align); |
68 | EXPECT_NE(P, nullptr); |
69 | void *AlignedP = reinterpret_cast<void *>( |
70 | scudo::roundUp(X: reinterpret_cast<scudo::uptr>(P), Boundary: Align)); |
71 | memset(s: AlignedP, c: 'A', n: Size); |
72 | L->deallocate(Options, P); |
73 | |
74 | std::vector<void *> V; |
75 | for (scudo::uptr I = 0; I < 32U; I++) |
76 | V.push_back(L->allocate(Options, Size)); |
77 | std::shuffle(V.begin(), V.end(), std::mt19937(std::random_device()())); |
78 | while (!V.empty()) { |
79 | L->deallocate(Options, V.back()); |
80 | V.pop_back(); |
81 | } |
82 | scudo::ScopedString Str; |
83 | L->getStats(&Str); |
84 | Str.output(); |
85 | L->unmapTestOnly(); |
86 | } |
87 | |
88 | struct NoCacheConfig { |
89 | static const bool MaySupportMemoryTagging = false; |
90 | template <typename> using TSDRegistryT = void; |
91 | template <typename> using PrimaryT = void; |
92 | template <typename Config> using SecondaryT = scudo::MapAllocator<Config>; |
93 | |
94 | struct Secondary { |
95 | template <typename Config> |
96 | using CacheT = scudo::MapAllocatorNoCache<Config>; |
97 | }; |
98 | }; |
99 | |
100 | struct TestConfig { |
101 | static const bool MaySupportMemoryTagging = false; |
102 | template <typename> using TSDRegistryT = void; |
103 | template <typename> using PrimaryT = void; |
104 | template <typename> using SecondaryT = void; |
105 | |
106 | struct Secondary { |
107 | struct Cache { |
108 | static const scudo::u32 EntriesArraySize = 128U; |
109 | static const scudo::u32 QuarantineSize = 0U; |
110 | static const scudo::u32 DefaultMaxEntriesCount = 64U; |
111 | static const scudo::uptr DefaultMaxEntrySize = 1UL << 20; |
112 | static const scudo::s32 MinReleaseToOsIntervalMs = INT32_MIN; |
113 | static const scudo::s32 MaxReleaseToOsIntervalMs = INT32_MAX; |
114 | }; |
115 | |
116 | template <typename Config> using CacheT = scudo::MapAllocatorCache<Config>; |
117 | }; |
118 | }; |
119 | |
120 | TEST(ScudoSecondaryTest, SecondaryBasic) { |
121 | testSecondaryBasic<NoCacheConfig>(); |
122 | testSecondaryBasic<scudo::DefaultConfig>(); |
123 | testSecondaryBasic<TestConfig>(); |
124 | } |
125 | |
126 | struct MapAllocatorTest : public Test { |
127 | using Config = scudo::DefaultConfig; |
128 | using LargeAllocator = scudo::MapAllocator<scudo::SecondaryConfig<Config>>; |
129 | |
130 | void SetUp() override { Allocator->init(nullptr); } |
131 | |
132 | void TearDown() override { Allocator->unmapTestOnly(); } |
133 | |
134 | std::unique_ptr<LargeAllocator> Allocator = |
135 | std::make_unique<LargeAllocator>(); |
136 | scudo::Options Options = |
137 | getOptionsForConfig<scudo::SecondaryConfig<Config>>(); |
138 | }; |
139 | |
140 | // This exercises a variety of combinations of size and alignment for the |
141 | // MapAllocator. The size computation done here mimic the ones done by the |
142 | // combined allocator. |
143 | TEST_F(MapAllocatorTest, SecondaryCombinations) { |
144 | constexpr scudo::uptr MinAlign = FIRST_32_SECOND_64(8, 16); |
145 | constexpr scudo::uptr = scudo::roundUp(X: 8, Boundary: MinAlign); |
146 | for (scudo::uptr SizeLog = 0; SizeLog <= 20; SizeLog++) { |
147 | for (scudo::uptr AlignLog = FIRST_32_SECOND_64(3, 4); AlignLog <= 16; |
148 | AlignLog++) { |
149 | const scudo::uptr Align = 1U << AlignLog; |
150 | for (scudo::sptr Delta = -128; Delta <= 128; Delta += 8) { |
151 | if ((1LL << SizeLog) + Delta <= 0) |
152 | continue; |
153 | const scudo::uptr UserSize = scudo::roundUp( |
154 | X: static_cast<scudo::uptr>((1LL << SizeLog) + Delta), Boundary: MinAlign); |
155 | const scudo::uptr Size = |
156 | HeaderSize + UserSize + (Align > MinAlign ? Align - HeaderSize : 0); |
157 | void *P = Allocator->allocate(Options, Size, Align); |
158 | EXPECT_NE(P, nullptr); |
159 | void *AlignedP = reinterpret_cast<void *>( |
160 | scudo::roundUp(X: reinterpret_cast<scudo::uptr>(P), Boundary: Align)); |
161 | memset(s: AlignedP, c: 0xff, n: UserSize); |
162 | Allocator->deallocate(Options, P); |
163 | } |
164 | } |
165 | } |
166 | scudo::ScopedString Str; |
167 | Allocator->getStats(&Str); |
168 | Str.output(); |
169 | } |
170 | |
171 | TEST_F(MapAllocatorTest, SecondaryIterate) { |
172 | std::vector<void *> V; |
173 | const scudo::uptr PageSize = scudo::getPageSizeCached(); |
174 | for (scudo::uptr I = 0; I < 32U; I++) |
175 | V.push_back(Allocator->allocate( |
176 | Options, (static_cast<scudo::uptr>(std::rand()) % 16U) * PageSize)); |
177 | auto Lambda = [&V](scudo::uptr Block) { |
178 | EXPECT_NE(std::find(V.begin(), V.end(), reinterpret_cast<void *>(Block)), |
179 | V.end()); |
180 | }; |
181 | Allocator->disable(); |
182 | Allocator->iterateOverBlocks(Lambda); |
183 | Allocator->enable(); |
184 | while (!V.empty()) { |
185 | Allocator->deallocate(Options, V.back()); |
186 | V.pop_back(); |
187 | } |
188 | scudo::ScopedString Str; |
189 | Allocator->getStats(&Str); |
190 | Str.output(); |
191 | } |
192 | |
193 | TEST_F(MapAllocatorTest, SecondaryOptions) { |
194 | // Attempt to set a maximum number of entries higher than the array size. |
195 | EXPECT_FALSE( |
196 | Allocator->setOption(scudo::Option::MaxCacheEntriesCount, 4096U)); |
197 | // A negative number will be cast to a scudo::u32, and fail. |
198 | EXPECT_FALSE(Allocator->setOption(scudo::Option::MaxCacheEntriesCount, -1)); |
199 | if (Allocator->canCache(0U)) { |
200 | // Various valid combinations. |
201 | EXPECT_TRUE(Allocator->setOption(scudo::Option::MaxCacheEntriesCount, 4U)); |
202 | EXPECT_TRUE( |
203 | Allocator->setOption(scudo::Option::MaxCacheEntrySize, 1UL << 20)); |
204 | EXPECT_TRUE(Allocator->canCache(1UL << 18)); |
205 | EXPECT_TRUE( |
206 | Allocator->setOption(scudo::Option::MaxCacheEntrySize, 1UL << 17)); |
207 | EXPECT_FALSE(Allocator->canCache(1UL << 18)); |
208 | EXPECT_TRUE(Allocator->canCache(1UL << 16)); |
209 | EXPECT_TRUE(Allocator->setOption(scudo::Option::MaxCacheEntriesCount, 0U)); |
210 | EXPECT_FALSE(Allocator->canCache(1UL << 16)); |
211 | EXPECT_TRUE(Allocator->setOption(scudo::Option::MaxCacheEntriesCount, 4U)); |
212 | EXPECT_TRUE( |
213 | Allocator->setOption(scudo::Option::MaxCacheEntrySize, 1UL << 20)); |
214 | EXPECT_TRUE(Allocator->canCache(1UL << 16)); |
215 | } |
216 | } |
217 | |
218 | struct MapAllocatorWithReleaseTest : public MapAllocatorTest { |
219 | void SetUp() override { Allocator->init(nullptr, /*ReleaseToOsInterval=*/0); } |
220 | |
221 | void performAllocations() { |
222 | std::vector<void *> V; |
223 | const scudo::uptr PageSize = scudo::getPageSizeCached(); |
224 | { |
225 | std::unique_lock<std::mutex> Lock(Mutex); |
226 | while (!Ready) |
227 | Cv.wait(Lock); |
228 | } |
229 | for (scudo::uptr I = 0; I < 128U; I++) { |
230 | // Deallocate 75% of the blocks. |
231 | const bool Deallocate = (std::rand() & 3) != 0; |
232 | void *P = Allocator->allocate( |
233 | Options, (static_cast<scudo::uptr>(std::rand()) % 16U) * PageSize); |
234 | if (Deallocate) |
235 | Allocator->deallocate(Options, P); |
236 | else |
237 | V.push_back(P); |
238 | } |
239 | while (!V.empty()) { |
240 | Allocator->deallocate(Options, V.back()); |
241 | V.pop_back(); |
242 | } |
243 | } |
244 | |
245 | std::mutex Mutex; |
246 | std::condition_variable Cv; |
247 | bool Ready = false; |
248 | }; |
249 | |
250 | TEST_F(MapAllocatorWithReleaseTest, SecondaryThreadsRace) { |
251 | std::thread Threads[16]; |
252 | for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++) |
253 | Threads[I] = |
254 | std::thread(&MapAllocatorWithReleaseTest::performAllocations, this); |
255 | { |
256 | std::unique_lock<std::mutex> Lock(Mutex); |
257 | Ready = true; |
258 | Cv.notify_all(); |
259 | } |
260 | for (auto &T : Threads) |
261 | T.join(); |
262 | scudo::ScopedString Str; |
263 | Allocator->getStats(&Str); |
264 | Str.output(); |
265 | } |
266 | |