Reland "ipcz: Ref counted fragments"

This is a reland of commit 6385cfb931f738334dc59e9d7d39eb7daa70f7ba

Original change's description:
> ipcz: Ref counted fragments
>
> Introduces RefCountedFragment and FragmentRef<T> as helpers to support
> ref-counted objects living in shared memory fragments, as allocated
> via NodeLinkMemory.
>
> Also introduces some builtin BlockAllocators to each NodeLinkMemory's
> primary buffer.
>
> This change lays the ground work for dynamic allocation of managed state
> objects between each connected pair of Routers.
>
> Bug: 1299283
> Change-Id: Ibc2859a8cdcca00fd0d9602664eceaaccb5bd9ae
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3750056
> Reviewed-by: Alex Gough <ajgo@chromium.org>
> Commit-Queue: Ken Rockot <rockot@google.com>
> Cr-Commit-Position: refs/heads/main@{#1021940}

Bug: 1299283
Change-Id: I5d43afc6547a6abc333884e0ab352221b944e5b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3752528
Reviewed-by: Alex Gough <ajgo@chromium.org>
Commit-Queue: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/main@{#1022194}
NOKEYCHECK=True
GitOrigin-RevId: e1512be64c53904a3b5f56ebf9d8f79a8054d9f1
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 75aef4a..85a4b98 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -218,6 +218,7 @@
     "ipcz/driver_transport.h",
     "ipcz/fragment.h",
     "ipcz/fragment_descriptor.h",
+    "ipcz/fragment_ref.h",
     "ipcz/link_side.h",
     "ipcz/link_type.h",
     "ipcz/message.h",
@@ -229,6 +230,7 @@
     "ipcz/parcel.h",
     "ipcz/parcel_queue.h",
     "ipcz/portal.h",
+    "ipcz/ref_counted_fragment.h",
     "ipcz/remote_router_link.h",
     "ipcz/router.h",
     "ipcz/sequence_number.h",
@@ -250,6 +252,7 @@
     "ipcz/driver_transport.cc",
     "ipcz/fragment.cc",
     "ipcz/fragment_descriptor.cc",
+    "ipcz/fragment_ref.cc",
     "ipcz/handle_type.h",
     "ipcz/link_side.cc",
     "ipcz/link_type.cc",
@@ -275,6 +278,7 @@
     "ipcz/parcel.cc",
     "ipcz/parcel_queue.cc",
     "ipcz/portal.cc",
+    "ipcz/ref_counted_fragment.cc",
     "ipcz/remote_router_link.cc",
     "ipcz/router.cc",
     "ipcz/router_descriptor.cc",
@@ -330,6 +334,7 @@
     "ipcz/node_connector_test.cc",
     "ipcz/node_link_test.cc",
     "ipcz/parcel_queue_test.cc",
+    "ipcz/ref_counted_fragment_test.cc",
     "ipcz/sequenced_queue_test.cc",
     "reference_drivers/sync_reference_driver_test.cc",
     "remote_portal_test.cc",
diff --git a/src/ipcz/fragment_ref.cc b/src/ipcz/fragment_ref.cc
new file mode 100644
index 0000000..e586924
--- /dev/null
+++ b/src/ipcz/fragment_ref.cc
@@ -0,0 +1,54 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ipcz/fragment_ref.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "ipcz/fragment.h"
+#include "ipcz/node_link_memory.h"
+#include "ipcz/ref_counted_fragment.h"
+#include "util/ref_counted.h"
+
+namespace ipcz::internal {
+
+GenericFragmentRef::GenericFragmentRef() = default;
+
+GenericFragmentRef::GenericFragmentRef(Ref<NodeLinkMemory> memory,
+                                       const Fragment& fragment)
+    : memory_(std::move(memory)), fragment_(fragment) {}
+
+GenericFragmentRef::~GenericFragmentRef() {
+  reset();
+}
+
+void GenericFragmentRef::reset() {
+  Ref<NodeLinkMemory> memory = std::move(memory_);
+  if (fragment_.is_null()) {
+    return;
+  }
+
+  Fragment fragment;
+  std::swap(fragment, fragment_);
+  if (!fragment.is_addressable()) {
+    return;
+  }
+
+  auto* ref_counted = static_cast<RefCountedFragment*>(fragment.address());
+  if (ref_counted->ReleaseRef() > 1 || !memory) {
+    return;
+  }
+
+  memory->buffer_pool().FreeFragment(fragment);
+}
+
+Fragment GenericFragmentRef::release() {
+  Fragment fragment;
+  std::swap(fragment_, fragment);
+  memory_.reset();
+  return fragment;
+}
+
+}  // namespace ipcz::internal
diff --git a/src/ipcz/fragment_ref.h b/src/ipcz/fragment_ref.h
new file mode 100644
index 0000000..e54135b
--- /dev/null
+++ b/src/ipcz/fragment_ref.h
@@ -0,0 +1,144 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IPCZ_SRC_IPCZ_FRAGMENT_REF_H_
+#define IPCZ_SRC_IPCZ_FRAGMENT_REF_H_
+
+#include <algorithm>
+#include <type_traits>
+#include <utility>
+
+#include "ipcz/fragment.h"
+#include "ipcz/fragment_descriptor.h"
+#include "ipcz/ref_counted_fragment.h"
+#include "third_party/abseil-cpp/absl/base/macros.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+
+class NodeLinkMemory;
+
+namespace internal {
+
+// Base class for any FragmentRef<T>, implementing common behavior for managing
+// the underlying RefCountedFragment.
+class GenericFragmentRef {
+ public:
+  GenericFragmentRef();
+
+  // Does not increase the ref count of the underlying RefCountedFragment,
+  // effectively assuming ownership of a previously acquired ref.
+  GenericFragmentRef(Ref<NodeLinkMemory> memory, const Fragment& fragment);
+
+  ~GenericFragmentRef();
+
+  const Ref<NodeLinkMemory>& memory() const { return memory_; }
+  const Fragment& fragment() const { return fragment_; }
+
+  bool is_null() const { return fragment_.is_null(); }
+  bool is_addressable() const { return fragment_.is_addressable(); }
+  bool is_pending() const { return fragment_.is_pending(); }
+
+  void reset();
+  Fragment release();
+
+  int32_t ref_count_for_testing() const {
+    return AsRefCountedFragment()->ref_count_for_testing();
+  }
+
+ protected:
+  RefCountedFragment* AsRefCountedFragment() const {
+    return static_cast<RefCountedFragment*>(fragment_.address());
+  }
+
+  // The NodeLinkMemory who ultimately owns this fragment's memory. May be null
+  // if the FragmentRef is unmanaged.
+  Ref<NodeLinkMemory> memory_;
+
+  Fragment fragment_;
+};
+
+}  // namespace internal
+
+// Holds a reference to a RefCountedFragment. When this object is destroyed, the
+// underlying ref count is decreased. If the ref count is decreased to zero, the
+// underlying Fragment is returned to its NodeLinkMemory.
+//
+// Some FragmentRefs may be designated as "unmanaged", meaning that they will
+// never attempt to free the underlying Fragment. These refs are used to
+// preserve type compatibility with other similar (but managed) FragmentRefs
+// when the underlying Fragment isn't dynamically allocated and can't be freed.
+//
+// For example most RouterLinkState fragments are dynamically allocated and
+// managed by FragmentRefs, but some instances are allocated at fixed locations
+// within the NodeLinkMemory and cannot be freed or reused. In both cases, ipcz
+// can refer to these objects using a FragmentRef<RouterLinkState>.
+template <typename T>
+class FragmentRef : public internal::GenericFragmentRef {
+ public:
+  static_assert(std::is_base_of<RefCountedFragment, T>::value,
+                "T must inherit RefCountedFragment for FragmentRef<T>");
+
+  constexpr FragmentRef() = default;
+  constexpr FragmentRef(std::nullptr_t) : FragmentRef() {}
+
+  // Adopts an existing ref to the RefCountedFragment located at the beginning
+  // of `fragment`, which is a Fragment owned by `memory.
+  FragmentRef(decltype(RefCountedFragment::kAdoptExistingRef),
+              Ref<NodeLinkMemory> memory,
+              const Fragment& fragment)
+      : GenericFragmentRef(std::move(memory), fragment) {
+    ABSL_ASSERT(memory_);
+    ABSL_ASSERT(fragment_.is_null() || fragment_.size() >= sizeof(T));
+  }
+
+  // Constructs an unmanaged FragmentRef, which references `fragment` and
+  // updates its refcount, but which never attempts to release `fragment` back
+  // to its NodeLinkMemory. This is only safe to use with Fragments which cannot
+  // be freed.
+  FragmentRef(decltype(RefCountedFragment::kUnmanagedRef),
+              const Fragment& fragment)
+      : GenericFragmentRef(nullptr, fragment) {
+    ABSL_ASSERT(fragment_.is_null() || fragment_.size() >= sizeof(T));
+  }
+
+  FragmentRef(const FragmentRef<T>& other)
+      : GenericFragmentRef(other.memory(), other.fragment()) {
+    if (!fragment_.is_null()) {
+      ABSL_ASSERT(fragment_.is_addressable());
+      AsRefCountedFragment()->AddRef();
+    }
+  }
+
+  FragmentRef(FragmentRef<T>&& other) noexcept
+      : GenericFragmentRef(std::move(other.memory_), other.fragment_) {
+    other.release();
+  }
+
+  FragmentRef<T>& operator=(const FragmentRef<T>& other) {
+    reset();
+    memory_ = other.memory();
+    fragment_ = other.fragment();
+    if (!fragment_.is_null()) {
+      ABSL_ASSERT(fragment_.is_addressable());
+      AsRefCountedFragment()->AddRef();
+    }
+    return *this;
+  }
+
+  FragmentRef<T>& operator=(FragmentRef<T>&& other) {
+    reset();
+    memory_ = std::move(other.memory_);
+    fragment_ = other.release();
+    return *this;
+  }
+
+  T* get() const { return static_cast<T*>(fragment_.address()); }
+  T* operator->() const { return get(); }
+  T& operator*() const { return *get(); }
+};
+
+}  // namespace ipcz
+
+#endif  // IPCZ_SRC_IPCZ_FRAGMENT_REF_H_
diff --git a/src/ipcz/node_link_memory.cc b/src/ipcz/node_link_memory.cc
index 5a8aba0..012f26c 100644
--- a/src/ipcz/node_link_memory.cc
+++ b/src/ipcz/node_link_memory.cc
@@ -25,12 +25,11 @@
 // Fixed allocation size for each NodeLink's primary shared buffer.
 constexpr size_t kPrimaryBufferSize = 65536;
 
-}  // namespace
+// The front of the primary buffer is reserved for special current and future
+// uses which require synchronous availability throughout a link's lifetime.
+constexpr size_t kPrimaryBufferReservedHeaderSize = 256;
 
