Assert that SharedMemoryMapping is only used with lock-free atomics

It's possible to bypass this check by passing a struct with std::atomic
members, but better than no check at all.

Bug: 357945779
Change-Id: I627461015865a55792ec11d9fefc3b3fe16ec7d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5766776
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Commit-Queue: Joe Mason <joenotcharles@google.com>
Cr-Commit-Position: refs/heads/main@{#1341016}
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 8e82634..0ce2724 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -4209,6 +4209,7 @@
       "logging_nocompile.nc",
       "memory/protected_memory_nocompile.nc",
       "memory/ref_counted_nocompile.nc",
+      "memory/shared_memory_mapping_nocompile.nc",
       "memory/weak_ptr_nocompile.nc",
       "metrics/field_trial_params_nocompile.nc",
       "metrics/histogram_nocompile.nc",
diff --git a/base/memory/shared_memory_mapping.h b/base/memory/shared_memory_mapping.h
index c268af8..66e7279 100644
--- a/base/memory/shared_memory_mapping.h
+++ b/base/memory/shared_memory_mapping.h
@@ -5,6 +5,7 @@
 #ifndef BASE_MEMORY_SHARED_MEMORY_MAPPING_H_
 #define BASE_MEMORY_SHARED_MEMORY_MAPPING_H_
 
+#include <atomic>
 #include <cstddef>
 #include <type_traits>
 
@@ -18,7 +19,30 @@
 namespace base {
 
 namespace subtle {
+
 class PlatformSharedMemoryRegion;
+
+// Constraints on types that are safe to copy across memory spaces.
+
+template <typename T>
+struct LockFreeIfAtomic {
+  // Non-atomics aren't synchronized, so trivially don't contain locks.
+  static constexpr bool is_lock_free = true;
+};
+
+template <typename T>
+struct LockFreeIfAtomic<std::atomic<T>> {
+  static constexpr bool is_lock_free = std::atomic<T>::is_always_lock_free;
+};
+
+template <typename T>
+concept SharedMemorySafe =
+    // Copying non-trivially-copyable object across memory spaces is dangerous.
+    std::is_trivially_copyable_v<T> &&
+    // If T is a std::atomic, it's unsafe to share across memory spaces unless
+    // it's lock-free.
+    LockFreeIfAtomic<T>::is_lock_free;
+
 }  // namespace subtle
 
 // Base class for scoped handles to a shared memory mapping created from a
@@ -123,10 +147,8 @@
   // Returns a pointer to a page-aligned const T if the mapping is valid and
   // large enough to contain a T, or nullptr otherwise.
   template <typename T>
+    requires subtle::SharedMemorySafe<T>
   const T* GetMemoryAs() const {
-    static_assert(std::is_trivially_copyable_v<T>,
-                  "Copying non-trivially-copyable object across memory spaces "
-                  "is dangerous");
     if (!IsValid())
       return nullptr;
     if (sizeof(T) > size())
@@ -141,10 +163,8 @@
   // will be returned. The first element, if any, is guaranteed to be
   // page-aligned.
   template <typename T>
+    requires subtle::SharedMemorySafe<T>
   span<const T> GetMemoryAsSpan() const {
-    static_assert(std::is_trivially_copyable_v<T>,
-                  "Copying non-trivially-copyable object across memory spaces "
-                  "is dangerous");
     if (!IsValid())
       return span<const T>();
     size_t count = size() / sizeof(T);
@@ -155,10 +175,8 @@
   // large enough to contain |count| elements, or an empty span otherwise. The
   // first element, if any, is guaranteed to be page-aligned.
   template <typename T>
+    requires subtle::SharedMemorySafe<T>
   span<const T> GetMemoryAsSpan(size_t count) const {
-    static_assert(std::is_trivially_copyable_v<T>,
-                  "Copying non-trivially-copyable object across memory spaces "
-                  "is dangerous");
     if (!IsValid())
       return span<const T>();
     if (size() / sizeof(T) < count)
@@ -213,10 +231,8 @@
   // Returns a pointer to a page-aligned T if the mapping is valid and large
   // enough to contain a T, or nullptr otherwise.
   template <typename T>
+    requires subtle::SharedMemorySafe<T>
   T* GetMemoryAs() const {
-    static_assert(std::is_trivially_copyable_v<T>,
-                  "Copying non-trivially-copyable object across memory spaces "
-                  "is dangerous");
     if (!IsValid())
       return nullptr;
     if (sizeof(T) > size())
@@ -230,10 +246,8 @@
   // enough to contain even one T: in that case, an empty span will be returned.
   // The first element, if any, is guaranteed to be page-aligned.
   template <typename T>
+    requires subtle::SharedMemorySafe<T>
   span<T> GetMemoryAsSpan() const {
-    static_assert(std::is_trivially_copyable_v<T>,
-                  "Copying non-trivially-copyable object across memory spaces "
-                  "is dangerous");
     if (!IsValid())
       return span<T>();
     size_t count = size() / sizeof(T);
@@ -244,10 +258,8 @@
   // enough to contain |count| elements, or an empty span otherwise. The first
   // element, if any, is guaranteed to be page-aligned.
   template <typename T>
+    requires subtle::SharedMemorySafe<T>
   span<T> GetMemoryAsSpan(size_t count) const {
-    static_assert(std::is_trivially_copyable_v<T>,
-                  "Copying non-trivially-copyable object across memory spaces "
-                  "is dangerous");
     if (!IsValid())
       return span<T>();
     if (size() / sizeof(T) < count)
diff --git a/base/memory/shared_memory_mapping_nocompile.nc b/base/memory/shared_memory_mapping_nocompile.nc
new file mode 100644
index 0000000..7034b384
--- /dev/null
+++ b/base/memory/shared_memory_mapping_nocompile.nc
@@ -0,0 +1,62 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This is a "No Compile Test" suite.
+// https://dev.chromium.org/developers/testing/no-compile-tests
+
+#include <string>
+#include <type_traits>
+#include <utility>
+
+#include "base/memory/read_only_shared_memory_region.h"
+#include "base/memory/shared_memory_mapping.h"
+
+namespace base {
+
+namespace {
+
+struct NotTriviallyCopyable {
+  std::string data;
+};
+
+static_assert(!std::is_trivially_copyable_v<NotTriviallyCopyable>);
+
+struct NotLockFree {
+  int large_array[1024] = {};
+};
+
+using NotLockFreeAtomic = std::atomic<NotLockFree>;
+
+static_assert(std::is_trivially_copyable_v<NotLockFreeAtomic>);
+static_assert(!NotLockFreeAtomic::is_always_lock_free);
+
+}  // namespace
+
+void RequireTriviallyCopyable() {
+  auto mapped_region =
+      ReadOnlySharedMemoryRegion::Create(sizeof(NotTriviallyCopyable));
+  WritableSharedMemoryMapping write_map = std::move(mapped_region.mapping);
+  write_map.GetMemoryAs<NotTriviallyCopyable>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAs'}}
+  write_map.GetMemoryAsSpan<NotTriviallyCopyable>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+  write_map.GetMemoryAsSpan<NotTriviallyCopyable>(1);  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+  ReadOnlySharedMemoryMapping read_map = mapped_region.region.Map();
+  read_map.GetMemoryAs<NotTriviallyCopyable>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAs'}}
+  read_map.GetMemoryAsSpan<NotTriviallyCopyable>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+  read_map.GetMemoryAsSpan<NotTriviallyCopyable>(1);  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+}
+
+void RequireLockFreeAtomic() {
+  auto mapped_region =
+      ReadOnlySharedMemoryRegion::Create(sizeof(NotLockFreeAtomic));
+  WritableSharedMemoryMapping write_map = std::move(mapped_region.mapping);
+  write_map.GetMemoryAs<NotLockFreeAtomic>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAs'}}
+  write_map.GetMemoryAsSpan<NotLockFreeAtomic>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+  write_map.GetMemoryAsSpan<NotLockFreeAtomic>(1);  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+  ReadOnlySharedMemoryMapping read_map = mapped_region.region.Map();
+  read_map.GetMemoryAs<NotLockFreeAtomic>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAs'}}
+  read_map.GetMemoryAsSpan<NotLockFreeAtomic>();  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+  read_map.GetMemoryAsSpan<NotLockFreeAtomic>(1);  // expected-error@base/memory/shared_memory_mapping_nocompile.nc:* {{no matching member function for call to 'GetMemoryAsSpan'}}
+}
+
+}  // namespace base
diff --git a/base/memory/shared_memory_mapping_unittest.cc b/base/memory/shared_memory_mapping_unittest.cc
index 24f49ee6..31dd519 100644
--- a/base/memory/shared_memory_mapping_unittest.cc
+++ b/base/memory/shared_memory_mapping_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <stdint.h>
 
+#include <atomic>
 #include <limits>
 
 #include "base/containers/span.h"
@@ -45,10 +46,10 @@
   CreateMapping(sizeof(uint32_t));
 
   uint32_t* write_ptr = write_mapping_.GetMemoryAs<uint32_t>();
-  EXPECT_NE(nullptr, write_ptr);
+  ASSERT_NE(nullptr, write_ptr);
 
   const uint32_t* read_ptr = read_mapping_.GetMemoryAs<uint32_t>();
-  EXPECT_NE(nullptr, read_ptr);
+  ASSERT_NE(nullptr, read_ptr);
 
   *write_ptr = 0u;
   EXPECT_EQ(0u, *read_ptr);
@@ -146,6 +147,33 @@
                   .empty());
 }
 
+TEST_F(SharedMemoryMappingTest, Atomic) {
+  CreateMapping(sizeof(std::atomic<uint32_t>));
+
+  auto* write_ptr = write_mapping_.GetMemoryAs<std::atomic<uint32_t>>();
+  ASSERT_NE(nullptr, write_ptr);
+
+  // Placement new to initialize the std::atomic in place.
+  new (write_ptr) std::atomic<uint32_t>;
+
+  const auto* read_ptr = read_mapping_.GetMemoryAs<std::atomic<uint32_t>>();
+  ASSERT_NE(nullptr, read_ptr);
+
+  write_ptr->store(0u, std::memory_order_relaxed);
+  EXPECT_EQ(0u, read_ptr->load(std::memory_order_relaxed));
+
+  write_ptr->store(0x12345678u, std::memory_order_relaxed);
+  EXPECT_EQ(0x12345678u, read_ptr->load(std::memory_order_relaxed));
+}
+
+TEST_F(SharedMemoryMappingTest, TooBigAtomic) {
+  CreateMapping(sizeof(std::atomic<uint8_t>));
+
+  EXPECT_EQ(nullptr, write_mapping_.GetMemoryAs<std::atomic<uint32_t>>());
+
+  EXPECT_EQ(nullptr, read_mapping_.GetMemoryAs<std::atomic<uint32_t>>());
+}
+
 // TODO(dcheng): This test is temporarily disabled on iOS. iOS devices allow
 // the creation of a 1GB shared memory region, but don't allow the region to be
 // mapped.
diff --git a/content/browser/renderer_host/render_process_host_impl.cc b/content/browser/renderer_host/render_process_host_impl.cc
index e29449c..2b558ed 100644
--- a/content/browser/renderer_host/render_process_host_impl.cc
+++ b/content/browser/renderer_host/render_process_host_impl.cc
@@ -1802,10 +1802,6 @@
   // the same host (e.g. from AgentSchedulingGroupHost::RenderProcessExited()).
   // In that case, we only need to resend the read-only memory region.
   if (!last_foreground_time_region_.IsValid()) {
-    static_assert(
-        std::atomic<base::TimeTicks>::is_always_lock_free,
-        "Atomically sharing TimeTicks across processes might be unsafe");
-
     last_foreground_time_region_ = base::ReadOnlySharedMemoryRegion::Create(
         sizeof(std::atomic<base::TimeTicks>));
     CHECK(last_foreground_time_region_.IsValid());
diff --git a/mojo/public/cpp/base/shared_memory_version.h b/mojo/public/cpp/base/shared_memory_version.h
index ed44fedf..b4b0678 100644
--- a/mojo/public/cpp/base/shared_memory_version.h
+++ b/mojo/public/cpp/base/shared_memory_version.h
@@ -18,8 +18,6 @@
 
 using VersionType = uint64_t;
 using SharedVersionType = std::atomic<VersionType>;
-static_assert(SharedVersionType::is_always_lock_free,
-              "Usage of SharedVersionType across processes might be unsafe");
 
 // This file contains classes to share a version between processes through
 // shared memory. A version is a nonzero monotonically increasing integer. A
diff --git a/services/network/restricted_cookie_manager.h b/services/network/restricted_cookie_manager.h
index 9ddbf342..64e11e4 100644
--- a/services/network/restricted_cookie_manager.h
+++ b/services/network/restricted_cookie_manager.h
@@ -188,8 +188,6 @@
 
  private:
   using SharedVersionType = std::atomic<uint64_t>;
-  static_assert(SharedVersionType::is_always_lock_free,
-                "Usage of SharedVersionType across processes might be unsafe");
 
   // Function to be called when an event is known to potentially invalidate
   // cookies the other side could have cached.