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