-// This structure always sits at offset 0 in the primary buffer and has a fixed
-// layout according to the NodeLink's agreed upon protocol version. This is the
-// layout for version 0 (currently the only version.)
-struct IPCZ_ALIGN(8) NodeLinkMemory::PrimaryBuffer {
+struct IPCZ_ALIGN(8) PrimaryBufferHeader {
   // Atomic generator for new unique BufferIds to use across the associated
   // NodeLink. This allows each side of a NodeLink to generate new BufferIds
   // spontaneously without synchronization or risk of collisions.
@@ -42,6 +41,51 @@
   std::atomic<uint64_t> next_sublink_id;
 };
 
+static_assert(sizeof(PrimaryBufferHeader) < kPrimaryBufferReservedHeaderSize);
+
+constexpr size_t kPrimaryBufferHeaderPaddingSize =
+    kPrimaryBufferReservedHeaderSize - sizeof(PrimaryBufferHeader);
+
+}  // namespace
+
+// This structure always sits at offset 0 in the primary buffer and has a fixed
+// layout according to the NodeLink's agreed upon protocol version. This is the
+// layout for version 0 (currently the only version.)
+struct IPCZ_ALIGN(8) NodeLinkMemory::PrimaryBuffer {
+  // Header + padding occupies the first 256 bytes.
+  PrimaryBufferHeader header;
+  uint8_t reserved_header_padding[kPrimaryBufferHeaderPaddingSize];
+
+  // Reserved memory for a series of fixed block allocators. Additional
+  // allocators may be adopted by a NodeLinkMemory over its lifetime, but these
+  // ones remain fixed within the primary buffer.
+  std::array<uint8_t, 4096> mem_for_64_byte_blocks;
+  std::array<uint8_t, 12288> mem_for_256_byte_blocks;
+  std::array<uint8_t, 15360> mem_for_512_byte_blocks;
+  std::array<uint8_t, 11264> mem_for_1024_byte_blocks;
+  std::array<uint8_t, 16384> mem_for_2048_byte_blocks;
+
+  BlockAllocator block_allocator_64() {
+    return BlockAllocator(absl::MakeSpan(mem_for_64_byte_blocks), 64);
+  }
+
+  BlockAllocator block_allocator_256() {
+    return BlockAllocator(absl::MakeSpan(mem_for_256_byte_blocks), 256);
+  }
+
+  BlockAllocator block_allocator_512() {
+    return BlockAllocator(absl::MakeSpan(mem_for_512_byte_blocks), 512);
+  }
+
+  BlockAllocator block_allocator_1024() {
+    return BlockAllocator(absl::MakeSpan(mem_for_1024_byte_blocks), 1024);
+  }
+
+  BlockAllocator block_allocator_2048() {
+    return BlockAllocator(absl::MakeSpan(mem_for_2048_byte_blocks), 2048);
+  }
+};
+
 NodeLinkMemory::NodeLinkMemory(Ref<Node> node,
                                DriverMemoryMapping primary_buffer_memory)
     : node_(std::move(node)),
