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