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 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 #include +#include +#include +#include #include #include +#include +#include #include +#include #include #include @@ -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