@@ -52,8 +96,17 @@
   static_assert(sizeof(PrimaryBuffer) <= kPrimaryBufferSize,
                 "PrimaryBuffer structure is too large.");
 
-  buffer_pool_.AddBuffer(BufferId{kPrimaryBufferId},
-                         std::move(primary_buffer_memory));
+  buffer_pool_.AddBuffer(kPrimaryBufferId, std::move(primary_buffer_memory));
+  buffer_pool_.RegisterBlockAllocator(kPrimaryBufferId,
+                                      primary_buffer_.block_allocator_64());
+  buffer_pool_.RegisterBlockAllocator(kPrimaryBufferId,
+                                      primary_buffer_.block_allocator_256());
+  buffer_pool_.RegisterBlockAllocator(kPrimaryBufferId,
+                                      primary_buffer_.block_allocator_512());
+  buffer_pool_.RegisterBlockAllocator(kPrimaryBufferId,
+                                      primary_buffer_.block_allocator_1024());
+  buffer_pool_.RegisterBlockAllocator(kPrimaryBufferId,
+                                      primary_buffer_.block_allocator_2048());
 }
 
 NodeLinkMemory::~NodeLinkMemory() = default;
@@ -71,15 +124,21 @@
   PrimaryBuffer& primary_buffer = memory->primary_buffer_;
 
   // The first allocable BufferId is 1, because the primary buffer uses 0.
