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}
NOKEYCHECK=True
GitOrigin-RevId: 6385cfb931f738334dc59e9d7d39eb7daa70f7ba
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..5bb0db4
--- /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, 0), &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, 0), &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, 0), &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, 0), &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, 0), &object2));
+  FragmentRef<TestObject> ref3(
+      RefCountedFragment::kUnmanagedRef,
+      Fragment(FragmentDescriptor(BufferId(0), 0, 0), &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