Index: base/metrics/persistent_memory_allocator.cc |
diff --git a/base/metrics/persistent_memory_allocator.cc b/base/metrics/persistent_memory_allocator.cc |
index 1d5df333d04e68d0e9ccacb819a8a1afa79985fd..6edbc2fee74d7660fe8eb5b19ad53c5321c650f9 100644 |
--- a/base/metrics/persistent_memory_allocator.cc |
+++ b/base/metrics/persistent_memory_allocator.cc |
@@ -45,12 +45,12 @@ enum : int { |
}; |
bool CheckFlag(const volatile std::atomic<uint32_t>* flags, int flag) { |
- uint32_t loaded_flags = flags->load(); |
+ uint32_t loaded_flags = flags->load(std::memory_order_relaxed); |
return (loaded_flags & flag) != 0; |
} |
void SetFlag(volatile std::atomic<uint32_t>* flags, int flag) { |
- uint32_t loaded_flags = flags->load(); |
+ uint32_t loaded_flags = flags->load(std::memory_order_relaxed); |
for (;;) { |
uint32_t new_flags = (loaded_flags & ~flag) | flag; |
// In the failue case, actual "flags" value stored in loaded_flags. |
@@ -119,6 +119,115 @@ const PersistentMemoryAllocator::Reference |
const PersistentMemoryAllocator::Reference |
PersistentMemoryAllocator::kReferenceNull = 0; |
+PersistentMemoryAllocator::Iterator::Iterator( |
+ const PersistentMemoryAllocator* allocator) |
+ : allocator_(allocator), last_record_(kReferenceQueue), record_count_(0) {} |
+ |
+PersistentMemoryAllocator::Iterator::Iterator( |
+ const PersistentMemoryAllocator* allocator, |
+ Reference starting_after) |
+ : allocator_(allocator), last_record_(starting_after), record_count_(0) { |
+ // Ensure that the starting point is a valid, iterable block (meaning it can |
+ // be read and has a non-zero "next" pointer). |
+ const volatile BlockHeader* block = |
+ allocator_->GetBlock(starting_after, 0, 0, false, false); |
+ if (!block || block->next.load(std::memory_order_relaxed) == 0) { |
+ NOTREACHED(); |
+ last_record_.store(kReferenceQueue, std::memory_order_release); |
+ } |
+} |
+ |
+PersistentMemoryAllocator::Reference |
+PersistentMemoryAllocator::Iterator::GetNext(uint32_t* type_return) { |
+ // Make a copy of the existing count of found-records, acquiring all changes |
+ // made to the allocator, notably "freeptr" (see comment in loop for why |
+ // the load of that value cannot be moved above here) that occurred during |
+ // any previous runs of this method, including those by parallel threads |
+ // that interrupted it. It pairs with the Release at the end of this method. |
+ // |
+ // Otherwise, if the compiler were to arrange the two loads such that |
+ // "count" was fetched _after_ "freeptr" then it would be possible for |
+ // this thread to be interrupted between them and other threads perform |
+ // multiple allocations, make-iterables, and iterations (with the included |
+ // increment of |record_count_|) culminating in the check at the bottom |
+ // mistakenly determining that a loop exists. Isn't this stuff fun? |
+ uint32_t count = record_count_.load(std::memory_order_acquire); |
+ |
+ Reference last = last_record_.load(std::memory_order_acquire); |
+ Reference next; |
+ while (true) { |
+ const volatile BlockHeader* block = |
+ allocator_->GetBlock(last, 0, 0, true, false); |
+ if (!block) // Invalid iterator state. |
+ return kReferenceNull; |
+ |
+ // The compiler and CPU can freely reorder all memory accesses on which |
+ // there are no dependencies. It could, for example, move the load of |
+ // "freeptr" to above this point because there are no explicit dependencies |
+ // between it and "next". If it did, however, then another block could |
+ // be queued after that but before the following load meaning there is |
+ // one more queued block than the future "detect loop by having more |
+ // blocks that could fit before freeptr" will allow. |
+ // |
+ // By "acquiring" the "next" value here, it's synchronized to the enqueue |
+ // of the node which in turn is synchronized to the allocation (which sets |
+ // freeptr). Thus, the scenario above cannot happen. |
+ next = block->next.load(std::memory_order_acquire); |
+ if (next == kReferenceQueue) // No next allocation in queue. |
+ return kReferenceNull; |
+ block = allocator_->GetBlock(next, 0, 0, false, false); |
+ if (!block) { // Memory is corrupt. |
+ allocator_->SetCorrupt(); |
+ return kReferenceNull; |
+ } |
+ |
+ // Update the "last_record" pointer to be the reference being returned. |
+ // If it fails then another thread has already iterated past it so loop |
+ // again. Failing will also load the existing value into "last" so there |
+ // is no need to do another such load when the while-loop restarts. A |
+ // "strong" compare-exchange is used because failing unnecessarily would |
+ // mean repeating some fairly costly validations above. |
+ if (last_record_.compare_exchange_strong(last, next)) { |
+ *type_return = block->type_id; |
+ break; |
+ } |
+ } |
+ |
+ // Memory corruption could cause a loop in the list. Such must be detected |
+ // so as to not cause an infinite loop in the caller. This is done by simply |
+ // making sure it doesn't iterate more times than the absolute maximum |
+ // number of allocations that could have been made. Callers are likely |
+ // to loop multiple times before it is detected but at least it stops. |
+ const uint32_t freeptr = std::min( |
+ allocator_->shared_meta()->freeptr.load(std::memory_order_relaxed), |
+ allocator_->mem_size_); |
+ const uint32_t max_records = |
+ freeptr / (sizeof(BlockHeader) + kAllocAlignment); |
+ if (count > max_records) { |
+ allocator_->SetCorrupt(); |
+ return kReferenceNull; |
+ } |
+ |
+ // Increment the count and release the changes made above. It pairs with |
+ // the Acquire at the top of this method. Note that this operation is not |
+ // strictly synchonized with fetching of the object to return, which would |
+ // have to be done inside the loop and is somewhat complicated to achieve. |
+ // It does not matter if it falls behind temporarily so long as it never |
+ // gets ahead. |
+ record_count_.fetch_add(1, std::memory_order_release); |
+ return next; |
+} |
+ |
+PersistentMemoryAllocator::Reference |
+PersistentMemoryAllocator::Iterator::GetNextOfType(uint32_t type_match) { |
+ Reference ref; |
+ uint32_t type_found; |
+ while ((ref = GetNext(&type_found)) != 0) { |
+ if (type_found == type_match) |
+ return ref; |
+ } |
+ return kReferenceNull; |
+} |
// static |
bool PersistentMemoryAllocator::IsMemoryAcceptable(const void* base, |
@@ -177,13 +286,13 @@ PersistentMemoryAllocator::PersistentMemoryAllocator( |
if (shared_meta()->cookie != 0 || |
shared_meta()->size != 0 || |
shared_meta()->version != 0 || |
- shared_meta()->freeptr.load() != 0 || |
- shared_meta()->flags.load() != 0 || |
+ shared_meta()->freeptr.load(std::memory_order_relaxed) != 0 || |
+ shared_meta()->flags.load(std::memory_order_relaxed) != 0 || |
shared_meta()->id != 0 || |
shared_meta()->name != 0 || |
shared_meta()->tailptr != 0 || |
shared_meta()->queue.cookie != 0 || |
- shared_meta()->queue.next.load() != 0 || |
+ shared_meta()->queue.next.load(std::memory_order_relaxed) != 0 || |
first_block->size != 0 || |
first_block->cookie != 0 || |
first_block->type_id != 0 || |
@@ -199,13 +308,14 @@ PersistentMemoryAllocator::PersistentMemoryAllocator( |
shared_meta()->page_size = mem_page_; |
shared_meta()->version = kGlobalVersion; |
shared_meta()->id = id; |
- shared_meta()->freeptr.store(sizeof(SharedMetadata)); |
+ shared_meta()->freeptr.store(sizeof(SharedMetadata), |
+ std::memory_order_release); |
// Set up the queue of iterable allocations. |
shared_meta()->queue.size = sizeof(BlockHeader); |
shared_meta()->queue.cookie = kBlockCookieQueue; |
- shared_meta()->queue.next.store(kReferenceQueue); |
- shared_meta()->tailptr.store(kReferenceQueue); |
+ shared_meta()->queue.next.store(kReferenceQueue, std::memory_order_release); |
+ shared_meta()->tailptr.store(kReferenceQueue, std::memory_order_release); |
// Allocate space for the name so other processes can learn it. |
if (!name.empty()) { |
@@ -218,10 +328,10 @@ PersistentMemoryAllocator::PersistentMemoryAllocator( |
} else { |
if (shared_meta()->size == 0 || |
shared_meta()->version == 0 || |
- shared_meta()->freeptr.load() == 0 || |
+ shared_meta()->freeptr.load(std::memory_order_relaxed) == 0 || |
shared_meta()->tailptr == 0 || |
shared_meta()->queue.cookie == 0 || |
- shared_meta()->queue.next.load() == 0) { |
+ shared_meta()->queue.next.load(std::memory_order_relaxed) == 0) { |
SetCorrupt(); |
} |
if (!readonly) { |
@@ -281,7 +391,8 @@ void PersistentMemoryAllocator::CreateTrackingHistograms( |
} |
size_t PersistentMemoryAllocator::used() const { |
- return std::min(shared_meta()->freeptr.load(), mem_size_); |
+ return std::min(shared_meta()->freeptr.load(std::memory_order_relaxed), |
+ mem_size_); |
} |
size_t PersistentMemoryAllocator::GetAllocSize(Reference ref) const { |
@@ -354,7 +465,8 @@ PersistentMemoryAllocator::Reference PersistentMemoryAllocator::AllocateImpl( |
// the code below but recognize that any failed compare-exchange operation |
// involving it will cause it to be loaded with a more recent value. The |
// code should either exit or restart the loop in that case. |
- /* const */ uint32_t freeptr = shared_meta()->freeptr.load(); |
+ /* const */ uint32_t freeptr = |
+ shared_meta()->freeptr.load(std::memory_order_acquire); |
// Allocation is lockless so we do all our caculation and then, if saving |
// indicates a change has occurred since we started, scrap everything and |
@@ -424,7 +536,7 @@ PersistentMemoryAllocator::Reference PersistentMemoryAllocator::AllocateImpl( |
if (block->size != 0 || |
block->cookie != kBlockCookieFree || |
block->type_id != 0 || |
- block->next.load() != 0) { |
+ block->next.load(std::memory_order_relaxed) != 0) { |
SetCorrupt(); |
return kReferenceNull; |
} |
@@ -437,8 +549,9 @@ PersistentMemoryAllocator::Reference PersistentMemoryAllocator::AllocateImpl( |
} |
void PersistentMemoryAllocator::GetMemoryInfo(MemoryInfo* meminfo) const { |
- uint32_t remaining = std::max(mem_size_ - shared_meta()->freeptr.load(), |
- (uint32_t)sizeof(BlockHeader)); |
+ uint32_t remaining = std::max( |
+ mem_size_ - shared_meta()->freeptr.load(std::memory_order_relaxed), |
+ (uint32_t)sizeof(BlockHeader)); |
meminfo->total = mem_size_; |
meminfo->free = IsCorrupt() ? 0 : remaining - sizeof(BlockHeader); |
} |
@@ -501,68 +614,6 @@ void PersistentMemoryAllocator::MakeIterable(Reference ref) { |
} |
} |
-void PersistentMemoryAllocator::CreateIterator(Iterator* state, |
- Reference starting_after) const { |
- if (starting_after) { |
- // Ensure that the starting point is a valid, iterable block. |
- const volatile BlockHeader* block = |
- GetBlock(starting_after, 0, 0, false, false); |
- if (!block || !block->next.load()) { |
- NOTREACHED(); |
- starting_after = kReferenceQueue; |
- } |
- } else { |
- // A zero beginning is really the Queue reference. |
- starting_after = kReferenceQueue; |
- } |
- |
- state->last = starting_after; |
- state->niter = 0; |
-} |
- |
-PersistentMemoryAllocator::Reference PersistentMemoryAllocator::GetNextIterable( |
- Iterator* state, |
- uint32_t* type_id) const { |
- const volatile BlockHeader* block = GetBlock(state->last, 0, 0, true, false); |
- if (!block) // invalid iterator state |
- return kReferenceNull; |
- |
- // The compiler and CPU can freely reorder all memory accesses on which |
- // there are no dependencies. It could, for example, move the load of |
- // "freeptr" above this point because there are no explicit dependencies |
- // between it and "next". If it did, however, then another block could |
- // be queued after that but before the following load meaning there is |
- // one more queued block than the future "detect loop by having more |
- // blocks that could fit before freeptr" will allow. |
- // |
- // By "acquiring" the "next" value here, it's synchronized to the enqueue |
- // of the node which in turn is synchronized to the allocation (which sets |
- // freeptr). Thus, the scenario above cannot happen. |
- uint32_t next = block->next.load(std::memory_order_acquire); |
- block = GetBlock(next, 0, 0, false, false); |
- if (!block) // no next allocation in queue |
- return kReferenceNull; |
- |
- // Memory corruption could cause a loop in the list. We need to detect |
- // that so as to not cause an infinite loop in the caller. We do this |
- // simply by making sure we don't iterate more than the absolute maximum |
- // number of allocations that could have been made. Callers are likely |
- // to loop multiple times before it is detected but at least it stops. |
- uint32_t freeptr = std::min( |
- shared_meta()->freeptr.load(std::memory_order_acquire), |
- mem_size_); |
- if (state->niter > freeptr / (sizeof(BlockHeader) + kAllocAlignment)) { |
- SetCorrupt(); |
- return kReferenceNull; |
- } |
- |
- state->last = next; |
- state->niter++; |
- *type_id = block->type_id; |
- |
- return next; |
-} |
- |
// The "corrupted" state is held both locally and globally (shared). The |
// shared flag can't be trusted since a malicious actor could overwrite it. |
// Because corruption can be detected during read-only operations such as |
@@ -571,7 +622,8 @@ PersistentMemoryAllocator::Reference PersistentMemoryAllocator::GetNextIterable( |
// maybe even the shared flag if the underlying data isn't actually read-only. |
void PersistentMemoryAllocator::SetCorrupt() const { |
LOG(ERROR) << "Corruption detected in shared-memory segment."; |
- const_cast<std::atomic<bool>*>(&corrupt_)->store(true); |
+ const_cast<std::atomic<bool>*>(&corrupt_)->store(true, |
+ std::memory_order_relaxed); |
if (!readonly_) { |
SetFlag(const_cast<volatile std::atomic<uint32_t>*>(&shared_meta()->flags), |
kFlagCorrupt); |
@@ -579,7 +631,8 @@ void PersistentMemoryAllocator::SetCorrupt() const { |
} |
bool PersistentMemoryAllocator::IsCorrupt() const { |
- if (corrupt_.load() || CheckFlag(&shared_meta()->flags, kFlagCorrupt)) { |
+ if (corrupt_.load(std::memory_order_relaxed) || |
+ CheckFlag(&shared_meta()->flags, kFlagCorrupt)) { |
SetCorrupt(); // Make sure all indicators are set. |
return true; |
} |
@@ -610,13 +663,16 @@ PersistentMemoryAllocator::GetBlock(Reference ref, uint32_t type_id, |
// Validation of referenced block-header. |
if (!free_ok) { |
- uint32_t freeptr = shared_meta()->freeptr.load(); |
+ uint32_t freeptr = std::min( |
+ shared_meta()->freeptr.load(std::memory_order_relaxed), mem_size_); |
if (ref + size > freeptr) |
return nullptr; |
const volatile BlockHeader* const block = |
reinterpret_cast<volatile BlockHeader*>(mem_base_ + ref); |
if (block->size < size) |
return nullptr; |
+ if (ref + block->size > freeptr) |
+ return nullptr; |
if (ref != kReferenceQueue && block->cookie != kBlockCookieAllocated) |
return nullptr; |
if (type_id != 0 && block->type_id != type_id) |