[RFC PATCH 11/12] drm/fabric: add mutation KUnit tests

Konstantin Sinyuk <[email protected]>
Newsgroups org.kernel.vger.linux-doc,org.freedesktop.lists.dri-devel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest,org.kernel.vger.netdev
Message-ID <56ac95cbe4dfdfec86f4828bd59e4b3539b9bb39.1787552412.git.ksinyuk@kernel.org>
Add KUnit coverage for the provisioning core: userspace-fabric lifetime
and its module reference, orphan attach and detach, administrative state,
peer-management authority, and failure paths that must not commit.

Threaded cases verify that mutation serializes against endpoint unregister
and competing fabric-ep-id registration, and that concurrent mutators never
overlap provider callbacks.

Signed-off-by: Konstantin Sinyuk <[email protected]>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
 drivers/gpu/drm/fabric/Kconfig           |    3 +-
 drivers/gpu/drm/fabric/drm_fabric_test.c | 1612 ++++++++++++++++++++--
 2 files changed, 1467 insertions(+), 148 deletions(-)

diff --git a/drivers/gpu/drm/fabric/Kconfig b/drivers/gpu/drm/fabric/Kconfig
index 2a70cac85b1d..7f427ac42a27 100644
--- a/drivers/gpu/drm/fabric/Kconfig
+++ b/drivers/gpu/drm/fabric/Kconfig
@@ -29,7 +29,8 @@ config DRM_FABRIC_KUNIT_TEST
 	depends on KUNIT=y || DRM_FABRIC=m
 	default KUNIT_ALL_TESTS
 	help
-	  Enable KUnit coverage for the drm_fabric object model.
+	  KUnit tests for the drm_fabric object model and topology-mutation
+	  paths.
 
 	  The tests are built into drm_fabric itself, so they need no exported
 	  symbols or test-only accessors in the production source.
diff --git a/drivers/gpu/drm/fabric/drm_fabric_test.c b/drivers/gpu/drm/fabric/drm_fabric_test.c
index 863b7cf69068..d2c88d183737 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_test.c
+++ b/drivers/gpu/drm/fabric/drm_fabric_test.c
@@ -11,9 +11,15 @@
 #include <kunit/test.h>
 #include <kunit/device.h>
 
+#include <linux/atomic.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
 #include <linux/device.h>
 #include <linux/err.h>
+#include <linux/jiffies.h>
+#include <linux/kthread.h>
 #include <linux/mutex.h>
+#include <linux/sched.h>
 #include <linux/string.h>
 
 #include <drm/drm_fabric.h>
@@ -35,6 +41,36 @@ static struct device *fabrictest_alloc_dev(struct kunit *test)
 	return dev;
 }
 
+static struct drm_fabric *fabrictest_find_fabric(u32 id)
+{
+	struct drm_fabric *fab;
+
+	mutex_lock(&drm_fabric_lock);
+	fab = drm_fabric_find_by_id(id);
+	mutex_unlock(&drm_fabric_lock);
+
+	return fab;
+}
+
+/*
+ * The FD-01 pin/unpin pair is only observable when drm_fabric is a loadable
+ * module; built-in, try_module_get() is a stub. Check this before trusting
+ * fabrictest_module_refcount().
+ */
+static bool fabrictest_module_refcount_observable(void)
+{
+	return IS_ENABLED(CONFIG_MODULE_UNLOAD) && IS_MODULE(CONFIG_DRM_FABRIC);
+}
+
+static int fabrictest_module_refcount(void)
+{
+#if defined(CONFIG_MODULE_UNLOAD) && IS_MODULE(CONFIG_DRM_FABRIC)
+	return module_refcount(THIS_MODULE);
+#else
+	return 0;
+#endif
+}
+
 static void fabrictest_unregister_fabric(void *fab)
 {
 	drm_fabric_unregister(fab);
@@ -759,44 +795,148 @@ static void drm_fabric_test_mesh_kn_topology(struct kunit *test)
 #undef KN_PORTS_PER_EP
 }
 
-static int fabrictest_stats_get(struct drm_fabric_port *port,
-				struct drm_fabric_port_stats *stats)
+/* Trivial provider that accepts every mutation so the core commits it. */
+static int fabrictest_mut_endpoint_set(struct drm_fabric_endpoint *ep,
+				       const struct drm_fabric_endpoint_change *change,
+				       struct drm_fabric *fabric)
 {
-	stats->read_bytes = 4096;
-	stats->write_bytes = 2048;
-	stats->link_down_count = 2;
-	stats->retrain_count = 3;
 	return 0;
 }
 
