Changeset 274608 in webkit
- Timestamp:
- Mar 17, 2021 7:40:00 PM (3 years ago)
- Location:
- trunk
- Files:
-
- 1 added
- 4 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/JSTests/ChangeLog
r274552 r274608 1 2021-03-17 Mark Lam <mark.lam@apple.com> 2 3 Fix race condition in ConcurrentPtrHashSet. 4 https://bugs.webkit.org/show_bug.cgi?id=223241 5 rdar://74637896 6 7 Reviewed by Yusuke Suzuki. 8 9 * stress/race-to-add-opaque-roots-in-ConcurrentPtrHashSet.js: Added. 10 1 11 2021-03-16 Ross Kirsling <ross.kirsling@sony.com> 2 12 -
trunk/Source/WTF/ChangeLog
r274603 r274608 1 2021-03-17 Mark Lam <mark.lam@apple.com> 2 3 Fix race condition in ConcurrentPtrHashSet. 4 https://bugs.webkit.org/show_bug.cgi?id=223241 5 rdar://74637896 6 7 Reviewed by Yusuke Suzuki. 8 9 There exists a race condition where ConcurrentPtrHashSet::resizeIfNecessary() may 10 not capture an entry added by ConcurrentPtrHashSet::addSlow() concurrently. 11 12 ConcurrentPtrHashSet::addSlow() currently does the following: 13 14 { 15 if (table->load.exchangeAdd(1) >= table->maxLoad()) // (a1) 16 return resizeAndAdd(ptr); // (a2) 17 18 for (;;) { 19 void* oldEntry = table->array[index].compareExchangeStrong(nullptr, ptr); // (a3) 20 if (!oldEntry) { 21 if (m_table.load() != table) { // (a4) 22 // We added an entry to an old table! We need to reexecute the add on the new table. 23 return add(ptr); // (a5) 24 } 25 return true; // (a6) 26 } 27 if (oldEntry == ptr) 28 return false; 29 30 ... // set index to next entry slot to try. 31 } 32 } 33 34 ConcurrentPtrHashSet::resizeIfNecessary() currently does the following: 35 36 { 37 auto locker = holdLock(m_lock); // (r1) 38 Table* table = m_table.loadRelaxed(); 39 if (table->load.loadRelaxed() < table->maxLoad()) 40 return; 41 42 // (r2) 43 44 std::unique_ptr<Table> newTable = Table::create(table->size * 2); 45 ... 46 for (unsigned i = 0; i < table->size; ++i) { // (r3) 47 void* ptr = table->array[i].loadRelaxed(); 48 if (!ptr) 49 continue; 50 51 ... // copy ptr to newTable. // (r4) 52 } 53 54 ... 55 m_table.store(newTable.get()); // (r5) 56 ... 57 } 58 59 Let's say thread T1 is executing addSlow(), and thread T2 is concurrently executing 60 resizeIfNecessary(). 61 62 Consider the following scenario (in chronological order): 63 1. T2 has arrived at just before (r5) i.e. it is already done copying the entries 64 in the old m_table. 65 2. T1 executes (a3) and writes a new entry into m_table. 66 3. T1 checks that the table hasn't been replaced at (a4), and sees that it has 67 not. 68 4. T1 returns at (a6), thinking that its new entry is committed. 69 5. T2 sets the new m_table at (r5), thereby discarding the new entry that T1 has 70 just written. 71 72 The fix is to set m_table to a newly introduced m_stubTable at (r2). m_stubTable 73 is set up with a size of 0, and load value of 10. This means it is always full. 74 With this, the following scenarios can play out: 75 76 Scenario 1: T2 installs m_stubTable before T1 reaches (a1) 77 78 1. At (a1), T1 sees that m_table (which is m_stubTable) is full. 79 2. T1 calls resizeAndAdd() at (a2), which ends up calling resizeIfNecessary() 80 and blocking on the lock at (r1). 81 82 Scenario 2: T2 installs m_stubTable after T1 reaches just before (a3) 83 84 1. T1 writes the new entry at (a3). 85 2. T1 checks m_table at (a4), and sees that it has changed (now pointing to 86 m_stubTable). 87 3. T1 calls add() again at (a5) to redo the operation, and ends with scenario 1. 88 89 Scenario 3: T2 installs m_stubTable after T1 reaches (a3), but before (a4) 90 91 1. The new entry has already been added, but we don't know if it made the cut off 92 for T2 to copy it or not. But, it doesn't matter because ... 93 2. T1 checks m_table at (a4), and sees that it has changed (now pointing to 94 m_stubTable). 95 3. T1 calls add() again at (a5) to redo the operation, and ends with scenario 1. 96 97 Scenario 4: T2 installs m_stubTable after T1 reaches (a4) 98 99 1. The new entry has already been added. 100 2. T1 checks m_table at (a4), and sees that it has NOT changed (because T2 hasn't 101 installed m_stubTable yet). This means T2's copy loop is guaranteed to not 102 have started yet i.e. the new entry will definitely be picked up by the copy 103 loop. 104 3. T1 returns at (a6), and all is well. 105 106 * wtf/ConcurrentPtrHashSet.cpp: 107 (WTF::ConcurrentPtrHashSet::deleteOldTables): 108 (WTF::ConcurrentPtrHashSet::initialize): 109 (WTF::ConcurrentPtrHashSet::containsImplSlow const): 110 (WTF::ConcurrentPtrHashSet::sizeSlow const): 111 (WTF::ConcurrentPtrHashSet::resizeIfNecessary): 112 (WTF::ConcurrentPtrHashSet::Table::initializeStub): 113 * wtf/ConcurrentPtrHashSet.h: 114 1 115 2021-03-17 Alex Christensen <achristensen@webkit.org> 2 116 -
trunk/Source/WTF/wtf/ConcurrentPtrHashSet.cpp
r261661 r274608 1 1 /* 2 * Copyright (C) 2017 Apple Inc. All rights reserved.2 * Copyright (C) 2017-2021 Apple Inc. All rights reserved. 3 3 * 4 4 * Redistribution and use in source and binary forms, with or without … … 43 43 // some bad crashes if we did make that mistake. 44 44 auto locker = holdLock(m_lock); 45 45 46 ASSERT(m_table.loadRelaxed() != &m_stubTable); 46 47 m_allTables.removeAllMatching( 47 48 [&] (std::unique_ptr<Table>& table) -> bool { … … 66 67 m_table.storeRelaxed(table.get()); 67 68 m_allTables.append(WTFMove(table)); 69 m_stubTable.initializeStub(); 68 70 } 69 71 … … 89 91 } 90 92 93 bool ConcurrentPtrHashSet::containsImplSlow(void* ptr) const 94 { 95 auto locker = holdLock(m_lock); 96 ASSERT(m_table.loadRelaxed() != &m_stubTable); 97 return containsImpl(ptr); 98 } 99 100 size_t ConcurrentPtrHashSet::sizeSlow() const 101 { 102 auto locker = holdLock(m_lock); 103 ASSERT(m_table.loadRelaxed() != &m_stubTable); 104 return size(); 105 } 106 91 107 void ConcurrentPtrHashSet::resizeIfNecessary() 92 108 { 93 109 auto locker = holdLock(m_lock); 94 110 Table* table = m_table.loadRelaxed(); 111 ASSERT(table != &m_stubTable); 95 112 if (table->load.loadRelaxed() < table->maxLoad()) 96 113 return; 97 114 115 // Stubbing out m_table with m_stubTable here is necessary to ensure that 116 // we don't miss copying any entries that may be concurrently be added. 117 // 118 // If addSlow() completes before this stubbing, the new entry is guaranteed 119 // to be copied below. 120 // 121 // If addSlow() completes after this stubbing, addSlow() will check m_table 122 // before it finishes, and detect that its newly added entry may not have 123 // made it in. As a result, it will try to re-add the entry, and end up 124 // blocking on resizeIfNecessary() until the resizing is donw. This is 125 // because m_stubTable will tell addSlow() think that the table is out of 126 // space and it needs to resize. NOTE: m_stubTable always says it is out of 127 // space. 128 m_table.store(&m_stubTable); 129 98 130 std::unique_ptr<Table> newTable = Table::create(table->size * 2); 99 131 unsigned mask = newTable->mask; … … 122 154 123 155 newTable->load.storeRelaxed(load); 124 156 125 157 m_table.store(newTable.get()); 158 159 // addSlow() will always start by exchangeAdd'ing 1 to the current m_table's 160 // load value before checking if it exceeds its max allowed load. For the 161 // real m_table, this is not an issue because at most, it will accummulate 162 // up to N extra adds above max load, where N is the number of threads 163 // concurrrently adding entries. 164 // 165 // However, m_table may be replaced with m_stubTable for each resize 166 // operation. As a result, the cummulative error on its load value 167 // may far exceed N (as specified above). To fix this, we always reset it 168 // here to prevent an overflow. Note: a load of stubDefaultLoadValue means 169 // that m_stubTable is full since its size is 0. 170 // 171 // In practice, this won't matter because we most likely won't do so many 172 // resize operations such that this will get to the point of overflowing. 173 // However, since resizing is not in the fast path, let's just be pedantic 174 // and reset it for correctness. 175 m_stubTable.load.store(Table::stubDefaultLoadValue); 176 126 177 m_allTables.append(WTFMove(newTable)); 127 178 } … … 144 195 } 145 196 197 void ConcurrentPtrHashSet::Table::initializeStub() 198 { 199 // The stub table is set up to look like it is already filled up. This is 200 // so that it can be used during resizing to force all attempts to add to 201 // be routed to resizeAndAdd() where it will block until the resizing is 202 // done. 203 size = 0; 204 mask = 0; 205 load.storeRelaxed(stubDefaultLoadValue); 206 array[0].storeRelaxed(nullptr); 207 } 208 146 209 } // namespace WTF 147 210 -
trunk/Source/WTF/wtf/ConcurrentPtrHashSet.h
r272192 r274608 75 75 size_t size() const 76 76 { 77 return m_table.loadRelaxed()->load.loadRelaxed(); 77 Table* table = m_table.loadRelaxed(); 78 if (table == &m_stubTable) 79 return sizeSlow(); 80 return table->load.loadRelaxed(); 78 81 } 79 82 … … 91 94 92 95 static std::unique_ptr<Table> create(unsigned size); 93 96 void initializeStub(); 97 94 98 unsigned maxLoad() const { return size / 2; } 95 99 100 // This can be any value >= 1 because the stub's size is 0, ensuring that 101 // m_stubTable is always seen as "full". We choose 10 for no reason other 102 // than it gives some warm fuzzies since it is greater than 1. 103 static constexpr unsigned stubDefaultLoadValue = 10; 104 96 105 unsigned size; // This is immutable. 97 106 unsigned mask; // This is immutable. … … 123 132 { 124 133 Table* table = m_table.loadRelaxed(); 134 if (table == &m_stubTable) 135 return containsImplSlow(ptr); 136 125 137 unsigned mask = table->mask; 126 138 unsigned startIndex = hash(ptr) & mask; … … 156 168 157 169 WTF_EXPORT_PRIVATE bool addSlow(Table* table, unsigned mask, unsigned startIndex, unsigned index, void* ptr); 170 WTF_EXPORT_PRIVATE bool containsImplSlow(void* ptr) const; 171 WTF_EXPORT_PRIVATE size_t sizeSlow() const; 158 172 159 173 void resizeIfNecessary(); 160 174 bool resizeAndAdd(void* ptr); 161 175 162 176 Vector<std::unique_ptr<Table>, 4> m_allTables; 163 177 Atomic<Table*> m_table; // This is never null. 164 Lock m_lock; // We just use this to control resize races. 178 Table m_stubTable; 179 mutable Lock m_lock; // We just use this to control resize races. 165 180 }; 166 181
Note: See TracChangeset
for help on using the changeset viewer.