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