pw_{rpc, sync_stl}: Assert if STL mutex is misused

- Update the pw_sync_stl mutex to track whether it is locked and assert
  if it's used improperly. The C++ standard states that unlocking an
  unlocked mutex is undefined behavior. Neither libc++ nor libstdc++
  crash when a mutex is unlocked multiple times (at least on Linux).
- Annotate the stub pw::rpc::internal::LockGuard class with thread
  safety annotations so that thread safety analysis applies when
  compiling without RPC locking.

Change-Id: Ibcd77e90aedafaa5aa05016e9657f2b6983104a2
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/77504
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_rpc/public/pw_rpc/internal/lock.h b/pw_rpc/public/pw_rpc/internal/lock.h
index 617eeae..efb0443 100644
--- a/pw_rpc/public/pw_rpc/internal/lock.h
+++ b/pw_rpc/public/pw_rpc/internal/lock.h
@@ -11,7 +11,6 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations under
 // the License.
-
 #pragma once
 
 #include "pw_rpc/internal/config.h"
@@ -40,9 +39,12 @@
   constexpr void unlock() PW_UNLOCK_FUNCTION() {}
 };
 
-class LockGuard {
+class PW_SCOPED_LOCKABLE LockGuard {
  public:
-  constexpr LockGuard(RpcLock&) {}
+  constexpr LockGuard([[maybe_unused]] RpcLock& mutex)
+      PW_EXCLUSIVE_LOCK_FUNCTION(mutex) {}
+
+  ~LockGuard() PW_UNLOCK_FUNCTION() = default;
 };
 
 #endif  // PW_RPC_USE_GLOBAL_MUTEX
diff --git a/pw_sync/public/pw_sync/mutex.h b/pw_sync/public/pw_sync/mutex.h
index 622e40f..eefbed6 100644
--- a/pw_sync/public/pw_sync/mutex.h
+++ b/pw_sync/public/pw_sync/mutex.h
@@ -69,6 +69,13 @@
 
   native_handle_type native_handle();
 
+ protected:
+  // Expose the NativeMutex directly to derived classes (TimedMutex) in
+  // case implementations use different types for backend::NativeMutex and
+  // native_handle().
+  backend::NativeMutex& native_type() { return native_type_; }
+  const backend::NativeMutex& native_type() const { return native_type_; }
+
  private:
   // This may be a wrapper around a native type with additional members.
   backend::NativeMutex native_type_;
diff --git a/pw_sync_stl/BUILD.bazel b/pw_sync_stl/BUILD.bazel
index 2d5113b..bb1a247 100644
--- a/pw_sync_stl/BUILD.bazel
+++ b/pw_sync_stl/BUILD.bazel
@@ -106,9 +106,11 @@
 
 pw_cc_library(
     name = "mutex",
+    srcs = ["mutex.cc"],
     target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
     deps = [
         ":mutex_headers",
+        "//pw_assert",
         "//pw_sync:mutex_facade",
     ],
 )
diff --git a/pw_sync_stl/BUILD.gn b/pw_sync_stl/BUILD.gn
index 0b7e56b..f3f07c2 100644
--- a/pw_sync_stl/BUILD.gn
+++ b/pw_sync_stl/BUILD.gn
@@ -91,6 +91,9 @@
     "public_overrides/pw_sync_backend/mutex_native.h",
   ]
   public_deps = [ "$dir_pw_sync:mutex.facade" ]
+  deps = [ dir_pw_assert ]
+
+  sources = [ "mutex.cc" ]
 }
 
 # This target provides the backend for pw::sync::TimedMutex.
diff --git a/pw_sync_stl/CMakeLists.txt b/pw_sync_stl/CMakeLists.txt
index bf8d343..b50bb38 100644
--- a/pw_sync_stl/CMakeLists.txt
+++ b/pw_sync_stl/CMakeLists.txt
@@ -17,4 +17,8 @@
 pw_add_module_library(pw_sync_stl.mutex_backend
   IMPLEMENTS_FACADES
     pw_sync.mutex
+  PUBLIC_DEPS
+    pw_assert
+  SOURCES
+    mutex.cc
 )