-  primary_buffer.next_buffer_id.store(1, std::memory_order_relaxed);
+  primary_buffer.header.next_buffer_id.store(1, std::memory_order_relaxed);
 
   // The first allocable SublinkId is kMaxInitialPortals. This way it doesn't
   // matter whether the two ends of a NodeLink initiate their connection with a
   // different initial portal count: neither can request more than
   // kMaxInitialPortals, so neither will be assuming initial ownership of any
   // SublinkIds at or above this value.
-  primary_buffer.next_sublink_id.store(kMaxInitialPortals,
-                                       std::memory_order_release);
+  primary_buffer.header.next_sublink_id.store(kMaxInitialPortals,
+                                              std::memory_order_release);
+
+  primary_buffer.block_allocator_64().InitializeRegion();
+  primary_buffer.block_allocator_256().InitializeRegion();
+  primary_buffer.block_allocator_512().InitializeRegion();
+  primary_buffer.block_allocator_1024().InitializeRegion();
+  primary_buffer.block_allocator_2048().InitializeRegion();
 
   return {
       .node_link_memory = std::move(memory),
@@ -95,12 +154,12 @@
 }
 
 BufferId NodeLinkMemory::AllocateNewBufferId() {
-  return BufferId{
-      primary_buffer_.next_buffer_id.fetch_add(1, std::memory_order_relaxed)};
+  return BufferId{primary_buffer_.header.next_buffer_id.fetch_add(
+      1, std::memory_order_relaxed)};
 }
 
 SublinkId NodeLinkMemory::AllocateSublinkIds(size_t count) {
-  return SublinkId{primary_buffer_.next_sublink_id.fetch_add(
+  return SublinkId{primary_buffer_.header.next_sublink_id.fetch_add(
       count, std::memory_order_relaxed)};
 }
 
diff --git a/src/ipcz/node_link_memory.h b/src/ipcz/node_link_memory.h
index b4d65a6..014a78e 100644
--- a/src/ipcz/node_link_memory.h
+++ b/src/ipcz/node_link_memory.h
@@ -63,7 +63,7 @@
   // Exposes the underlying BufferPool which owns all shared buffers for this
   // NodeLinkMemory and which facilitates dynamic allocation of the fragments
   // within.
-  BufferPool& buffer_pool();
+  BufferPool& buffer_pool() { return buffer_pool_; }
 
   // Returns a new BufferId which should still be unused by any buffer in this
   // NodeLinkMemory's BufferPool, or that of its peer NodeLinkMemory. When
diff --git a/src/ipcz/ref_counted_fragment.cc b/src/ipcz/ref_counted_fragment.cc
new file mode 100644
index 0000000..14b21cf
--- /dev/null
+++ b/src/ipcz/ref_counted_fragment.cc
@@ -0,0 +1,23 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ipcz/ref_counted_fragment.h"
+
+#include "third_party/abseil-cpp/absl/base/macros.h"
+
+namespace ipcz {
+
+RefCountedFragment::RefCountedFragment() = default;
+
+RefCountedFragment::~RefCountedFragment() = default;
+
+void RefCountedFragment::AddRef() {
+  ref_count_.fetch_add(1, std::memory_order_relaxed);
+}
+
+int32_t RefCountedFragment::ReleaseRef() {
+  return ref_count_.fetch_sub(1, std::memory_order_acq_rel);
+}
+
+}  // namespace ipcz
diff --git a/src/ipcz/ref_counted_fragment.h b/src/ipcz/ref_counted_fragment.h
new file mode 100644
index 0000000..ff69c45
--- /dev/null
+++ b/src/ipcz/ref_counted_fragment.h
@@ -0,0 +1,41 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IPCZ_SRC_IPCZ_REF_COUNTED_FRAGMENT_H_
+#define IPCZ_SRC_IPCZ_REF_COUNTED_FRAGMENT_H_
+
+#include <atomic>
+
+#include "ipcz/ipcz.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+
+// A RefCountedFragment is an object allocated within a shared Fragment from
+// NodeLinkMemory, and which is automatially freed when its last reference is
+// released. Consumers can hold onto references to RefCountedFragment objects
+// by holding a FragmentRef.
+struct IPCZ_ALIGN(4) RefCountedFragment {
+  enum { kAdoptExistingRef };
+  enum { kUnmanagedRef };
+
+  RefCountedFragment();
+  ~RefCountedFragment();
+
+  int32_t ref_count_for_testing() const { return ref_count_; }
+
+  // Increments the reference count for this object.
+  void AddRef();
+
+  // Releases a reference and returns the previous reference count. If this
+  // returns 1, the underlying Fragment can be safely freed.
+  int32_t ReleaseRef();
+
+ private:
+  std::atomic<int32_t> ref_count_{1};
+};
+
+}  // namespace ipcz
+
+#endif  // IPCZ_SRC_IPCZ_REF_COUNTED_FRAGMENT_H_
diff --git a/src/ipcz/ref_counted_fragment_test.cc b/src/ipcz/ref_counted_fragment_test.cc
new file mode 100644
index 0000000..5425304
--- /dev/null
+++ b/src/ipcz/ref_counted_fragment_test.cc
@@ -0,0 +1,179 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ipcz/ref_counted_fragment.h"
+
+#include <atomic>
+#include <tuple>
+
+#include "ipcz/fragment.h"
+#include "ipcz/fragment_ref.h"
+#include "ipcz/node.h"
+#include "ipcz/node_link_memory.h"
+#include "reference_drivers/sync_reference_driver.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+namespace {
+
+const IpczDriver& kTestDriver = reference_drivers::kSyncReferenceDriver;
+
+using RefCountedFragmentTest = testing::Test;
+
+using TestObject = RefCountedFragment;
+
+TEST_F(RefCountedFragmentTest, NullRef) {
+  FragmentRef<TestObject> ref;
+  EXPECT_TRUE(ref.is_null());
+  EXPECT_FALSE(ref.is_addressable());
+
+  ref.reset();
+  EXPECT_TRUE(ref.is_null());
+  EXPECT_FALSE(ref.is_addressable());
+
+  FragmentRef<TestObject> other1 = ref;
+  EXPECT_TRUE(ref.is_null());
+  EXPECT_FALSE(ref.is_addressable());
+  EXPECT_TRUE(other1.is_null());
+  EXPECT_FALSE(other1.is_addressable());
+
+  FragmentRef<TestObject> other2 = std::move(ref);
+  EXPECT_TRUE(ref.is_null());
+  EXPECT_FALSE(ref.is_addressable());
+  EXPECT_TRUE(other2.is_null());
+  EXPECT_FALSE(other2.is_addressable());
+
+  ref = other1;
+  EXPECT_TRUE(ref.is_null());
+  EXPECT_FALSE(ref.is_addressable());
+  EXPECT_TRUE(other1.is_null());
+  EXPECT_FALSE(other1.is_addressable());
+
+  ref = std::move(other2);
+  EXPECT_TRUE(ref.is_null());
+  EXPECT_FALSE(ref.is_addressable());
+  EXPECT_TRUE(other1.is_null());
+  EXPECT_FALSE(other1.is_addressable());
+}
+
+TEST_F(RefCountedFragmentTest, SimpleRef) {
+  TestObject object;
+
+  FragmentRef<TestObject> ref(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, sizeof(object)), &object));
+  EXPECT_EQ(1, object.ref_count_for_testing());
+  ref.reset();
+  EXPECT_EQ(0, object.ref_count_for_testing());
+}
+
+TEST_F(RefCountedFragmentTest, Copy) {
+  TestObject object1;
+
+  FragmentRef<TestObject> ref1(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, sizeof(object1)), &object1));
+  EXPECT_EQ(1, object1.ref_count_for_testing());
+
+  FragmentRef<TestObject> other1 = ref1;
+  EXPECT_EQ(2, object1.ref_count_for_testing());
+  other1.reset();
+  EXPECT_EQ(1, object1.ref_count_for_testing());
+  EXPECT_TRUE(other1.is_null());
+  EXPECT_FALSE(other1.is_addressable());
+
+  TestObject object2;
+  auto ref2 = FragmentRef<TestObject>(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, sizeof(object2)), &object2));
+  EXPECT_EQ(1, object1.ref_count_for_testing());
+  EXPECT_EQ(1, object2.ref_count_for_testing());
+  ref2 = ref1;
+  EXPECT_EQ(2, object1.ref_count_for_testing());
+  EXPECT_EQ(0, object2.ref_count_for_testing());
+  EXPECT_FALSE(ref1.is_null());
+  EXPECT_TRUE(ref1.is_addressable());
+  EXPECT_FALSE(ref2.is_null());
+  EXPECT_TRUE(ref2.is_addressable());
+  ref1.reset();
+  EXPECT_EQ(1, object1.ref_count_for_testing());
+  EXPECT_EQ(0, object2.ref_count_for_testing());
+  EXPECT_TRUE(ref1.is_null());
+  EXPECT_FALSE(ref1.is_addressable());
+  ref2.reset();
+  EXPECT_EQ(0, object1.ref_count_for_testing());
+  EXPECT_EQ(0, object2.ref_count_for_testing());
+  EXPECT_TRUE(ref2.is_null());
+  EXPECT_FALSE(ref2.is_addressable());
+}
+
+TEST_F(RefCountedFragmentTest, Move) {
+  TestObject object1;
+
+  FragmentRef<TestObject> ref1(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, sizeof(object1)), &object1));
+  EXPECT_EQ(1, ref1.ref_count_for_testing());
+
+  FragmentRef<TestObject> other1 = std::move(ref1);
+  EXPECT_EQ(1, object1.ref_count_for_testing());
+  EXPECT_FALSE(other1.is_null());
+  EXPECT_TRUE(other1.is_addressable());
+  EXPECT_TRUE(ref1.is_null());
+  EXPECT_FALSE(ref1.is_addressable());
+  other1.reset();
+  EXPECT_TRUE(other1.is_null());
+  EXPECT_FALSE(other1.is_addressable());
+  EXPECT_EQ(0, object1.ref_count_for_testing());
+
+  TestObject object2;
+  TestObject object3;
+  FragmentRef<TestObject> ref2(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, sizeof(object2)), &object2));
+  FragmentRef<TestObject> ref3(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, sizeof(object3)), &object3));
+
+  EXPECT_FALSE(ref2.is_null());
+  EXPECT_TRUE(ref2.is_addressable());
+  EXPECT_FALSE(ref3.is_null());
+  EXPECT_TRUE(ref3.is_addressable());
+  EXPECT_EQ(1, object2.ref_count_for_testing());
+  EXPECT_EQ(1, object3.ref_count_for_testing());
+  ref3 = std::move(ref2);
+  EXPECT_EQ(1, object2.ref_count_for_testing());
+  EXPECT_EQ(0, object3.ref_count_for_testing());
+  EXPECT_TRUE(ref2.is_null());
+  EXPECT_FALSE(ref2.is_addressable());
+  EXPECT_FALSE(ref3.is_null());
+  EXPECT_TRUE(ref3.is_addressable());
+  ref3.reset();
+  EXPECT_TRUE(ref3.is_null());
+  EXPECT_FALSE(ref3.is_addressable());
+  EXPECT_EQ(0, object2.ref_count_for_testing());
+  EXPECT_EQ(0, object3.ref_count_for_testing());
+}
+
+TEST_F(RefCountedFragmentTest, Free) {
+  auto node = MakeRefCounted<Node>(Node::Type::kNormal, kTestDriver,
+                                   IPCZ_INVALID_DRIVER_HANDLE);
+  auto memory = NodeLinkMemory::Allocate(std::move(node)).node_link_memory;
+
+  // Allocate a ton of fragments and let them be released by FragmentRef on
+  // destruction. If the fragments aren't freed properly, allocations will fail
+  // and so will the test.
+  constexpr size_t kNumAllocations = 100000;
+  for (size_t i = 0; i < kNumAllocations; ++i) {
+    Fragment fragment =
+        memory->buffer_pool().AllocateFragment(sizeof(TestObject));
+    EXPECT_TRUE(fragment.is_addressable());
+    FragmentRef<TestObject> ref(RefCountedFragment::kAdoptExistingRef, memory,
+                                fragment);
+  }
+}
+
+}  // namespace
+}  // namespace ipcz