1 //===-- tsd_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 "tsd_exclusive.h"
12 #include "tsd_shared.h"
13 
14 #include <condition_variable>
15 #include <mutex>
16 #include <set>
17 #include <thread>
18 
19 // We mock out an allocator with a TSD registry, mostly using empty stubs. The
20 // cache contains a single volatile uptr, to be able to test that several
21 // concurrent threads will not access or modify the same cache at the same time.
22 template <class Config> class MockAllocator {
23 public:
24   using ThisT = MockAllocator<Config>;
25   using TSDRegistryT = typename Config::template TSDRegistryT<ThisT>;
26   using CacheT = struct MockCache { volatile scudo::uptr Canary; };
27   using QuarantineCacheT = struct MockQuarantine {};
28 
29   void init() {
30     // This should only be called once by the registry.
31     EXPECT_FALSE(Initialized);
32     Initialized = true;
33   }
34 
35   void unmapTestOnly() { TSDRegistry.unmapTestOnly(this); }
36   void initCache(CacheT *Cache) { *Cache = {}; }
37   void commitBack(scudo::TSD<MockAllocator> *TSD) {}
38   TSDRegistryT *getTSDRegistry() { return &TSDRegistry; }
39   void callPostInitCallback() {}
40 
41   bool isInitialized() { return Initialized; }
42 
43 private:
44   bool Initialized = false;
45   TSDRegistryT TSDRegistry;
46 };
47 
48 struct OneCache {
49   template <class Allocator>
50   using TSDRegistryT = scudo::TSDRegistrySharedT<Allocator, 1U, 1U>;
51 };
52 
53 struct SharedCaches {
54   template <class Allocator>
55   using TSDRegistryT = scudo::TSDRegistrySharedT<Allocator, 16U, 8U>;
56 };
57 
58 struct ExclusiveCaches {
59   template <class Allocator>
60   using TSDRegistryT = scudo::TSDRegistryExT<Allocator>;
61 };
62 
63 TEST(ScudoTSDTest, TSDRegistryInit) {
64   using AllocatorT = MockAllocator<OneCache>;
65   auto Deleter = [](AllocatorT *A) {
66     A->unmapTestOnly();
67     delete A;
68   };
69   std::unique_ptr<AllocatorT, decltype(Deleter)> Allocator(new AllocatorT,
70                                                            Deleter);
71   EXPECT_FALSE(Allocator->isInitialized());
72 
73   auto Registry = Allocator->getTSDRegistry();
74   Registry->init(Allocator.get());
75   EXPECT_TRUE(Allocator->isInitialized());
76 }
77 
78 template <class AllocatorT> static void testRegistry() {
79   auto Deleter = [](AllocatorT *A) {
80     A->unmapTestOnly();
81     delete A;
82   };
83   std::unique_ptr<AllocatorT, decltype(Deleter)> Allocator(new AllocatorT,
84                                                            Deleter);
85   EXPECT_FALSE(Allocator->isInitialized());
86 
87   auto Registry = Allocator->getTSDRegistry();
88   Registry->initThreadMaybe(Allocator.get(), /*MinimalInit=*/true);
89   EXPECT_TRUE(Allocator->isInitialized());
90 
91   bool UnlockRequired;
92   auto TSD = Registry->getTSDAndLock(&UnlockRequired);
93   EXPECT_NE(TSD, nullptr);
94   EXPECT_EQ(TSD->Cache.Canary, 0U);
95   if (UnlockRequired)
96     TSD->unlock();
97 
98   Registry->initThreadMaybe(Allocator.get(), /*MinimalInit=*/false);
99   TSD = Registry->getTSDAndLock(&UnlockRequired);
100   EXPECT_NE(TSD, nullptr);
101   EXPECT_EQ(TSD->Cache.Canary, 0U);
102   memset(&TSD->Cache, 0x42, sizeof(TSD->Cache));
103   if (UnlockRequired)
104     TSD->unlock();
105 }
106 
107 TEST(ScudoTSDTest, TSDRegistryBasic) {
108   testRegistry<MockAllocator<OneCache>>();
109   testRegistry<MockAllocator<SharedCaches>>();
110 #if !SCUDO_FUCHSIA
111   testRegistry<MockAllocator<ExclusiveCaches>>();
112 #endif
113 }
114 
115 static std::mutex Mutex;
116 static std::condition_variable Cv;
117 static bool Ready;
118 
119 template <typename AllocatorT> static void stressCache(AllocatorT *Allocator) {
120   auto Registry = Allocator->getTSDRegistry();
121   {
122     std::unique_lock<std::mutex> Lock(Mutex);
123     while (!Ready)
124       Cv.wait(Lock);
125   }
126   Registry->initThreadMaybe(Allocator, /*MinimalInit=*/false);
127   bool UnlockRequired;
128   auto TSD = Registry->getTSDAndLock(&UnlockRequired);
129   EXPECT_NE(TSD, nullptr);
130   // For an exclusive TSD, the cache should be empty. We cannot guarantee the
131   // same for a shared TSD.
132   if (!UnlockRequired)
133     EXPECT_EQ(TSD->Cache.Canary, 0U);
134   // Transform the thread id to a uptr to use it as canary.
135   const scudo::uptr Canary = static_cast<scudo::uptr>(
136       std::hash<std::thread::id>{}(std::this_thread::get_id()));
137   TSD->Cache.Canary = Canary;
138   // Loop a few times to make sure that a concurrent thread isn't modifying it.
139   for (scudo::uptr I = 0; I < 4096U; I++)
140     EXPECT_EQ(TSD->Cache.Canary, Canary);
141   if (UnlockRequired)
142     TSD->unlock();
143 }
144 
145 template <class AllocatorT> static void testRegistryThreaded() {
146   Ready = false;
147   auto Deleter = [](AllocatorT *A) {
148     A->unmapTestOnly();
149     delete A;
150   };
151   std::unique_ptr<AllocatorT, decltype(Deleter)> Allocator(new AllocatorT,
152                                                            Deleter);
153   std::thread Threads[32];
154   for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++)
155     Threads[I] = std::thread(stressCache<AllocatorT>, Allocator.get());
156   {
157     std::unique_lock<std::mutex> Lock(Mutex);
158     Ready = true;
159     Cv.notify_all();
160   }
161   for (auto &T : Threads)
162     T.join();
163 }
164 
165 TEST(ScudoTSDTest, TSDRegistryThreaded) {
166   testRegistryThreaded<MockAllocator<OneCache>>();
167   testRegistryThreaded<MockAllocator<SharedCaches>>();
168 #if !SCUDO_FUCHSIA
169   testRegistryThreaded<MockAllocator<ExclusiveCaches>>();
170 #endif
171 }
172 
173 static std::set<void *> Pointers;
174 
175 static void stressSharedRegistry(MockAllocator<SharedCaches> *Allocator) {
176   std::set<void *> Set;
177   auto Registry = Allocator->getTSDRegistry();
178   {
179     std::unique_lock<std::mutex> Lock(Mutex);
180     while (!Ready)
181       Cv.wait(Lock);
182   }
183   Registry->initThreadMaybe(Allocator, /*MinimalInit=*/false);
184   bool UnlockRequired;
185   for (scudo::uptr I = 0; I < 4096U; I++) {
186     auto TSD = Registry->getTSDAndLock(&UnlockRequired);
187     EXPECT_NE(TSD, nullptr);
188     Set.insert(reinterpret_cast<void *>(TSD));
189     if (UnlockRequired)
190       TSD->unlock();
191   }
192   {
193     std::unique_lock<std::mutex> Lock(Mutex);
194     Pointers.insert(Set.begin(), Set.end());
195   }
196 }
197 
198 TEST(ScudoTSDTest, TSDRegistryTSDsCount) {
199   Ready = false;
200   Pointers.clear();
201   using AllocatorT = MockAllocator<SharedCaches>;
202   auto Deleter = [](AllocatorT *A) {
203     A->unmapTestOnly();
204     delete A;
205   };
206   std::unique_ptr<AllocatorT, decltype(Deleter)> Allocator(new AllocatorT,
207                                                            Deleter);
208   // We attempt to use as many TSDs as the shared cache offers by creating a
209   // decent amount of threads that will be run concurrently and attempt to get
210   // and lock TSDs. We put them all in a set and count the number of entries
211   // after we are done.
212   std::thread Threads[32];
213   for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++)
214     Threads[I] = std::thread(stressSharedRegistry, Allocator.get());
215   {
216     std::unique_lock<std::mutex> Lock(Mutex);
217     Ready = true;
218     Cv.notify_all();
219   }
220   for (auto &T : Threads)
221     T.join();
222   // The initial number of TSDs we get will be the minimum of the default count
223   // and the number of CPUs.
224   EXPECT_LE(Pointers.size(), 8U);
225   Pointers.clear();
226   auto Registry = Allocator->getTSDRegistry();
227   // Increase the number of TSDs to 16.
228   Registry->setOption(scudo::Option::MaxTSDsCount, 16);
229   Ready = false;
230   for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++)
231     Threads[I] = std::thread(stressSharedRegistry, Allocator.get());
232   {
233     std::unique_lock<std::mutex> Lock(Mutex);
234     Ready = true;
235     Cv.notify_all();
236   }
237   for (auto &T : Threads)
238     T.join();
239   // We should get 16 distinct TSDs back.
240   EXPECT_EQ(Pointers.size(), 16U);
241 }
242