diff --git a/pw_sync_stl/mutex.cc b/pw_sync_stl/mutex.cc
new file mode 100644
index 0000000..177dc06
--- /dev/null
+++ b/pw_sync_stl/mutex.cc
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_sync/mutex.h"
+
+#include "pw_assert/check.h"
+#include "pw_sync_stl/mutex_native.h"
+
+namespace pw::sync {
+
+Mutex::~Mutex() {
+  PW_CHECK(!native_type_.locked, "Mutex was locked when it went out of scope");
+}
+
+namespace backend {
+
+void NativeMutex::SetLockedState(bool new_state) {
+  PW_CHECK_UINT_NE(locked,
+                   new_state,
+                   "Called %slock(), but the mutex is already in that state",
+                   new_state ? "" : "un");
+  locked = new_state;
+}
+
+}  // namespace backend
+}  // namespace pw::sync
diff --git a/pw_sync_stl/public/pw_sync_stl/mutex_inline.h b/pw_sync_stl/public/pw_sync_stl/mutex_inline.h
index 88548c4..005d089 100644
--- a/pw_sync_stl/public/pw_sync_stl/mutex_inline.h
+++ b/pw_sync_stl/public/pw_sync_stl/mutex_inline.h
@@ -19,14 +19,27 @@
 
 inline Mutex::Mutex() : native_type_() {}
 
-inline Mutex::~Mutex() {}
+inline void Mutex::lock() {
+  native_handle().lock();
+  native_type_.SetLockedState(true);
+}
 
-inline void Mutex::lock() { native_type_.lock(); }
+inline bool Mutex::try_lock() {
+  if (native_handle().try_lock()) {
+    native_type_.SetLockedState(true);
+    return true;
+  }
+  return false;
+}
 
-inline bool Mutex::try_lock() { return native_type_.try_lock(); }
+inline void Mutex::unlock() {
+  native_type_.SetLockedState(false);
+  native_handle().unlock();
+}
 
-inline void Mutex::unlock() { native_type_.unlock(); }
-
-inline Mutex::native_handle_type Mutex::native_handle() { return native_type_; }
+// Return a std::timed_mutex instead of the customized NativeMutex class.
+inline Mutex::native_handle_type Mutex::native_handle() {
+  return native_type_.mutex;
+}
 
 }  // namespace pw::sync
diff --git a/pw_sync_stl/public/pw_sync_stl/mutex_native.h b/pw_sync_stl/public/pw_sync_stl/mutex_native.h
index fb04d56..c90f44e 100644
--- a/pw_sync_stl/public/pw_sync_stl/mutex_native.h
+++ b/pw_sync_stl/public/pw_sync_stl/mutex_native.h
@@ -17,7 +17,19 @@
 
 namespace pw::sync::backend {
 
-using NativeMutex = std::timed_mutex;
+// The NativeMutex class adds a flag that tracks whether the std::timed_mutex is
+// locked. The C++ standard states that misusing a mutex is undefined behavior,
+// so library implementations may simply ignore misuse. This ensures misuse hits
+// a PW_CHECK.
+struct NativeMutex {
+  // The locked state is tracked in a variable. This function asserts if the
+  // state is inconsistent (e.g. unlocking an unlocked mutex).
+  void SetLockedState(bool new_state);
+
+  std::timed_mutex mutex;
+  bool locked = false;
+};
+
 using NativeMutexHandle = std::timed_mutex&;
 
 }  // namespace pw::sync::backend
diff --git a/pw_sync_stl/public/pw_sync_stl/timed_mutex_inline.h b/pw_sync_stl/public/pw_sync_stl/timed_mutex_inline.h
index 2fa904a..6ea86eb 100644
--- a/pw_sync_stl/public/pw_sync_stl/timed_mutex_inline.h
+++ b/pw_sync_stl/public/pw_sync_stl/timed_mutex_inline.h
@@ -19,12 +19,20 @@
 namespace pw::sync {
 
 inline bool TimedMutex::try_lock_for(chrono::SystemClock::duration timeout) {
-  return native_handle().try_lock_for(timeout);
+  if (native_handle().try_lock_for(timeout)) {
+    native_type().SetLockedState(true);
+    return true;
+  }
+  return false;
 }
 
 inline bool TimedMutex::try_lock_until(
     chrono::SystemClock::time_point deadline) {
-  return native_handle().try_lock_until(deadline);
+  if (native_handle().try_lock_until(deadline)) {
+    native_type().SetLockedState(true);
+    return true;
+  }
+  return false;
 }
 
 }  // namespace pw::sync