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