-static const struct drm_fabric_ops fabrictest_stats_ops = {
-	.port_stats_get = fabrictest_stats_get,
+static int fabrictest_mut_port_set(struct drm_fabric_port *port,
+				   enum drm_fabric_admin_state admin)
+{
+	return 0;
+}
+
+static int fabrictest_mut_port_peer_new(struct drm_fabric_port *port,
+					const struct drm_fabric_peer *peer)
+{
+	return 0;
+}
+
+static int fabrictest_mut_port_peer_del(struct drm_fabric_port *port)
+{
+	return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_mut_ops = {
+	.endpoint_set	= fabrictest_mut_endpoint_set,
+	.port_set	= fabrictest_mut_port_set,
+	.port_peer_new	= fabrictest_mut_port_peer_new,
+	.port_peer_del	= fabrictest_mut_port_peer_del,
 };
 
-/* This does not exercise netlink dispatch or error propagation. */
-static void drm_fabric_test_port_stats_ops_registration(struct kunit *test)
+/*
+ * Provider that rejects every mutation: the core calls it before committing, so
+ * a failure must leave state, generation and notifications untouched.
+ */
+static int fabrictest_fail_port_set(struct drm_fabric_port *port,
+				    enum drm_fabric_admin_state admin)
 {
-	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	return -EIO;
+}
+
+static int fabrictest_fail_port_peer_new(struct drm_fabric_port *port,
+					 const struct drm_fabric_peer *peer)
+{
+	return -EIO;
+}
+
+static const struct drm_fabric_ops fabrictest_fail_ops = {
+	.port_set	= fabrictest_fail_port_set,
+	.port_peer_new	= fabrictest_fail_port_peer_new,
+};
+
+/*
+ * Internal mutators assert drm_fabric_mutation_lock is held, matching the
+ * netlink pre/post_doit contract; wrap each with the lock here.
+ */
+static int fabrictest_ep_set_locked(struct drm_fabric_endpoint *ep,
+				    const struct drm_fabric_endpoint_change *change)
+{
+	int ret;
+
+	mutex_lock(&drm_fabric_mutation_lock);
+	ret = drm_fabric_endpoint_set(ep, change);
+	mutex_unlock(&drm_fabric_mutation_lock);
+	return ret;
+}
+
+static int fabrictest_port_admin_locked(struct drm_fabric_port *port,
+					enum drm_fabric_admin_state admin)
+{
+	int ret;
+
+	mutex_lock(&drm_fabric_mutation_lock);
+	ret = drm_fabric_port_set_admin(port, admin);
+	mutex_unlock(&drm_fabric_mutation_lock);
+	return ret;
+}
+
+static int fabrictest_port_peer_new_locked(struct drm_fabric_port *port,
+					   const struct drm_fabric_peer *peer)
+{
+	int ret;
+
+	mutex_lock(&drm_fabric_mutation_lock);
+	ret = drm_fabric_port_peer_new(port, peer);
+	mutex_unlock(&drm_fabric_mutation_lock);
+	return ret;
+}
+
+static int fabrictest_port_peer_del_locked(struct drm_fabric_port *port)
+{
+	int ret;
+
+	mutex_lock(&drm_fabric_mutation_lock);
+	ret = drm_fabric_port_peer_del(port);
+	mutex_unlock(&drm_fabric_mutation_lock);
+	return ret;
+}
+
+static int fabrictest_user_fabric_new_locked(enum drm_fabric_type type,
+					     u64 instance_id, const char *name,
+					     u32 *fabric_id_out)
+{
+	int ret;
+
+	mutex_lock(&drm_fabric_mutation_lock);
+	ret = drm_fabric_user_fabric_new(type, instance_id, name, fabric_id_out);
+	mutex_unlock(&drm_fabric_mutation_lock);
+	return ret;
+}
+
+static int fabrictest_user_fabric_del_locked(u32 fabric_id)
+{
+	int ret;
+
+	mutex_lock(&drm_fabric_mutation_lock);
+	ret = drm_fabric_user_fabric_del(fabric_id);
+	mutex_unlock(&drm_fabric_mutation_lock);
+	return ret;
+}
+
+static void drm_fabric_test_failed_mutation_no_commit(struct kunit *test)
+{
+	struct device *dev = fabrictest_alloc_dev(test);
 	struct drm_fabric_desc fdesc = {
-		.type = DRM_FABRIC_TYPE_SYNTHETIC,
-		.name = "test-stats",
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "test-failmut",
+	};
+	/* USERSPACE peer_mode so PORT_PEER_NEW reaches the provider below. */
+	struct drm_fabric_port_desc pdesc = {
+		.index = 0, .max_lane_count = 4,
+		.peer_mode = DRM_FABRIC_PEER_MODE_USERSPACE,
 	};
-	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
 	struct drm_fabric_endpoint_desc edesc = {
-		.fabric_ep_id = 0x5A,
-		.parent = fabrictest_dev,
-		.ops = &fabrictest_stats_ops,
-		.ports = &pdesc,
-		.num_ports = 1,
+		.fabric_ep_id = 0x66, .parent = dev, .ops = &fabrictest_fail_ops,
+		.ports = &pdesc, .num_ports = 1,
 	};
-	struct drm_fabric_endpoint_desc edesc_noops = {
-		.fabric_ep_id = 0x5B,
-		.parent = fabrictest_dev,
-		.ports = &pdesc,
-		.num_ports = 1,
+	struct drm_fabric_peer peer = {
+		.peer_id = 0x67, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
 	};
-	struct drm_fabric_port_stats stats = {};
-	struct drm_fabric_endpoint *ep, *ep_noops;
+	enum drm_fabric_admin_state admin0;
+	struct drm_fabric_endpoint *ep;
 	struct drm_fabric_port *port;
 	struct drm_fabric *fab;
 	u32 gen;
@@ -813,188 +953,194 @@ static void drm_fabric_test_port_stats_ops_registration(struct kunit *test)
 
 	port = fabrictest_port(ep, 0);
 	KUNIT_ASSERT_NOT_NULL(test, port);
+	admin0 = port->admin_state;
+	gen = drm_fabric_base_seq;
 
-	KUNIT_ASSERT_NOT_NULL(test, ep->ops);
-	KUNIT_ASSERT_NOT_NULL(test, ep->ops->port_stats_get);
-
-	/* A stats read is not a topology change: seq must not move. */
-	gen = fabrictest_seq_read();
-	KUNIT_EXPECT_EQ(test, ep->ops->port_stats_get(port, &stats), 0);
-	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), gen);
-	KUNIT_EXPECT_EQ(test, stats.read_bytes, 4096ULL);
-	KUNIT_EXPECT_EQ(test, stats.write_bytes, 2048ULL);
-	KUNIT_EXPECT_EQ(test, stats.link_down_count, 2ULL);
-	KUNIT_EXPECT_EQ(test, stats.retrain_count, 3ULL);
+	KUNIT_EXPECT_EQ(test,
+			fabrictest_port_admin_locked(port, DRM_FABRIC_ADMIN_STATE_UP), -EIO);
+	KUNIT_EXPECT_EQ(test, port->admin_state, admin0);
+	KUNIT_EXPECT_EQ(test, drm_fabric_base_seq, gen);
 
-	ep_noops = drm_fabric_endpoint_register(fab, &edesc_noops);
-	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_noops));
-	KUNIT_ASSERT_EQ(test, 0,
-			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_noops));
-	KUNIT_EXPECT_TRUE(test, !ep_noops->ops || !ep_noops->ops->port_stats_get);
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_new_locked(port, &peer), -EIO);
+	KUNIT_EXPECT_FALSE(test, port->has_peer);
+	KUNIT_EXPECT_EQ(test, drm_fabric_base_seq, gen);
 }
 
-/*
- * Unregistering an endpoint that has a peer link must clear only that
- * endpoint's own port record; it must not touch the still-registered far
- * side's peer record. (Contrast with drm_fabric_port_unset_peer(), which
- * clears a peer explicitly and is covered separately.)
- */
-static void drm_fabric_test_local_unplug_keeps_edge(struct kunit *test)
+static void drm_fabric_test_orphan_attach_detach(struct kunit *test)
 {
 	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
 	struct drm_fabric_desc fdesc = {
 		.type = DRM_FABRIC_TYPE_SYNTHETIC,
-		.name = "test-unplug",
+		.name = "test-attach",
 	};
 	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
-	struct drm_fabric_endpoint_desc eadesc = {
-		.fabric_ep_id = 0xA0,
-		.name = "unplug-a",
-		.parent = fabrictest_dev,
-		.ports = &pdesc,
-		.num_ports = 1,
-	};
-	struct drm_fabric_endpoint_desc ebdesc = {
-		.fabric_ep_id = 0xB0,
-		.name = "unplug-b",
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x55,
+		.name = "orphan-ep",
 		.parent = fabrictest_dev,
+		.ops = &fabrictest_mut_ops,
 		.ports = &pdesc,
 		.num_ports = 1,
 	};
-	struct drm_fabric_peer to_b = {
-		.peer_id = 0xB0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
-	};
-	struct drm_fabric_peer to_a = {
-		.peer_id = 0xA0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
-	};
+	struct drm_fabric_endpoint_change change;
 	struct drm_fabric *fab;
-	struct drm_fabric_endpoint *ep_a, *ep_b;
-	struct drm_fabric_port *pa, *pb;
+	struct drm_fabric_endpoint *ep;
+	u32 seq;
 
 	fab = drm_fabric_register(&fdesc);
 	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
 
-	ep_a = drm_fabric_endpoint_register(fab, &eadesc);
-	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
-	KUNIT_ASSERT_EQ(test, 0,
-			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
-
-	ep_b = drm_fabric_endpoint_register(fab, &ebdesc);
-	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_b));
+	ep = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
 	KUNIT_ASSERT_EQ(test, 0,
-			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_b));
-
-	pa = fabrictest_port(ep_a, 0);
-	pb = fabrictest_port(ep_b, 0);
-	KUNIT_ASSERT_NOT_NULL(test, pa);
-	KUNIT_ASSERT_NOT_NULL(test, pb);
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
 
-	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &to_b), 0);
-	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pb, &to_a), 0);
-	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+	KUNIT_EXPECT_NULL(test, ep->fabric);
+	KUNIT_EXPECT_EQ(test, ep->admin_state, DRM_FABRIC_ADMIN_STATE_DOWN);
 
 	/*
-	 * Remove B without retracting its peer first, modelling abrupt provider
-	 * teardown.
+	 * Membership and admin state are independent in the core; provider
+	 * policy may reject combinations such as admin-up on an orphan.
 	 */
-	kunit_release_action(test, fabrictest_unregister_endpoint, ep_b);
+	seq = drm_fabric_base_seq;
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_ADMIN, .admin = DRM_FABRIC_ADMIN_STATE_UP,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+	KUNIT_EXPECT_EQ(test, ep->admin_state, DRM_FABRIC_ADMIN_STATE_UP);
+	KUNIT_EXPECT_NE(test, drm_fabric_base_seq, seq);
 
