[vm/concurrency] Refactor SafepointRwLock to ensure unlocking never causes safepoint transition.
The previous implementation of SafepointRwLock has an issue (discovered
by Slava, see bug) with the following code pattern:
{
SafepointReadRwLocker locker(...);
return Foo(); // <-- Returns raw ptr.
}
After the return expression has been evaluated to a raw pointer
the destructor might enter safepoint and cause GC, which leaves
the raw pointer unchanged (since it's not in a handle).
This CL refactors the implementation to ensure we don't perform any
state transition in the destructor, therefore eliminate the
possibility of GC moving the object we return.
Issue https://github.com/dart-lang/sdk/issues/44998
TEST=Relying on existing SafepointRwLock.
Change-Id: Ib7a62b36838edd4b39ad67a8c58f048aa05aa144
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/185062
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
diff --git a/runtime/vm/lockers.cc b/runtime/vm/lockers.cc
index cb04006..30e6ef8 100644
--- a/runtime/vm/lockers.cc
+++ b/runtime/vm/lockers.cc
@@ -86,4 +86,120 @@
}
}
+#if defined(DEBUG)
+bool SafepointRwLock::IsCurrentThreadReader() {
+ ThreadId id = OSThread::GetCurrentThreadId();
+ if (IsCurrentThreadWriter()) {
+ return true;
+ }
+ MonitorLocker ml(&monitor_);
+ for (intptr_t i = readers_ids_.length() - 1; i >= 0; i--) {
+ if (readers_ids_.At(i) == id) {
+ return true;
+ }
+ }
+ return false;
+}
+#endif // defined(DEBUG)
+
+bool SafepointRwLock::EnterRead() {
+ // No need to safepoint if the current thread is not attached.
+ auto thread = Thread::Current();
+ const bool can_block_without_safepoint = thread == nullptr;
+
+ bool acquired_read_lock = false;
+ if (!TryEnterRead(can_block_without_safepoint, &acquired_read_lock)) {
+ // Important: must never hold monitor_ when blocking for safepoint.
+ TransitionVMToBlocked transition(thread);
+ const bool ok = TryEnterRead(/*can_block=*/true, &acquired_read_lock);
+ RELEASE_ASSERT(ok);
+ RELEASE_ASSERT(acquired_read_lock);
+ }
+ return acquired_read_lock;
+}
+
+bool SafepointRwLock::TryEnterRead(bool can_block, bool* acquired_read_lock) {
+ MonitorLocker ml(&monitor_);
+ if (IsCurrentThreadWriter()) {
+ *acquired_read_lock = false;
+ return true;
+ }
+ if (can_block) {
+ while (state_ < 0) {
+ ml.Wait();
+ }
+ }
+ if (state_ >= 0) {
+ ++state_;
+ DEBUG_ONLY(readers_ids_.Add(OSThread::GetCurrentThreadId()));
+ *acquired_read_lock = true;
+ return true;
+ }
+ return false;
+}
+
+void SafepointRwLock::LeaveRead() {
+ MonitorLocker ml(&monitor_);
+ ASSERT(state_ > 0);
+#if defined(DEBUG)
+ {
+ intptr_t i = readers_ids_.length() - 1;
+ ThreadId id = OSThread::GetCurrentThreadId();
+ while (i >= 0) {
+ if (readers_ids_.At(i) == id) {
+ readers_ids_.RemoveAt(i);
+ break;
+ }
+ i--;
+ }
+ ASSERT(i >= 0);
+ }
+#endif
+ if (--state_ == 0) {
+ ml.NotifyAll();
+ }
+}
+
+void SafepointRwLock::EnterWrite() {
+ // No need to safepoint if the current thread is not attached.
+ auto thread = Thread::Current();
+ const bool can_block_without_safepoint = thread == nullptr;
+
+ if (!TryEnterWrite(can_block_without_safepoint)) {
+ // Important: must never hold monitor_ when blocking for safepoint.
+ TransitionVMToBlocked transition(thread);
+ const bool ok = TryEnterWrite(/*can_block=*/true);
+ RELEASE_ASSERT(ok);
+ }
+}
+
+bool SafepointRwLock::TryEnterWrite(bool can_block) {
+ MonitorLocker ml(&monitor_);
+ if (IsCurrentThreadWriter()) {
+ state_--;
+ return true;
+ }
+ if (can_block) {
+ while (state_ != 0) {
+ ml.Wait();
+ }
+ }
+ if (state_ == 0) {
+ writer_id_ = OSThread::GetCurrentThreadId();
+ state_ = -1;
+ return true;
+ }
+ return false;
+}
+
+void SafepointRwLock::LeaveWrite() {
+ MonitorLocker ml(&monitor_);
+ ASSERT(state_ < 0);
+ if (++state_ < 0) {
+ return;
+ }
+ writer_id_ = OSThread::kInvalidThreadId;
+ ml.NotifyAll();
+}
+
} // namespace dart
diff --git a/runtime/vm/lockers.h b/runtime/vm/lockers.h
index d5b2576..7213808 100644
--- a/runtime/vm/lockers.h
+++ b/runtime/vm/lockers.h
@@ -322,21 +322,7 @@
SafepointRwLock() {}
~SafepointRwLock() {}
-#if defined(DEBUG)
- bool IsCurrentThreadReader() {
- ThreadId id = OSThread::GetCurrentThreadId();
- if (IsCurrentThreadWriter()) {
- return true;
- }
- MutexLocker ml(&reader_ids_mutex_);
- for (intptr_t i = readers_ids_.length() - 1; i >= 0; i--) {
- if (readers_ids_.At(i) == id) {
- return true;
- }
- }
- return false;
- }
-#endif // defined(DEBUG)
+ DEBUG_ONLY(bool IsCurrentThreadReader());
bool IsCurrentThreadWriter() {
return writer_id_ == OSThread::GetCurrentThreadId();
@@ -346,82 +332,33 @@
friend class SafepointReadRwLocker;
friend class SafepointWriteRwLocker;
- // returns [true] if read lock was acuired,
+ // returns [true] if read lock was acquired,
// returns [false] if the thread didn't have to acquire read lock due
// to the thread already holding write lock
- bool EnterRead() {
- SafepointMonitorLocker ml(&monitor_);
- if (IsCurrentThreadWriter()) {
- return false;
- }
- while (state_ < 0) {
- ml.Wait();
- }
-#if defined(DEBUG)
- {
- MutexLocker ml(&reader_ids_mutex_);
- readers_ids_.Add(OSThread::GetCurrentThreadId());
- }
-#endif
- ++state_;
- return true;
- }
- void LeaveRead() {
- SafepointMonitorLocker ml(&monitor_);
- ASSERT(state_ > 0);
-#if defined(DEBUG)
- {
- MutexLocker ml(&reader_ids_mutex_);
- intptr_t i = readers_ids_.length() - 1;
- ThreadId id = OSThread::GetCurrentThreadId();
- while (i >= 0) {
- if (readers_ids_.At(i) == id) {
- readers_ids_.RemoveAt(i);
- break;
- }
- i--;
- }
- ASSERT(i >= 0);
- }
-#endif
- if (--state_ == 0) {
- ml.NotifyAll();
- }
- }
+ bool EnterRead();
+ bool TryEnterRead(bool can_block, bool* acquired_read_lock);
+ void LeaveRead();
- void EnterWrite() {
- SafepointMonitorLocker ml(&monitor_);
- if (IsCurrentThreadWriter()) {
- state_--;
- return;
- }
- while (state_ != 0) {
- ml.Wait();
- }
- writer_id_ = OSThread::GetCurrentThreadId();
- state_ = -1;
- }
- void LeaveWrite() {
- SafepointMonitorLocker ml(&monitor_);
- ASSERT(state_ < 0);
- state_++;
- if (state_ < 0) {
- return;
- }
- writer_id_ = OSThread::kInvalidThreadId;
- ml.NotifyAll();
- }
+ void EnterWrite();
+ bool TryEnterWrite(bool can_block);
+ void LeaveWrite();
+ // We maintain an invariant that this monitor is never locked for long periods
+ // of time: Any thread that acquired this monitor must always be able to do
+ // it's work and release it (or wait on the monitor which will also release
+ // it).
+ //
+ // In particular we must ensure the monitor is never held and then a potential
+ // safepoint operation is triggered, since another thread could try to acquire
+ // the lock and it would deadlock.
Monitor monitor_;
+
// [state_] > 0 : The lock is held by multiple readers.
// [state_] == 0 : The lock is free (no readers/writers).
// [state_] < 0 : The lock is held by a single writer (possibly nested).
intptr_t state_ = 0;
-#if defined(DEBUG)
- Mutex reader_ids_mutex_;
- MallocGrowableArray<ThreadId> readers_ids_;
-#endif
+ DEBUG_ONLY(MallocGrowableArray<ThreadId> readers_ids_);
ThreadId writer_id_ = OSThread::kInvalidThreadId;
};
diff --git a/runtime/vm/os_thread.h b/runtime/vm/os_thread.h
index 675836c1..edd66f5 100644
--- a/runtime/vm/os_thread.h
+++ b/runtime/vm/os_thread.h
@@ -377,6 +377,7 @@
friend class MonitorLocker;
friend class SafepointMonitorLocker;
+ friend class SafepointRwLock;
friend void Dart_TestMonitor();
DISALLOW_COPY_AND_ASSIGN(Monitor);
};