mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Konstantin Sinyuk <ksinyuk@kernel.org>
To: dri-devel@lists.freedesktop.org
Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
	Francois Dugast <francois.dugast@intel.com>,
	David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
	Maxime Ripard <mripard@kernel.org>,
	Thomas Zimmermann <tzimmermann@suse.de>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Donald Hunter <donald.hunter@gmail.com>,
	Jakub Kicinski <kuba@kernel.org>,
	"David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
	Ilia Levi <ilia.levi@intel.com>,
	Rodrigo Vivi <rodrigo.vivi@intel.com>,
	linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [RFC PATCH 11/12] drm/fabric: add mutation KUnit tests
Date: Mon, 24 Aug 2026 11:09:39 +0300	[thread overview]
Message-ID: <56ac95cbe4dfdfec86f4828bd59e4b3539b9bb39.1787552412.git.ksinyuk@kernel.org> (raw)
In-Reply-To: <cover.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 <ksinyuk@kernel.org>
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


  parent reply	other threads:[~2026-08-24  8:10 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-24  8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 01/12] drm/fabric: add core object model and provider API Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 02/12] drm/fabric: add query uAPI and generated headers Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 03/12] drm/fabric: implement query netlink operations Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 04/12] drm/fabric: add read-only synthetic provider Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 05/12] drm/fabric: add object-model KUnit tests Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 06/12] drm/fabric: add YNL query and policy selftests Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 07/12] drm/fabric: add topology-provisioning core Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 08/12] drm/fabric: add provisioning netlink uAPI Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 09/12] drm/fabric: implement mutation netlink operations Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 10/12] drm/fabric: make the synthetic provider writable Konstantin Sinyuk
2026-08-24  8:09 ` Konstantin Sinyuk [this message]
2026-08-24  8:09 ` [RFC PATCH 12/12] drm/fabric: add mutation netlink selftests Konstantin Sinyuk

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=56ac95cbe4dfdfec86f4828bd59e4b3539b9bb39.1787552412.git.ksinyuk@kernel.org \
    --to=ksinyuk@kernel.org \
    --cc=airlied@gmail.com \
    --cc=corbet@lwn.net \
    --cc=davem@davemloft.net \
    --cc=donald.hunter@gmail.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=edumazet@google.com \
    --cc=francois.dugast@intel.com \
    --cc=horms@kernel.org \
    --cc=ilia.levi@intel.com \
    --cc=kuba@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=rodrigo.vivi@intel.com \
    --cc=simona@ffwll.ch \
    --cc=skhan@linuxfoundation.org \
    --cc=tzimmermann@suse.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®