-	/* The surviving half-edge must be byte-unchanged: no field mutated. */
-	KUNIT_EXPECT_TRUE(test, pa->has_peer);
-	KUNIT_EXPECT_MEMEQ(test, &pa->peer, &to_b, sizeof(pa->peer));
+	/* Attaching changes membership only; admin_state is untouched. */
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = fab->id,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+	KUNIT_EXPECT_PTR_EQ(test, ep->fabric, fab);
+	KUNIT_EXPECT_EQ(test, ep->admin_state, DRM_FABRIC_ADMIN_STATE_UP);
+
+	/* Detach while admin is UP is accepted: membership clears, admin is kept. */
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = 0,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+	KUNIT_EXPECT_NULL(test, ep->fabric);
+	KUNIT_EXPECT_EQ(test, ep->admin_state, DRM_FABRIC_ADMIN_STATE_UP);
+
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_ADMIN, .admin = DRM_FABRIC_ADMIN_STATE_DOWN,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+	KUNIT_EXPECT_EQ(test, ep->admin_state, DRM_FABRIC_ADMIN_STATE_DOWN);
+
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = 0,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+	KUNIT_EXPECT_NULL(test, ep->fabric);
 }
 
 /*
- * A's peer record names a port index, not an object; registering and then
- * unregistering an unrelated third endpoint must not perturb it.
+ * fabric_ep_id must be unique among a fabric's members (peer descriptors
+ * resolve against it); orphan ids do not resolve and may collide.
  */
-static void drm_fabric_test_remote_peer_retained(struct kunit *test)
+static void drm_fabric_test_ep_id_unique(struct kunit *test)
 {
-	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct device *dev = fabrictest_alloc_dev(test);
 	struct drm_fabric_desc fdesc = {
-		.type = DRM_FABRIC_TYPE_SYNTHETIC,
-		.name = "test-remote",
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "test-epid",
 	};
 	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
-	struct drm_fabric_endpoint_desc eadesc = {
-		.fabric_ep_id = 0xA0,
-		.name = "remote-a",
-		.parent = fabrictest_dev,
-		.ports = &pdesc,
-		.num_ports = 1,
-	};
-	struct drm_fabric_endpoint_desc ecdesc = {
-		.fabric_ep_id = 0xC0,
-		.name = "remote-c",
-		.parent = fabrictest_dev,
-		.ports = &pdesc,
-		.num_ports = 1,
-	};
-	/* 0xBEEF has no local endpoint object. */
-	struct drm_fabric_peer remote = {
-		.peer_id = 0xBEEF, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	struct drm_fabric_endpoint_desc edesc = {
+		.name = "epid", .parent = dev, .ops = &fabrictest_mut_ops,
+		.ports = &pdesc, .num_ports = 1,
 	};
+	struct drm_fabric_endpoint_change attach;
+	struct drm_fabric_endpoint *ep_a, *ep_dup, *orphan_a, *orphan_b, *orphan_c;
 	struct drm_fabric *fab;
-	struct drm_fabric_endpoint *ep_a, *ep_c;
-	struct drm_fabric_port *pa;
 
 	fab = drm_fabric_register(&fdesc);
 	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
 
-	ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+	edesc.fabric_ep_id = 0x42;
+	ep_a = drm_fabric_endpoint_register(fab, &edesc);
 	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
 
-	pa = fabrictest_port(ep_a, 0);
-	KUNIT_ASSERT_NOT_NULL(test, pa);
+	/* A second member reusing that id is rejected. */
+	edesc.fabric_ep_id = 0x42;
+	ep_dup = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(ep_dup));
+	KUNIT_EXPECT_EQ(test, PTR_ERR(ep_dup), -EEXIST);
 
-	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &remote), 0);
-	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+	edesc.fabric_ep_id = 0x43;
+	ep_dup = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_dup));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_dup));
 
-	ep_c = drm_fabric_endpoint_register(fab, &ecdesc);
-	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_c));
+	/* Orphans do not resolve peers, so two may share an id. */
+	edesc.fabric_ep_id = 0x42;
+	orphan_a = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(orphan_a));
 	KUNIT_ASSERT_EQ(test, 0,
-			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_c));
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, orphan_a));
 
-	kunit_release_action(test, fabrictest_unregister_endpoint, ep_c);
+	edesc.fabric_ep_id = 0x42;
+	orphan_b = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(orphan_b));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, orphan_b));
 
-	KUNIT_EXPECT_TRUE(test, pa->has_peer);
-	KUNIT_EXPECT_MEMEQ(test, &pa->peer, &remote, sizeof(pa->peer));
+	/* Attaching an orphan whose id collides with a member is rejected. */
+	attach = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = fab->id,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(orphan_a, &attach), -EEXIST);
+	KUNIT_EXPECT_NULL(test, orphan_a->fabric);
+
+	edesc.fabric_ep_id = 0x44;
+	orphan_c = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(orphan_c));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, orphan_c));
+
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(orphan_c, &attach), 0);
+	KUNIT_EXPECT_PTR_EQ(test, orphan_c->fabric, fab);
 }
 
-/*
- * Removing an endpoint with multiple peered ports must bump the topology
- * generation exactly once, not once per port torn down.
- */
-static void drm_fabric_test_subtree_delete_single_bump(struct kunit *test)
+static void drm_fabric_test_port_admin_peer(struct kunit *test)
 {
 	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
 	struct drm_fabric_desc fdesc = {
 		.type = DRM_FABRIC_TYPE_SYNTHETIC,
-		.name = "test-subtree",
+		.name = "test-portadmin",
 	};
-	struct drm_fabric_port_desc pdescs[3] = {
-		{ .index = 0, .max_lane_count = 4 },
-		{ .index = 1, .max_lane_count = 4 },
-		{ .index = 2, .max_lane_count = 4 },
+	/* userspace-managed so this test can drive the PORT_PEER_NEW path. */
+	struct drm_fabric_port_desc pdesc = {
+		.index = 0, .max_lane_count = 4,
+		.peer_mode = DRM_FABRIC_PEER_MODE_USERSPACE,
 	};
 	struct drm_fabric_endpoint_desc edesc = {
-		.fabric_ep_id = 0xD0,
-		.name = "subtree-ep",
+		.fabric_ep_id = 0x77,
 		.parent = fabrictest_dev,
-		.ports = pdescs,
-		.num_ports = 3,
+		.ops = &fabrictest_mut_ops,
+		.ports = &pdesc,
+		.num_ports = 1,
 	};
 	struct drm_fabric_peer peer = {
-		.peer_id = 0xD1, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+		.peer_id = 0x88,
+		.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+		.port_index = 2,
 	};
 	struct drm_fabric *fab;
 	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+	u32 seq;
 
 	fab = drm_fabric_register(&fdesc);
 	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
@@ -1006,13 +1152,1173 @@ static void drm_fabric_test_subtree_delete_single_bump(struct kunit *test)
 	KUNIT_ASSERT_EQ(test, 0,
 			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
 
-	/* Two of the three ports carry a half-edge. */
-	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 0), &peer), 0);
-	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 1), &peer), 0);
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
 
-	fabrictest_seed_seq(test, 100);
-	kunit_release_action(test, fabrictest_unregister_endpoint, ep);
-	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), 101);
+	seq = drm_fabric_base_seq;
+	KUNIT_EXPECT_EQ(test, fabrictest_port_admin_locked(port, DRM_FABRIC_ADMIN_STATE_UP), 0);
+	KUNIT_EXPECT_EQ(test, port->admin_state, DRM_FABRIC_ADMIN_STATE_UP);
+	KUNIT_EXPECT_NE(test, drm_fabric_base_seq, seq);
+
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+	seq = drm_fabric_base_seq;
+	KUNIT_EXPECT_EQ(test, fabrictest_port_admin_locked(port, DRM_FABRIC_ADMIN_STATE_DOWN), 0);
+	KUNIT_EXPECT_EQ(test, port->admin_state, DRM_FABRIC_ADMIN_STATE_DOWN);
+	KUNIT_EXPECT_NE(test, drm_fabric_base_seq, seq);
+	/* Admin and operational state are independent; admin-down preserves oper. */
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+	seq = drm_fabric_base_seq;
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_new_locked(port, &peer), 0);
+	KUNIT_EXPECT_TRUE(test, port->has_peer);
+	KUNIT_EXPECT_EQ(test, port->peer.peer_id, 0x88ULL);
+	KUNIT_EXPECT_NE(test, drm_fabric_base_seq, seq);
+
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_new_locked(port, &peer), -EEXIST);
+
+	seq = drm_fabric_base_seq;
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_del_locked(port), 0);
+	KUNIT_EXPECT_FALSE(test, port->has_peer);
+	KUNIT_EXPECT_NE(test, drm_fabric_base_seq, seq);
+
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_del_locked(port), -ENOENT);
+}
+
+/*
+ * PROVIDER ports take peers only from drm_fabric_port_set_peer() (kernel side);
+ * USERSPACE ports take peers only through the locked PEER_NEW/DEL mutators.
+ * Each rejects the other's path with -EOPNOTSUPP.
+ */
+static void drm_fabric_test_peer_mode(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-peermode",
+	};
+	struct drm_fabric_port_desc pdescs[2] = {
+		{ .index = 0, .max_lane_count = 4,
+		  .peer_mode = DRM_FABRIC_PEER_MODE_PROVIDER },
+		{ .index = 1, .max_lane_count = 4,
+		  .peer_mode = DRM_FABRIC_PEER_MODE_USERSPACE },
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x91,
+		.parent = fabrictest_dev,
+		.ops = &fabrictest_mut_ops,
+		.ports = pdescs,
+		.num_ports = 2,
+	};
+	struct drm_fabric_peer peer = {
+		.peer_id = 0xA1,
+		.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+		.port_index = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *pport, *uport;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	pport = fabrictest_port(ep, 0);
+	uport = fabrictest_port(ep, 1);
+	KUNIT_ASSERT_NOT_NULL(test, pport);
+	KUNIT_ASSERT_NOT_NULL(test, uport);
+
+	/* Provider-managed port: the provider programs it; the user path is refused. */
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(pport, &peer), 0);
+	KUNIT_EXPECT_TRUE(test, pport->has_peer);
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_new_locked(pport, &peer), -EOPNOTSUPP);
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_del_locked(pport), -EOPNOTSUPP);
+	/* The refused user calls leave the provider's peer intact. */
+	KUNIT_EXPECT_TRUE(test, pport->has_peer);
+	/* Same-source duplicate/absent errors are preserved. */
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(pport, &peer), -EEXIST);
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_unset_peer(pport), 0);
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_unset_peer(pport), -ENOENT);
+
+	/* Userspace-managed port: the user path programs it; the provider is refused. */
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_new_locked(uport, &peer), 0);
+	KUNIT_EXPECT_TRUE(test, uport->has_peer);
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(uport, &peer), -EOPNOTSUPP);
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_unset_peer(uport), -EOPNOTSUPP);
+	/* The refused provider calls leave the userspace peer intact. */
+	KUNIT_EXPECT_TRUE(test, uport->has_peer);
+	KUNIT_EXPECT_EQ(test, uport->peer.peer_id, 0xA1ULL);
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_new_locked(uport, &peer), -EEXIST);
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_del_locked(uport), 0);
+	KUNIT_EXPECT_EQ(test, fabrictest_port_peer_del_locked(uport), -ENOENT);
+}
+
+/*
+ * Model A reports operational state from the provisioning callback.
+ * drm_fabric_lock must be dropped across the callback to avoid recursion.
+ */
+struct fabrictest_model_a_ctx {
+	unsigned int	calls;
+	bool		mutation_lock_held;
+	bool		fabric_lock_held;
+};
+
+static int fabrictest_model_a_port_set(struct drm_fabric_port *port,
+				       enum drm_fabric_admin_state admin)
+{
+	struct fabrictest_model_a_ctx *ctx = port->endpoint->priv;
+
+	ctx->calls++;
+#ifdef CONFIG_LOCKDEP
+	ctx->mutation_lock_held = lockdep_is_held(&drm_fabric_mutation_lock);
+	ctx->fabric_lock_held = lockdep_is_held(&drm_fabric_lock);
+#endif
+
+	lockdep_assert_held(&drm_fabric_mutation_lock);
+	lockdep_assert_not_held(&drm_fabric_lock);
+
+	/* Takes drm_fabric_lock: a core that had not dropped it would deadlock here. */
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+	return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_model_a_ops = {
+	.port_set = fabrictest_model_a_port_set,
+};
+
+static void drm_fabric_test_model_a_oper_report(struct kunit *test)
+{
+	struct device *dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "test-modela",
+	};
+	struct drm_fabric_port_desc pdesc = {
+		.index = 0, .max_lane_count = 4,
+		.peer_mode = DRM_FABRIC_PEER_MODE_USERSPACE,
+	};
+	struct fabrictest_model_a_ctx ctx = {};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x5a,
+		.parent = dev,
+		.ops = &fabrictest_model_a_ops,
+		.priv = &ctx,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+	u32 seq;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	KUNIT_ASSERT_NE(test, port->admin_state, DRM_FABRIC_ADMIN_STATE_UP);
+	KUNIT_ASSERT_NE(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+	seq = drm_fabric_base_seq;
+
+	KUNIT_EXPECT_EQ(test,
+			fabrictest_port_admin_locked(port, DRM_FABRIC_ADMIN_STATE_UP), 0);
+
+	KUNIT_EXPECT_EQ(test, ctx.calls, 1u);
+	if (IS_ENABLED(CONFIG_LOCKDEP)) {
+		KUNIT_EXPECT_TRUE(test, ctx.mutation_lock_held);
+		KUNIT_EXPECT_FALSE(test, ctx.fabric_lock_held);
+	}
+
+	/*
+	 * The synchronous oper report committed inside the callback and the
+	 * administrative state committed after it, with no recursive deadlock.
+	 */
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_EQ(test, port->admin_state, DRM_FABRIC_ADMIN_STATE_UP);
+
+	KUNIT_EXPECT_NE(test, drm_fabric_base_seq, seq);
+
+	/*
+	 * Repeating the same admin state is a no-op: no second provider call,
+	 * no second seq bump.
+	 */
+	seq = drm_fabric_base_seq;
+	KUNIT_EXPECT_EQ(test,
+			fabrictest_port_admin_locked(port, DRM_FABRIC_ADMIN_STATE_UP), 0);
+	KUNIT_EXPECT_EQ(test, ctx.calls, 1u);
+	KUNIT_EXPECT_EQ(test, drm_fabric_base_seq, seq);
+}
+
+struct fabrictest_unreg_race {
+	struct drm_fabric_endpoint *ep;
+	struct completion started;
+	struct completion finished;
+};
+
+static int fabrictest_unreg_thread(void *arg)
+{
+	struct fabrictest_unreg_race *r = arg;
+
+	complete(&r->started);
+	drm_fabric_endpoint_unregister(r->ep);
+	complete(&r->finished);
+
+	/* Stay alive until the test reaps us, so kthread_stop() is valid. */
+	while (!kthread_should_stop())
+		schedule_timeout_interruptible(msecs_to_jiffies(10));
+	return 0;
+}
+
+/*
+ * drm_fabric_endpoint_unregister() must take mutation_lock itself, so it cannot
+ * race a concurrent mutator: it blocks until the lock is free.
+ */
+static void drm_fabric_test_unregister_serializes_mutation(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-unreg",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x99,
+		.parent = fabrictest_dev,
+		.ops = &fabrictest_mut_ops,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct fabrictest_unreg_race r;
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct task_struct *task;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	/* The worker owns this endpoint's unregister, so no kunit teardown action. */
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+
+	r.ep = ep;
+	init_completion(&r.started);
+	init_completion(&r.finished);
+
+	mutex_lock(&drm_fabric_mutation_lock);
+
+	task = kthread_run(fabrictest_unreg_thread, &r, "fabrtest-unreg");
+	if (IS_ERR(task)) {
+		/* A fatal assertion would skip cleanup; unwind before failing. */
+		mutex_unlock(&drm_fabric_mutation_lock);
+		drm_fabric_endpoint_unregister(ep);
+		KUNIT_FAIL(test, "kthread_run failed: %pe", task);
+		return;
+	}
+
+	KUNIT_EXPECT_GT(test,
+			wait_for_completion_timeout(&r.started, msecs_to_jiffies(1000)),
+			0);
+	msleep(50);
+
+	/* Racer entered unregister() but is still stuck waiting for the lock. */
+	KUNIT_EXPECT_FALSE(test, try_wait_for_completion(&r.finished));
+
+	/* Release => unregister proceeds and must finish promptly. */
+	mutex_unlock(&drm_fabric_mutation_lock);
+	KUNIT_EXPECT_GT(test,
+			wait_for_completion_timeout(&r.finished, msecs_to_jiffies(5000)),
+			0);
+
+	kthread_stop(task);
+}
+
+static void fabrictest_stop_thread(void *t)
+{
+	kthread_stop(t);
+}
+
+/*
+ * Attach and endpoint registration compete for one fabric_ep_id.
+ * mutation_lock makes registration wait, then fail with -EEXIST.
+ */
+struct fabrictest_l4 {
+	struct completion cb_entered;
+	struct completion cb_release;
+	struct completion attach_done;
+	struct completion reg_done;
+	int attach_ret;
+	struct drm_fabric_endpoint *reg_ep;
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *orphan;
+	struct device *dev;
+	u64 ep_id;
+};
+
+/*
+ * Stalls inside the provider callback (mutation_lock held) so a second thread
+ * can be started and observed to block on the same fabric_ep_id.
+ */
+static int fabrictest_l4_endpoint_set(struct drm_fabric_endpoint *ep,
+				      const struct drm_fabric_endpoint_change *change,
+				      struct drm_fabric *fabric)
+{
+	struct fabrictest_l4 *l4 = ep->priv;
+
+	complete(&l4->cb_entered);
+	/* Bounded so a test abort can never wedge teardown on this thread. */
+	wait_for_completion_timeout(&l4->cb_release, msecs_to_jiffies(10000));
+	return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_l4_ops = {
+	.endpoint_set = fabrictest_l4_endpoint_set,
+};
+
+static int fabrictest_l4_attach_thread(void *arg)
+{
+	struct fabrictest_l4 *l4 = arg;
+	struct drm_fabric_endpoint_change attach = {
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC,
+		.fabric_id = l4->fab->id,
+	};
+
+	l4->attach_ret = fabrictest_ep_set_locked(l4->orphan, &attach);
+	complete(&l4->attach_done);
+
+	while (!kthread_should_stop())
+		schedule_timeout_interruptible(msecs_to_jiffies(10));
+	return 0;
+}
+
+static int fabrictest_l4_register_thread(void *arg)
+{
+	struct fabrictest_l4 *l4 = arg;
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = l4->ep_id,
+		.name = "l4-b",
+		.parent = l4->dev,
+		.ops = &fabrictest_l4_ops,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+
+	l4->reg_ep = drm_fabric_endpoint_register(l4->fab, &edesc);
+	complete(&l4->reg_done);
+
+	while (!kthread_should_stop())
+		schedule_timeout_interruptible(msecs_to_jiffies(10));
+	return 0;
+}
+
+static void drm_fabric_test_attach_register_collision(struct kunit *test)
+{
+	struct device *dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "test-l4",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc;
+	struct fabrictest_l4 *l4;
+	struct task_struct *t1, *t2;
+
+	l4 = kunit_kzalloc(test, sizeof(*l4), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, l4);
+	init_completion(&l4->cb_entered);
+	init_completion(&l4->cb_release);
+	init_completion(&l4->attach_done);
+	init_completion(&l4->reg_done);
+	l4->dev = dev;
+	l4->ep_id = 0x4242;
+
+	l4->fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(l4->fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, l4->fab));
+
+	/* Orphan A with fabric_ep_id X and a blocking endpoint_set callback. */
+	edesc = (struct drm_fabric_endpoint_desc){
+		.fabric_ep_id = l4->ep_id,
+		.name = "l4-a",
+		.parent = dev,
+		.ops = &fabrictest_l4_ops,
+		.priv = l4,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	l4->orphan = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(l4->orphan));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test,
+						  fabrictest_unregister_endpoint,
+						  l4->orphan));
+
+	/*
+	 * T1 attaches A -> F and blocks inside the provider callback while it
+	 * holds drm_fabric_mutation_lock.
+	 */
+	t1 = kthread_run(fabrictest_l4_attach_thread, l4, "fabrtest-l4-a");
+	KUNIT_ASSERT_FALSE(test, IS_ERR(t1));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_stop_thread, t1));
+	KUNIT_ASSERT_GT(test,
+			wait_for_completion_timeout(&l4->cb_entered, msecs_to_jiffies(5000)),
+			0);
+
+	/* T2 races to register B with the same id directly into F. */
+	t2 = kthread_run(fabrictest_l4_register_thread, l4, "fabrtest-l4-b");
+	KUNIT_ASSERT_FALSE(test, IS_ERR(t2));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_stop_thread, t2));
+
+	/* Register thread is queued behind the stalled attach, not finished. */
+	msleep(50);
+	KUNIT_EXPECT_FALSE(test, try_wait_for_completion(&l4->reg_done));
+
+	/* Release A's callback: the attach commits and claims X. */
+	complete(&l4->cb_release);
+	KUNIT_EXPECT_GT(test,
+			wait_for_completion_timeout(&l4->attach_done, msecs_to_jiffies(5000)),
+			0);
+	KUNIT_EXPECT_EQ(test, l4->attach_ret, 0);
+	KUNIT_EXPECT_PTR_EQ(test, l4->orphan->fabric, l4->fab);
+
+	/* B then proceeds and must fail: X is now owned by A. */
+	KUNIT_EXPECT_GT(test,
+			wait_for_completion_timeout(&l4->reg_done, msecs_to_jiffies(5000)),
+			0);
+	if (!IS_ERR(l4->reg_ep)) {
+		KUNIT_ASSERT_EQ(test, 0,
+				kunit_add_action_or_reset(test,
+							  fabrictest_unregister_endpoint,
+							  l4->reg_ep));
+		KUNIT_FAIL(test, "racing registration unexpectedly succeeded");
+		return;
+	}
+
+	KUNIT_EXPECT_EQ(test, PTR_ERR(l4->reg_ep), -EEXIST);
+}
+
+static int fabrictest_stats_get(struct drm_fabric_port *port,
+				struct drm_fabric_port_stats *stats)
+{
+	/* The statistics callback may sleep and runs without either fabric lock. */
+	lockdep_assert_not_held(&drm_fabric_lock);
+	lockdep_assert_not_held(&drm_fabric_mutation_lock);
+
+	stats->read_bytes = 4096;
+	stats->write_bytes = 2048;
+	stats->link_down_count = 2;
+	stats->retrain_count = 3;
+	return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_stats_ops = {
+	.port_stats_get = fabrictest_stats_get,
+};
+
+/* This does not exercise netlink dispatch or error propagation. */
+static void drm_fabric_test_port_stats_ops_registration(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-stats",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x5A,
+		.parent = fabrictest_dev,
+		.ops = &fabrictest_stats_ops,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_desc edesc_noops = {
+		.fabric_ep_id = 0x5B,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_port_stats stats = {};
+	struct drm_fabric_endpoint *ep, *ep_noops;
+	struct drm_fabric_port *port;
+	struct drm_fabric *fab;
+	u32 gen;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	KUNIT_ASSERT_NOT_NULL(test, ep->ops);
+	KUNIT_ASSERT_NOT_NULL(test, ep->ops->port_stats_get);
+
+	/* A stats read is not a topology change: seq must not move. */
+	gen = fabrictest_seq_read();
+	KUNIT_EXPECT_EQ(test, ep->ops->port_stats_get(port, &stats), 0);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), gen);
+	KUNIT_EXPECT_EQ(test, stats.read_bytes, 4096ULL);
+	KUNIT_EXPECT_EQ(test, stats.write_bytes, 2048ULL);
+	KUNIT_EXPECT_EQ(test, stats.link_down_count, 2ULL);
+	KUNIT_EXPECT_EQ(test, stats.retrain_count, 3ULL);
+
+	ep_noops = drm_fabric_endpoint_register(fab, &edesc_noops);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_noops));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_noops));
+	KUNIT_EXPECT_TRUE(test, !ep_noops->ops || !ep_noops->ops->port_stats_get);
+}
+
+static void drm_fabric_test_user_fabric_new_del(struct kunit *test)
+{
+	bool refcount_observable = fabrictest_module_refcount_observable();
+	int baseline = refcount_observable ? fabrictest_module_refcount() : 0;
+	struct drm_fabric *fab;
+	u32 fid = 0;
+	int ret;
+
+	ret = fabrictest_user_fabric_new_locked(DRM_FABRIC_TYPE_SYNTHETIC, 0x1234, "vpod0", &fid);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_NE(test, fid, 0);
+	if (ret || !fid)
+		return;
+
+	fab = fabrictest_find_fabric(fid);
+	KUNIT_EXPECT_NOT_NULL(test, fab);
+	if (fab)
+		KUNIT_EXPECT_EQ(test, fab->type, DRM_FABRIC_TYPE_SYNTHETIC);
+
+	/* FD-01: publishing a userspace fabric must pin the module. */
+	if (refcount_observable)
+		KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline + 1);
+
+	KUNIT_EXPECT_EQ(test, fabrictest_user_fabric_del_locked(fid), 0);
+
+	fab = fabrictest_find_fabric(fid);
+	KUNIT_EXPECT_NULL(test, fab);
+
+	/* FD-01: removing it must release that pin again. */
+	if (refcount_observable)
+		KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline);
+}
+
+/*
+ * Keyed by id, not pointer: after deletion this returns -ENOENT instead of
+ * touching freed memory.
+ */
+static void fabrictest_user_fabric_del(void *p)
+{
+	fabrictest_user_fabric_del_locked(*(u32 *)p);
+}
+
+/*
+ * FD-01: a rejected FABRIC_NEW must not publish a fabric or leak a module
+ * reference. Invalid type is refused before try_module_get(); a duplicate
+ * (type, instance_id) is refused after it, so only that path tests module_put().
+ */
+static void drm_fabric_test_user_fabric_new_reject_no_module_ref(struct kunit *test)
+{
+	bool refcount_observable = fabrictest_module_refcount_observable();
+	int baseline = refcount_observable ? fabrictest_module_refcount() : 0;
+	u32 *fid = kunit_kzalloc(test, sizeof(*fid), GFP_KERNEL);
+	u32 dup_fid = 0;
+	int ret;
+
+	KUNIT_ASSERT_NOT_NULL(test, fid);
+
+	ret = fabrictest_user_fabric_new_locked((enum drm_fabric_type)0, 0xa1a1,
+						"test-new-invalid", NULL);
+	KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+	if (refcount_observable)
+		KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline);
+
+	ret = fabrictest_user_fabric_new_locked(DRM_FABRIC_TYPE_SYNTHETIC, 0xa2a2,
+						"test-new-dup", fid);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_user_fabric_del, fid));
+
+	ret = fabrictest_user_fabric_new_locked(DRM_FABRIC_TYPE_SYNTHETIC, 0xa2a2,
+						"test-new-dup2", &dup_fid);
+	KUNIT_EXPECT_EQ(test, ret, -EEXIST);
+	KUNIT_EXPECT_EQ(test, dup_fid, 0);
+	KUNIT_EXPECT_NOT_NULL(test, fabrictest_find_fabric(*fid));
+
+	/* Only the first, successful registration should still be pinning us. */
+	if (refcount_observable)
+		KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline + 1);
+}
+
+/*
+ * Sweep of the endpoint_set()/user_fabric_del() error paths: no-op change,
+ * nonexistent fabric, already-attached, and provider-vs-user ownership.
+ */
+static void drm_fabric_test_reject_paths(struct kunit *test)
+{
+	bool refcount_observable = fabrictest_module_refcount_observable();
+	int baseline = refcount_observable ? fabrictest_module_refcount() : 0;
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	/* Distinct instance_ids: this case needs two live fabrics, not a
+	 * uniqueness collision (which (type, instance_id) equality would now
+	 * trigger -- including for instance_id 0).
+	 */
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-reject",
+		.instance_id = 0x2001,
+	};
+	struct drm_fabric_desc fdesc2 = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-reject2",
+		.instance_id = 0x2002,
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x99,
+		.name = "reject-ep",
+		.parent = fabrictest_dev,
+		.ops = &fabrictest_mut_ops,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_change change;
+	struct drm_fabric *fab, *fab2;
+	struct drm_fabric_endpoint *ep;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	fab2 = drm_fabric_register(&fdesc2);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab2));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab2));
+
+	ep = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	/* An empty change request is a core no-op; netlink maps no-attrs to -EINVAL. */
+	change = (struct drm_fabric_endpoint_change){ .valid = 0 };
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = 0x7fffffff,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), -ENOENT);
+
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = fab->id,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), 0);
+	KUNIT_EXPECT_PTR_EQ(test, ep->fabric, fab);
+
+	change = (struct drm_fabric_endpoint_change){
+		.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = fab2->id,
+	};
+	KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &change), -EBUSY);
+	KUNIT_EXPECT_PTR_EQ(test, ep->fabric, fab);
+
+	/* Full FABRIC_DEL ownership/emptiness matrix. */
+
+	/* Unknown id: not found, before any ownership or emptiness check. */
+	KUNIT_EXPECT_EQ(test, fabrictest_user_fabric_del_locked(0x7fffffff), -ENOENT);
+
+	/*
+	 * Provider-owned fabrics are refused with -EPERM whether empty (fab2) or
+	 * non-empty (fab holds @ep): a provider keeps sole ownership of its
+	 * fabric's lifetime, and -EPERM is checked before the -EBUSY emptiness
+	 * test.
+	 */
+	KUNIT_EXPECT_EQ(test, fabrictest_user_fabric_del_locked(fab->id), -EPERM);
+	KUNIT_EXPECT_EQ(test, fabrictest_user_fabric_del_locked(fab2->id), -EPERM);
+
+	/*
+	 * FD-01: provider-owned fabrics never took a module reference, and a
+	 * rejected delete must not touch either the object or a reference.
+	 */
+	KUNIT_EXPECT_PTR_EQ(test, fabrictest_find_fabric(fab->id), fab);
+	KUNIT_EXPECT_PTR_EQ(test, fabrictest_find_fabric(fab2->id), fab2);
+	if (refcount_observable)
+		KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline);
+
+	/*
+	 * Userspace-owned fabrics: non-empty is -EBUSY, empty is deletable.
+	 * Reuse @ep (moved out of @fab) to make the userspace fabric non-empty.
+	 */
+	{
+		struct drm_fabric_endpoint_change detach = {
+			.valid = DRM_FABRIC_EP_CHANGE_FABRIC, .fabric_id = 0,
+		};
+		struct drm_fabric_endpoint_change attach = {
+			.valid = DRM_FABRIC_EP_CHANGE_FABRIC,
+		};
+		u32 *uid = kunit_kzalloc(test, sizeof(*uid), GFP_KERNEL);
+		int ret;
+
+		KUNIT_ASSERT_NOT_NULL(test, uid);
+		ret = fabrictest_user_fabric_new_locked(DRM_FABRIC_TYPE_SYNTHETIC,
+							0x2003, "test-user-del",
+							uid);
+		KUNIT_ASSERT_EQ(test, ret, 0);
+		KUNIT_ASSERT_EQ(test, 0,
+				kunit_add_action_or_reset(test,
+							  fabrictest_user_fabric_del,
+							  uid));
+		if (refcount_observable)
+			KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline + 1);
+
+		KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &detach), 0);
+		attach.fabric_id = *uid;
+		KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &attach), 0);
+
+		KUNIT_EXPECT_EQ(test, fabrictest_user_fabric_del_locked(*uid), -EBUSY);
+
+		/*
+		 * FD-01: the -EBUSY rejection must leave the fabric resolvable
+		 * and its module reference held, exactly as before the attempt.
+		 */
+		KUNIT_EXPECT_NOT_NULL(test, fabrictest_find_fabric(*uid));
+		if (refcount_observable)
+			KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline + 1);
+
+		KUNIT_EXPECT_EQ(test, fabrictest_ep_set_locked(ep, &detach), 0);
+		KUNIT_EXPECT_EQ(test, fabrictest_user_fabric_del_locked(*uid), 0);
+
+		/* FD-01: a successful delete drops both the object and the pin. */
+		KUNIT_EXPECT_NULL(test, fabrictest_find_fabric(*uid));
+		if (refcount_observable)
+			KUNIT_EXPECT_EQ(test, fabrictest_module_refcount(), baseline);
+	}
+}
+
+/*
+ * Unregistering an endpoint that has a peer link must clear only that
+ * endpoint's own port record; it must not touch the still-registered far
+ * side's peer record. (Contrast with drm_fabric_port_unset_peer(), which
+ * clears a peer explicitly and is covered separately.)
+ */
+static void drm_fabric_test_local_unplug_keeps_edge(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-unplug",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc eadesc = {
+		.fabric_ep_id = 0xA0,
+		.name = "unplug-a",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_desc ebdesc = {
+		.fabric_ep_id = 0xB0,
+		.name = "unplug-b",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_peer to_b = {
+		.peer_id = 0xB0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric_peer to_a = {
+		.peer_id = 0xA0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep_a, *ep_b;
+	struct drm_fabric_port *pa, *pb;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
+
+	ep_b = drm_fabric_endpoint_register(fab, &ebdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_b));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_b));
+
+	pa = fabrictest_port(ep_a, 0);
+	pb = fabrictest_port(ep_b, 0);
+	KUNIT_ASSERT_NOT_NULL(test, pa);
+	KUNIT_ASSERT_NOT_NULL(test, pb);
+
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &to_b), 0);
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pb, &to_a), 0);
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+
+	/*
+	 * Remove B without retracting its peer first, modelling abrupt provider
+	 * teardown.
+	 */
+	kunit_release_action(test, fabrictest_unregister_endpoint, ep_b);
+
+	/* The surviving half-edge must be byte-unchanged: no field mutated. */
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+	KUNIT_EXPECT_MEMEQ(test, &pa->peer, &to_b, sizeof(pa->peer));
+}
+
+/*
+ * A's peer record names a port index, not an object; registering and then
+ * unregistering an unrelated third endpoint must not perturb it.
+ */
+static void drm_fabric_test_remote_peer_retained(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-remote",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc eadesc = {
+		.fabric_ep_id = 0xA0,
+		.name = "remote-a",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_desc ecdesc = {
+		.fabric_ep_id = 0xC0,
+		.name = "remote-c",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	/* 0xBEEF has no local endpoint object. */
+	struct drm_fabric_peer remote = {
+		.peer_id = 0xBEEF, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep_a, *ep_c;
+	struct drm_fabric_port *pa;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
+
+	pa = fabrictest_port(ep_a, 0);
+	KUNIT_ASSERT_NOT_NULL(test, pa);
+
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &remote), 0);
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+
+	ep_c = drm_fabric_endpoint_register(fab, &ecdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_c));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_c));
+
+	kunit_release_action(test, fabrictest_unregister_endpoint, ep_c);
+
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+	KUNIT_EXPECT_MEMEQ(test, &pa->peer, &remote, sizeof(pa->peer));
+}
+
+/*
+ * Removing an endpoint with multiple peered ports must bump the topology
+ * generation exactly once, not once per port torn down.
+ */
+static void drm_fabric_test_subtree_delete_single_bump(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-subtree",
+	};
+	struct drm_fabric_port_desc pdescs[3] = {
+		{ .index = 0, .max_lane_count = 4 },
+		{ .index = 1, .max_lane_count = 4 },
+		{ .index = 2, .max_lane_count = 4 },
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0xD0,
+		.name = "subtree-ep",
+		.parent = fabrictest_dev,
+		.ports = pdescs,
+		.num_ports = 3,
+	};
+	struct drm_fabric_peer peer = {
+		.peer_id = 0xD1, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	/* Two of the three ports carry a half-edge. */
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 0), &peer), 0);
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 1), &peer), 0);
+
+	fabrictest_seed_seq(test, 100);
+	kunit_release_action(test, fabrictest_unregister_endpoint, ep);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), 101);
+}
+
+#define FABRICTEST_CONC_THREADS	4
+#define FABRICTEST_CONC_ITERS	200
+
+struct fabrictest_conc_ctx {
+	/* Sampled *inside* the provider hook (under the core lock). */
+	atomic_t	in_flight;
+	atomic_t	max_in_flight;
+	atomic_t	calls;
+	/* Sampled *around* the core mutation call (incl. lock wait). */
+	atomic_t	contenders;
+	atomic_t	max_contenders;
+	atomic_t	started;
+	atomic_t	done;
+	int		nthreads;
+};
+
+/* Lock-free running maximum; cmpxchg retries until the value only grows. */
+static void fabrictest_bump_max(atomic_t *max, int cur)
+{
+	int old = atomic_read(max);
+
+	while (cur > old)
+		old = atomic_cmpxchg(max, old, cur);
+}
+
+/* Record concurrent callback entry, then sleep to widen the overlap window. */
+static void fabrictest_conc_enter(struct fabrictest_conc_ctx *ctx)
+{
+	fabrictest_bump_max(&ctx->max_in_flight,
+			    atomic_inc_return(&ctx->in_flight));
+	atomic_inc(&ctx->calls);
+	usleep_range(20, 60);
+	atomic_dec(&ctx->in_flight);
+}
+
+static int fabrictest_mock_port_set(struct drm_fabric_port *port,
+				    enum drm_fabric_admin_state admin)
+{
+	fabrictest_conc_enter(port->endpoint->priv);
+	return 0;
+}
+
+static int fabrictest_mock_endpoint_set(struct drm_fabric_endpoint *ep,
+					const struct drm_fabric_endpoint_change *change,
+					struct drm_fabric *fabric)
+{
+	fabrictest_conc_enter(ep->priv);
+	return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_conc_ops = {
+	.port_set = fabrictest_mock_port_set,
+	.endpoint_set = fabrictest_mock_endpoint_set,
+};
+
+struct fabrictest_worker {
+	struct drm_fabric_endpoint	*ep;
+	struct drm_fabric_port		*port;
+	struct fabrictest_conc_ctx	*ctx;
+	int				kind;	/* 0: PORT_SET, 1: ENDPOINT_SET */
+	int				iters;
+};
+
+/*
+ * All worker state lives in one kunit-managed allocation so the kthreads never
+ * dereference the test function's stack.  Combined with the per-thread stop
+ * action below, an assert-abort during spawn can still reap every worker before
+ * its backing memory (and the endpoint it touches) is torn down.
+ */
+struct fabrictest_conc_harness {
+	struct fabrictest_conc_ctx	ctx;
+	struct fabrictest_worker	workers[FABRICTEST_CONC_THREADS];
+	struct task_struct		*threads[FABRICTEST_CONC_THREADS];
+};
+
+static int fabrictest_mutator(void *arg)
+{
+	struct fabrictest_worker *w = arg;
+	struct fabrictest_conc_ctx *ctx = w->ctx;
+	unsigned long deadline;
+	int i;
+
+	/*
+	 * Barrier: don't start hammering until every worker is up, so the
+	 * contention window is as wide as possible.
+	 */
+	atomic_inc(&ctx->started);
+	deadline = jiffies + msecs_to_jiffies(1000);
+	while (atomic_read(&ctx->started) < ctx->nthreads &&
+	       time_before(jiffies, deadline))
+		cond_resched();
+
+	for (i = 0; i < w->iters; i++) {
+		enum drm_fabric_admin_state admin =
+			(i & 1) ? DRM_FABRIC_ADMIN_STATE_UP : DRM_FABRIC_ADMIN_STATE_DOWN;
+
+		/*
+		 * Count threads in/awaiting the mutator (the locked wrapper
+		 * blocks on drm_fabric_mutation_lock if another worker holds it),
+		 * so the test can prove real contention happened rather than
+		 * passing vacuously.
+		 */
+		fabrictest_bump_max(&ctx->max_contenders,
+				    atomic_inc_return(&ctx->contenders));
+		if (w->kind == 0) {
+			fabrictest_port_admin_locked(w->port, admin);
+		} else {
+			struct drm_fabric_endpoint_change change = {
+				.valid = DRM_FABRIC_EP_CHANGE_ADMIN,
+				.admin = admin,
+			};
+
+			fabrictest_ep_set_locked(w->ep, &change);
+		}
+		atomic_dec(&ctx->contenders);
+		cond_resched();
+	}
+
+	atomic_inc(&ctx->done);
+
+	/* Idle until the test reaps us so the threadfn never exits early. */
+	while (!kthread_should_stop())
+		schedule_timeout_interruptible(msecs_to_jiffies(2));
+
+	return 0;
+}
+
+/*
+ * FABRICTEST_CONC_THREADS racers alternate port-admin and endpoint-admin
+ * mutators; mutation_lock must serialize them into the provider hook.
+ */
+static void drm_fabric_test_concurrent_mutation(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-conc",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct fabrictest_conc_harness *h;
+	struct drm_fabric_endpoint_desc edesc;
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+	unsigned long deadline;
+	int i;
+
+	h = kunit_kzalloc(test, sizeof(*h), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, h);
+	h->ctx.nthreads = FABRICTEST_CONC_THREADS;
+
+	edesc = (struct drm_fabric_endpoint_desc){
+		.fabric_ep_id = 0xC0,
+		.name = "conc-ep",
+		.parent = fabrictest_dev,
+		.ops = &fabrictest_conc_ops,
+		.priv = &h->ctx,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	for (i = 0; i < FABRICTEST_CONC_THREADS; i++) {
+		h->workers[i] = (struct fabrictest_worker){
+			.ep = ep,
+			.port = port,
+			.ctx = &h->ctx,
+			.kind = i & 1,
+			.iters = FABRICTEST_CONC_ITERS,
+		};
+		h->threads[i] = kthread_run(fabrictest_mutator, &h->workers[i],
+					    "fabrtest-conc/%d", i);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(h->threads[i]));
+		/* Reap this worker if a later assertion aborts the test. */
+		KUNIT_ASSERT_EQ(test, 0,
+				kunit_add_action_or_reset(test,
+							  fabrictest_stop_thread,
+							  h->threads[i]));
+	}
+
+	deadline = jiffies + msecs_to_jiffies(10000);
+	while (atomic_read(&h->ctx.done) < FABRICTEST_CONC_THREADS &&
+	       time_before(jiffies, deadline))
+		schedule_timeout_interruptible(msecs_to_jiffies(20));
+
+	/* Correctness: no deadlock / lost wakeup, every worker completed. */
+	KUNIT_EXPECT_EQ(test, atomic_read(&h->ctx.done), FABRICTEST_CONC_THREADS);
+	KUNIT_EXPECT_GT(test, atomic_read(&h->ctx.calls), 0);
+
+	/* Correctness: the object model is consistent after the storm. */
+	KUNIT_EXPECT_PTR_EQ(test, ep->fabric, fab);
+	KUNIT_EXPECT_LE(test, (int)port->admin_state, (int)DRM_FABRIC_ADMIN_STATE_UP);
+	KUNIT_EXPECT_LE(test, (int)ep->admin_state, (int)DRM_FABRIC_ADMIN_STATE_UP);
+
+	/*
+	 * Prove real contention occurred while the provider callback stayed
+	 * serialized.
+	 */
+	KUNIT_EXPECT_GE_MSG(test, atomic_read(&h->ctx.max_contenders), 2,
+			    "workers never contended; concurrency not exercised");
+
+	/*
+	 * Provider callbacks must not overlap. max_contenders >= 2 makes this
+	 * assertion non-vacuous.
+	 */
+	KUNIT_EXPECT_EQ_MSG(test, atomic_read(&h->ctx.max_in_flight), 1,
+			    "provider hooks overlapped; mutations did not serialise");
 }
 
 static void drm_fabric_test_switch_topology(struct kunit *test)
@@ -1299,10 +2605,22 @@ static struct kunit_case drm_fabric_test_cases[] = {
 	KUNIT_CASE(drm_fabric_test_port_oper_state_rejects_invalid),
 	KUNIT_CASE(drm_fabric_test_register_rejects_invalid_type),
 	KUNIT_CASE(drm_fabric_test_mesh_kn_topology),
+	KUNIT_CASE(drm_fabric_test_orphan_attach_detach),
+	KUNIT_CASE(drm_fabric_test_ep_id_unique),
+	KUNIT_CASE(drm_fabric_test_failed_mutation_no_commit),
+	KUNIT_CASE(drm_fabric_test_port_admin_peer),
+	KUNIT_CASE(drm_fabric_test_peer_mode),
+	KUNIT_CASE(drm_fabric_test_model_a_oper_report),
+	KUNIT_CASE_SLOW(drm_fabric_test_unregister_serializes_mutation),
+	KUNIT_CASE_SLOW(drm_fabric_test_attach_register_collision),
 	KUNIT_CASE(drm_fabric_test_port_stats_ops_registration),
+	KUNIT_CASE(drm_fabric_test_user_fabric_new_del),
+	KUNIT_CASE(drm_fabric_test_user_fabric_new_reject_no_module_ref),
+	KUNIT_CASE(drm_fabric_test_reject_paths),
 	KUNIT_CASE(drm_fabric_test_local_unplug_keeps_edge),
 	KUNIT_CASE(drm_fabric_test_remote_peer_retained),
 	KUNIT_CASE(drm_fabric_test_subtree_delete_single_bump),
+	KUNIT_CASE_SLOW(drm_fabric_test_concurrent_mutation),
 	KUNIT_CASE(drm_fabric_test_switch_topology),
 	KUNIT_CASE_PARAM(drm_fabric_test_topology_param,
 			 fabrictest_topo_gen_params),
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.