mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Pavol Sakac <sakacpav@amazon.de>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
	Tejun Heo <tj@kernel.org>,
	"Rafael J . Wysocki" <rafael@kernel.org>,
	Danilo Krummrich <dakr@kernel.org>
Cc: <driver-core@lists.linux.dev>, <linux-kernel@vger.kernel.org>,
	"Andy Shevchenko" <andriy.shevchenko@linux.intel.com>,
	Xu Yang <xu.yang_2@nxp.com>,
	Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>,
	<nh-open-source@amazon.com>
Subject: [PATCH 2/3] driver core: Index class glue directories by parent kobject
Date: Fri, 11 Sep 2026 19:16:04 +0200	[thread overview]
Message-ID: <20260911171639.68348-2-sakacpav@amazon.de> (raw)
In-Reply-To: <20260911-vfopt-s3-v1-0-66e3602f76f7@amazon.de>

get_device_parent() finds a parent's glue directory by walking the
class's glue_dirs kset list under gdp_mutex, so one per parent, as
vfio-dev needs per SR-IOV VF, is quadratic.

Index them by parent kobject in an rbtree embedded in the class's
subsys_private, which dies with the kset list it indexes, so no
per-entry class check is needed: two classes below one parent are told
apart by tree selection. The key is a kobject because a parentless
class device hangs off the shared "virtual" kobject, referenced by the
glue dir while indexed. The rb_node lives in struct class_dir, adding
no allocation and no failure mode.

gdp_mutex serializes the index, as it has glue dir lookup/create/remove
since commit 77d3d7c1d561f
("driver-core: fix race condition in get_device_parent()") and
commit e4a60d1390609
("sysfs: driver core: Fix glue dir race condition by gdp_mutex").

A kernfs name lookup in the parent's directory needs no new state, but
takes the kernfs root rwsem under gdp_mutex, behind the writes
concurrent sysfs directory creation generates.

A KUnit suite covers the index: reuse of one parent's glue directory,
two classes below one parent, the parentless "virtual" cases, reap and
recreate, name collision, many parents, device_move(), and class
teardown.

Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/base.h               |   4 +
 drivers/base/core.c               | 112 +++++--
 drivers/base/test/.kunitconfig    |   1 +
 drivers/base/test/Kconfig         |  12 +
 drivers/base/test/Makefile        |   2 +
 drivers/base/test/glue-dir-test.c | 466 ++++++++++++++++++++++++++++++
 6 files changed, 566 insertions(+), 31 deletions(-)
 create mode 100644 drivers/base/test/glue-dir-test.c

diff --git a/drivers/base/base.h b/drivers/base/base.h
index a5b7abc10ff0..f5d608f4aaa5 100644
--- a/drivers/base/base.h
+++ b/drivers/base/base.h
@@ -11,6 +11,7 @@
  *
  */
 #include <linux/notifier.h>
+#include <linux/rbtree_types.h>
 
 /**
  * struct subsys_private - structure to hold the private to the driver core
@@ -32,6 +33,8 @@
  * @dev_root: Default device to use as the parent.
  * @glue_dirs: "glue" directory to put in-between the parent device to
  *	       avoid namespace conflicts
+ * @glue_dirs_index: the class's glue dirs by parent kobject, under gdp_mutex;
+ *		     zeroed is an empty rb_root
  * @class: pointer back to the struct class that this structure is associated
  *	   with.
  * @lock_key: Lock class key for use by the lock validator
@@ -55,6 +58,7 @@ struct subsys_private {
 	struct device *dev_root;
 
 	struct kset glue_dirs;
+	struct rb_root glue_dirs_index;
 	const struct class *class;
 
 	struct lock_class_key lock_key;
diff --git a/drivers/base/core.c b/drivers/base/core.c
index 4c0c373998a1..5dea641cbdb6 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -26,6 +26,7 @@
 #include <linux/of.h>
 #include <linux/of_device.h>
 #include <linux/pm_runtime.h>
+#include <linux/rbtree.h>
 #include <linux/sched/mm.h>
 #include <linux/sched/signal.h>
 #include <linux/slab.h>
@@ -3263,6 +3264,8 @@ struct kobject *virtual_device_parent(void)
 struct class_dir {
 	struct kobject kobj;
 	const struct class *class;
+	/* in the class's glue_dirs_index, keyed by kobj.parent (gdp_mutex) */
+	struct rb_node index_node;
 };
 
 #define to_class_dir(obj) container_of(obj, struct class_dir, kobj)
@@ -3298,6 +3301,7 @@ static struct kobject *class_dir_create_and_add(struct subsys_private *sp,
 
 	dir->class = sp->class;
 	kobject_init(&dir->kobj, &class_dir_ktype);
+	RB_CLEAR_NODE(&dir->index_node);
 
 	dir->kobj.kset = &sp->glue_dirs;
 
@@ -3311,6 +3315,66 @@ static struct kobject *class_dir_create_and_add(struct subsys_private *sp,
 
 static DEFINE_MUTEX(gdp_mutex);
 
+/*
+ * Glue-dir lookup index: each class's glue dirs by parent kobject, in an
+ * rbtree embedded in its subsys_private.  The glue_dirs kset remains the
+ * membership and identity authority (kobj->kset); the index replaces only
+ * the list walk in get_device_parent() and dies with the subsys_private
+ * generation that owns both.
+ */
+static int glue_dir_cmp_key(const void *key, const struct rb_node *node)
+{
+	const struct class_dir *cd = rb_entry(node, struct class_dir,
+					      index_node);
+
+	if ((unsigned long)key < (unsigned long)cd->kobj.parent)
+		return -1;
+	return (unsigned long)key > (unsigned long)cd->kobj.parent;
+}
+
+static bool glue_dir_less(struct rb_node *lhs, const struct rb_node *rhs)
+{
+	const struct class_dir *cd = rb_entry(lhs, struct class_dir,
+					      index_node);
+
+	return glue_dir_cmp_key(cd->kobj.parent, rhs) < 0;
+}
+
+static struct kobject *glue_dir_lookup(struct subsys_private *sp,
+				       struct kobject *parent_kobj)
+{
+	struct rb_node *node;
+
+	lockdep_assert_held(&gdp_mutex);
+
+	node = rb_find(parent_kobj, &sp->glue_dirs_index, glue_dir_cmp_key);
+	if (!node)
+		return NULL;
+
+	return kobject_get(&rb_entry(node, struct class_dir, index_node)->kobj);
+}
+
+static void glue_dir_index(struct subsys_private *sp, struct kobject *glue_dir)
+{
+	lockdep_assert_held(&gdp_mutex);
+
+	/* Lookup-before-create under gdp_mutex keeps keys unique. */
+	rb_add(&to_class_dir(glue_dir)->index_node, &sp->glue_dirs_index,
+	       glue_dir_less);
+}
+
+/*
+ * rb_erase() uses only recorded tree links, but glue_dir_less() keys on
+ * kobj.parent: the dir, indexed at create, leaves the index before
+ * kobject_del() clears its parent -- an indexed node's key must never mutate.
+ */
+static void glue_dir_unindex(struct subsys_private *sp,
+			     struct kobject *glue_dir)
+{
+	lockdep_assert_held(&gdp_mutex);
+	rb_erase(&to_class_dir(glue_dir)->index_node, &sp->glue_dirs_index);
+}
+
 static struct kobject *get_device_parent(struct device *dev,
 					 struct device *parent)
 {
@@ -3338,13 +3402,7 @@ static struct kobject *get_device_parent(struct device *dev,
 		mutex_lock(&gdp_mutex);
 
 		/* find our class-directory at the parent and reference it */
-		spin_lock(&sp->glue_dirs.list_lock);
-		list_for_each_entry(k, &sp->glue_dirs.list, entry)
-			if (k->parent == parent_kobj) {
-				kobj = kobject_get(k);
-				break;
-			}
-		spin_unlock(&sp->glue_dirs.list_lock);
+		kobj = glue_dir_lookup(sp, parent_kobj);
 		if (kobj) {
 			mutex_unlock(&gdp_mutex);
 			subsys_put(sp);
@@ -3354,6 +3412,8 @@ static struct kobject *get_device_parent(struct device *dev,
 		/* or create a new class-directory at the parent device */
 		k = class_dir_create_and_add(sp, parent_kobj);
 		/* do not emit an uevent for this simple "glue" directory */
+		if (!IS_ERR(k))
+			glue_dir_index(sp, k);
 		mutex_unlock(&gdp_mutex);
 		subsys_put(sp);
 		return k;
@@ -3375,28 +3435,6 @@ static struct kobject *get_device_parent(struct device *dev,
 	return NULL;
 }
 
-static inline bool live_in_glue_dir(struct kobject *kobj,
-				    struct device *dev)
-{
-	struct subsys_private *sp;
-	bool retval;
-
-	if (!kobj || !dev->class)
-		return false;
-
-	sp = class_to_subsys(dev->class);
-	if (!sp)
-		return false;
-
-	if (kobj->kset == &sp->glue_dirs)
-		retval = true;
-	else
-		retval = false;
-
-	subsys_put(sp);
-	return retval;
-}
-
 static inline struct kobject *get_glue_dir(struct device *dev)
 {
 	return dev->kobj.parent;
@@ -3426,11 +3464,19 @@ static inline bool kobject_has_children(struct kobject *kobj)
  */
 static void cleanup_glue_dir(struct device *dev, struct kobject *glue_dir)
 {
+	struct subsys_private *sp;
 	unsigned int ref;
 
 	/* see if we live in a "glue" directory */
-	if (!live_in_glue_dir(glue_dir, dev))
+	if (!glue_dir || !dev->class)
+		return;
+	sp = class_to_subsys(dev->class);
+	if (!sp)
+		return;
+	if (glue_dir->kset != &sp->glue_dirs) {
+		subsys_put(sp);
 		return;
+	}
 
 	mutex_lock(&gdp_mutex);
 	/**
@@ -3482,10 +3528,14 @@ static void cleanup_glue_dir(struct device *dev, struct kobject *glue_dir)
 	 * for glue_dir kobj is 1.
 	 */
 	ref = kref_read(&glue_dir->kref);
-	if (!kobject_has_children(glue_dir) && !--ref)
+	if (!kobject_has_children(glue_dir) && !--ref) {
+		glue_dir_unindex(sp, glue_dir);
 		kobject_del(glue_dir);
+	}
 	kobject_put(glue_dir);
 	mutex_unlock(&gdp_mutex);
+	/* outside gdp_mutex: the last put runs the class's release callback */
+	subsys_put(sp);
 }
 
 static int device_add_class_symlinks(struct device *dev)
diff --git a/drivers/base/test/.kunitconfig b/drivers/base/test/.kunitconfig
index 473923f0998b..28322bad39a7 100644
--- a/drivers/base/test/.kunitconfig
+++ b/drivers/base/test/.kunitconfig
@@ -1,2 +1,3 @@
 CONFIG_KUNIT=y
 CONFIG_DM_KUNIT_TEST=y
+CONFIG_GLUE_DIR_KUNIT_TEST=y
diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 542ce07530a1..253b5bd96aff 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -24,3 +24,15 @@ config DRIVER_SWNODE_KUNIT_TEST
 	tristate "KUnit Tests for software node fw_devlink links" if !KUNIT_ALL_TESTS
 	depends on KUNIT
 	default KUNIT_ALL_TESTS
+
+config GLUE_DIR_KUNIT_TEST
+	tristate "KUnit Tests for class glue directories" if !KUNIT_ALL_TESTS
+	depends on KUNIT && SYSFS
+	default KUNIT_ALL_TESTS
+	help
+	  Enable this option to test the class glue directories the driver
+	  core places class devices under: that siblings share one, that a
+	  reaped one is created again, and that a same-named child which is
+	  not a glue directory is never mistaken for one.
+
+	  If unsure say N.
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index 9ced7bbd569f..f13f0c399bea 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -8,3 +8,5 @@ obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) += property-entry-test.o
 CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
 
 obj-$(CONFIG_DRIVER_SWNODE_KUNIT_TEST) += swnode-devlink-test.o
+
+obj-$(CONFIG_GLUE_DIR_KUNIT_TEST) += glue-dir-test.o
diff --git a/drivers/base/test/glue-dir-test.c b/drivers/base/test/glue-dir-test.c
new file mode 100644
index 000000000000..fcdfdd3acd6f
--- /dev/null
+++ b/drivers/base/test/glue-dir-test.c
@@ -0,0 +1,466 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the class glue-directory index: a class device whose
+ * parent is not itself a class device is placed under a per-parent "glue"
+ * directory named after the class, which get_device_parent() finds
+ * through a per-class index keyed by the parent kobject.  Every case
+ * drives that lookup through plain device registration.
+ */
+
+#include <kunit/resource.h>
+#include <kunit/test.h>
+
+#include <linux/device.h>
+#include <linux/kernfs.h>
+#include <linux/kobject.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+
+static void glue_dev_release(struct device *dev)
+{
+	kfree(dev);
+}
+
+static void glue_dev_unregister(void *data)
+{
+	device_unregister(data);
+}
+
+static void glue_root_unregister(void *data)
+{
+	root_device_unregister(data);
+}
+
+static void glue_class_destroy(void *data)
+{
+	class_destroy(data);
+}
+
+/* Does a child named @name exist under @parent? */
+static bool glue_child_visible(struct kobject *parent, const char *name)
+{
+	struct kernfs_node *kn = kernfs_find_and_get(parent->sd, name);
+	bool found = kn;
+
+	kernfs_put(kn);
+	return found;
+}
+
+/* Freed by glue_dev_release() when the last reference drops. */
+static struct device *glue_dev_alloc(struct kunit *test,
+				     struct device *parent,
+				     const struct class *class,
+				     const char *name)
+{
+	struct device *dev;
+	int ret;
+
+	dev = kzalloc_obj(*dev);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+
+	device_initialize(dev);
+	dev->parent = parent;
+	dev->class = class;
+	dev->release = glue_dev_release;
+
+	ret = dev_set_name(dev, "%s", name);
+	if (ret)
+		put_device(dev);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	return dev;
+}
+
+/* Register a class device; unregistered again by a deferred kunit action. */
+static struct device *glue_dev_add(struct kunit *test, struct device *parent,
+				   const struct class *class,
+				   const char *name)
+{
+	struct device *dev = glue_dev_alloc(test, parent, class, name);
+	int ret;
+
+	ret = device_add(dev);
+	if (ret)
+		put_device(dev);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+							glue_dev_unregister,
+							dev), 0);
+	return dev;
+}
+
+static struct class *glue_class(struct kunit *test, const char *name)
+{
+	struct class *class = class_create(name);
+
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, class);
+	KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+							glue_class_destroy,
+							class), 0);
+	return class;
+}
+
+static struct device *glue_root(struct kunit *test, const char *name)
+{
+	struct device *root = root_device_register(name);
+
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+	KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+							glue_root_unregister,
+							root), 0);
+	return root;
+}
+
+/* Siblings share one glue dir: the second lookup has to hit the first. */
+static void glue_test_reuse(struct kunit *test)
+{
+	struct device *root, *dev_a, *dev_b;
+	struct kobject *glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_reuse");
+	root = glue_root(test, "glue_kunit_reuse_root");
+
+	dev_a = glue_dev_add(test, root, class, "reuseA");
+	dev_b = glue_dev_add(test, root, class, "reuseB");
+
+	glue = dev_a->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue);
+	KUNIT_EXPECT_PTR_NE(test, glue, &root->kobj);
+	KUNIT_EXPECT_PTR_EQ(test, glue, dev_b->kobj.parent);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(glue, "reuseA"));
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(glue, "reuseB"));
+}
+
+/* Two classes below one parent: each consults only its own class's index. */
+static void glue_test_cross_class(struct kunit *test)
+{
+	struct class *class_a, *class_b;
+	struct device *root, *dev_a, *dev_b;
+
+	class_a = glue_class(test, "glue_kunit_xclass_a");
+	class_b = glue_class(test, "glue_kunit_xclass_b");
+	root = glue_root(test, "glue_kunit_xclass_root");
+
+	dev_a = glue_dev_add(test, root, class_a, "xclassA");
+	dev_b = glue_dev_add(test, root, class_b, "xclassB");
+
+	KUNIT_ASSERT_NOT_NULL(test, dev_a->kobj.parent);
+	KUNIT_EXPECT_PTR_NE(test, dev_a->kobj.parent, dev_b->kobj.parent);
+}
+
+/* Reap retires the index entry: the next add must not find the freed dir. */
+static void glue_test_reap_recreate(struct kunit *test)
+{
+	struct device *root, *dev;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_reap");
+	root = glue_root(test, "glue_kunit_reap_root");
+
+	dev = glue_dev_add(test, root, class, "reap0");
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(&root->kobj,
+						   "glue_kunit_reap"));
+
+	/* last child gone: the glue dir goes with it ... */
+	kunit_release_action(test, glue_dev_unregister, dev);
+	KUNIT_EXPECT_FALSE(test, glue_child_visible(&root->kobj,
+						    "glue_kunit_reap"));
+
+	/* ... and a further registration recreates it */
+	dev = glue_dev_add(test, root, class, "reap1");
+	KUNIT_ASSERT_NOT_NULL(test, dev->kobj.parent);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(dev->kobj.parent, "reap1"));
+}
+
+static ssize_t glue_kunit_collide_show(struct device *dev,
+				       struct device_attribute *attr,
+				       char *buf)
+{
+	return sysfs_emit(buf, "\n");
+}
+static DEVICE_ATTR_RO(glue_kunit_collide);
+
+/*
+ * A same-named child that is not a glue dir must never be taken for one:
+ * the colliding add fails on the duplicate sysfs create and leaves
+ * nothing stale, so the same registration succeeds once the file is gone.
+ */
+static void glue_test_name_collision(struct kunit *test)
+{
+	struct device *root, *dev;
+	struct kernfs_node *kn;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_collide");
+	root = glue_root(test, "glue_kunit_collide_root");
+
+	KUNIT_ASSERT_EQ(test,
+			device_create_file(root,
+					   &dev_attr_glue_kunit_collide), 0);
+
+	dev = glue_dev_alloc(test, root, class, "collide0");
+	KUNIT_ASSERT_EQ(test, device_add(dev), -EEXIST);
+	put_device(dev);
+
+	/* the same-named child is still the attribute file */
+	kn = kernfs_find_and_get(root->kobj.sd, "glue_kunit_collide");
+	KUNIT_ASSERT_NOT_NULL(test, kn);
+	KUNIT_EXPECT_EQ(test, kernfs_type(kn), KERNFS_FILE);
+	kernfs_put(kn);
+
+	device_remove_file(root, &dev_attr_glue_kunit_collide);
+	dev = glue_dev_add(test, root, class, "collide0");
+
+	/* and it lands in a glue dir of the class's name, not on the root */
+	KUNIT_ASSERT_NOT_NULL(test, dev->kobj.parent);
+	KUNIT_EXPECT_PTR_NE(test, dev->kobj.parent, &root->kobj);
+	KUNIT_EXPECT_STREQ(test, kobject_name(dev->kobj.parent), class->name);
+	KUNIT_EXPECT_PTR_EQ(test, dev->kobj.parent->parent, &root->kobj);
+}
+
+/*
+ * A parentless class device is keyed on the shared "virtual" kobject
+ * rather than on a device, so two of them still share one glue dir.
+ */
+static void glue_test_virtual_parent(struct kunit *test)
+{
+	struct device *dev_a, *dev_b;
+	struct kobject *glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_virtual");
+
+	dev_a = glue_dev_add(test, NULL, class, "virtA");
+	dev_b = glue_dev_add(test, NULL, class, "virtB");
+
+	glue = dev_a->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue);
+	KUNIT_EXPECT_PTR_EQ(test, glue, dev_b->kobj.parent);
+	KUNIT_ASSERT_NOT_NULL(test, glue->parent);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue->parent), "virtual");
+}
+
+/*
+ * Every class's parentless devices share the one "virtual" key; two
+ * classes below it must still land in dirs of their own class's name.
+ */
+static void glue_test_virtual_cross_class(struct kunit *test)
+{
+	struct class *class_a, *class_b;
+	struct device *dev_a, *dev_b;
+	struct kobject *glue_a, *glue_b;
+
+	class_a = glue_class(test, "glue_kunit_virtxc_a");
+	class_b = glue_class(test, "glue_kunit_virtxc_b");
+
+	dev_a = glue_dev_add(test, NULL, class_a, "virtxcA");
+	dev_b = glue_dev_add(test, NULL, class_b, "virtxcB");
+
+	glue_a = dev_a->kobj.parent;
+	glue_b = dev_b->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue_a);
+	KUNIT_ASSERT_NOT_NULL(test, glue_b);
+
+	KUNIT_EXPECT_PTR_NE(test, glue_a, glue_b);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue_a), class_a->name);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue_b), class_b->name);
+
+	/* both dirs hang off the one shared key */
+	KUNIT_ASSERT_NOT_NULL(test, glue_a->parent);
+	KUNIT_EXPECT_PTR_EQ(test, glue_a->parent, glue_b->parent);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue_a->parent), "virtual");
+}
+
+#define GLUE_MANY_PARENTS 256
+
+/*
+ * With hundreds of parents indexed, every lookup must resolve the dir
+ * anchored at its own parent -- the case that falsifies the key comparison.
+ */
+static void glue_test_many_parents(struct kunit *test)
+{
+	struct device **roots, **devs;
+	struct class *class;
+	char name[32];
+	int i;
+
+	class = glue_class(test, "glue_kunit_many");
+
+	roots = kunit_kcalloc(test, GLUE_MANY_PARENTS, sizeof(*roots),
+			      GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, roots);
+	devs = kunit_kcalloc(test, GLUE_MANY_PARENTS, sizeof(*devs),
+			     GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, devs);
+
+	for (i = 0; i < GLUE_MANY_PARENTS; i++) {
+		snprintf(name, sizeof(name), "glue_kunit_many_root%d", i);
+		roots[i] = glue_root(test, name);
+		snprintf(name, sizeof(name), "many%d", i);
+		devs[i] = glue_dev_add(test, roots[i], class, name);
+	}
+
+	/* Distinctness via anchoring: a kobject has one parent. */
+	for (i = 0; i < GLUE_MANY_PARENTS; i++) {
+		struct kobject *glue = devs[i]->kobj.parent;
+
+		KUNIT_ASSERT_NOT_NULL(test, glue);
+		KUNIT_EXPECT_PTR_EQ(test, glue->parent, &roots[i]->kobj);
+	}
+}
+
+/*
+ * device_move() looks up against the new parent and must leave the old
+ * parent's entry behind as a valid hit: the old glue dir is never reaped
+ * (base behaviour), so a later device under the old parent reuses it.
+ */
+static void glue_test_device_move(struct kunit *test)
+{
+	struct device *root_a, *root_b, *dev, *dev_a2;
+	struct kobject *old_glue, *new_glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_move");
+	root_a = glue_root(test, "glue_kunit_move_rootA");
+	root_b = glue_root(test, "glue_kunit_move_rootB");
+
+	dev = glue_dev_add(test, root_a, class, "move0");
+	old_glue = dev->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, old_glue);
+
+	KUNIT_ASSERT_EQ(test, device_move(dev, root_b, DPM_ORDER_NONE), 0);
+
+	new_glue = dev->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, new_glue);
+	KUNIT_EXPECT_PTR_NE(test, new_glue, old_glue);
+	KUNIT_EXPECT_PTR_EQ(test, new_glue->parent, &root_b->kobj);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(new_glue, "move0"));
+
+	/* nothing reaps the old dir on the success path: it is still there */
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(&root_a->kobj,
+						   "glue_kunit_move"));
+
+	/* and it must still be the hit for the old parent */
+	dev_a2 = glue_dev_add(test, root_a, class, "move1");
+	KUNIT_EXPECT_PTR_EQ(test, dev_a2->kobj.parent, old_glue);
+}
+
+/*
+ * Removing one of two siblings must not retire the shared glue dir:
+ * the survivor keeps it visible, and a further sibling still reuses
+ * it -- a spurious unindex would fail that add on a duplicate create.
+ */
+static void glue_test_no_reap(struct kunit *test)
+{
+	struct device *root, *dev_a, *dev_b, *dev_c;
+	struct kobject *glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_noreap");
+	root = glue_root(test, "glue_kunit_noreap_root");
+
+	dev_a = glue_dev_add(test, root, class, "noreapA");
+	dev_b = glue_dev_add(test, root, class, "noreapB");
+	glue = dev_b->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue);
+
+	kunit_release_action(test, glue_dev_unregister, dev_a);
+
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(&root->kobj,
+						   "glue_kunit_noreap"));
+	KUNIT_EXPECT_PTR_EQ(test, dev_b->kobj.parent, glue);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(glue, "noreapB"));
+
+	dev_c = glue_dev_add(test, root, class, "noreapC");
+	KUNIT_EXPECT_PTR_EQ(test, dev_c->kobj.parent, glue);
+}
+
+/*
+ * Static so that unregistering under a live device frees only the
+ * driver-core generation, not the struct class the device points at.
+ */
+static const struct class glue_gone_class_a = {
+	.name = "glue_kunit_gone_a",
+};
+
+static const struct class glue_gone_class_b = {
+	.name = "glue_kunit_gone_b",
+};
+
+static void glue_static_class_unregister(void *data)
+{
+	class_unregister(data);
+}
+
+/*
+ * Unregistering a class under a live device is tolerated API misuse: the
+ * device's glue dir can no longer be reaped and is deliberately leaked
+ * (class_dir, kernfs node, and parent reference), here as in the base
+ * tree; kmemleak reports all three on every run.
+ *
+ * The case pins the index's placement: a dead generation's tree dies
+ * with its subsys_private, so the successor class starts on an empty
+ * tree and cannot resolve the leaked dir.  A global index would put the
+ * dead entry back in its search path.
+ */
+static void glue_test_class_gone(struct kunit *test)
+{
+	struct device *root, *dev_a, *dev_b;
+	struct kobject *glue_a;
+
+	KUNIT_ASSERT_EQ(test, class_register(&glue_gone_class_a), 0);
+	KUNIT_ASSERT_EQ(test,
+			kunit_add_action_or_reset(test,
+						  glue_static_class_unregister,
+						  (void *)&glue_gone_class_a),
+			0);
+	root = glue_root(test, "glue_kunit_gone_root");
+
+	dev_a = glue_dev_add(test, root, &glue_gone_class_a, "goneA");
+	glue_a = dev_a->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue_a);
+
+	/* the misuse: the class goes away under a live device */
+	kunit_release_action(test, glue_static_class_unregister,
+			     (void *)&glue_gone_class_a);
+
+	KUNIT_ASSERT_EQ(test, class_register(&glue_gone_class_b), 0);
+	KUNIT_ASSERT_EQ(test,
+			kunit_add_action_or_reset(test,
+						  glue_static_class_unregister,
+						  (void *)&glue_gone_class_b),
+			0);
+
+	dev_b = glue_dev_add(test, root, &glue_gone_class_b, "goneB");
+	KUNIT_ASSERT_NOT_NULL(test, dev_b->kobj.parent);
+	KUNIT_EXPECT_PTR_NE(test, dev_b->kobj.parent, glue_a);
+	KUNIT_EXPECT_PTR_NE(test, dev_b->kobj.parent, &root->kobj);
+	KUNIT_EXPECT_STREQ(test, kobject_name(dev_b->kobj.parent),
+			   glue_gone_class_b.name);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(dev_b->kobj.parent,
+						   "goneB"));
+}
+
+static struct kunit_case glue_dir_tests[] = {
+	KUNIT_CASE(glue_test_reuse),
+	KUNIT_CASE(glue_test_cross_class),
+	KUNIT_CASE(glue_test_reap_recreate),
+	KUNIT_CASE(glue_test_name_collision),
+	KUNIT_CASE(glue_test_virtual_parent),
+	KUNIT_CASE(glue_test_virtual_cross_class),
+	KUNIT_CASE(glue_test_many_parents),
+	KUNIT_CASE(glue_test_device_move),
+	KUNIT_CASE(glue_test_no_reap),
+	KUNIT_CASE(glue_test_class_gone),
+	{}
+};
+
+static struct kunit_suite glue_dir_test_suite = {
+	.name = "glue_dir",
+	.test_cases = glue_dir_tests,
+};
+
+kunit_test_suite(glue_dir_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for class glue directories");
+MODULE_LICENSE("GPL");
-- 
2.47.3


  parent reply	other threads:[~2026-09-11 17:17 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-11 17:16 [PATCH 0/3] kernfs, driver core: Cut per-node lock traffic in bulk registration Pavol Sakac
2026-09-11 17:16 ` [PATCH 1/3] kernfs: activate nodes while linking them Pavol Sakac
2026-09-11 18:12   ` Andy Shevchenko
2026-09-11 17:16 ` Pavol Sakac [this message]
2026-09-11 18:11   ` [PATCH 2/3] driver core: Index class glue directories by parent kobject Andy Shevchenko
2026-09-11 17:16 ` [PATCH 3/3] kernfs: batch inode ID allocation per CPU Pavol Sakac

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=20260911171639.68348-2-sakacpav@amazon.de \
    --to=sakacpav@amazon.de \
    --cc=andriy.shevchenko@linux.intel.com \
    --cc=bartosz.golaszewski@oss.qualcomm.com \
    --cc=dakr@kernel.org \
    --cc=driver-core@lists.linux.dev \
    --cc=gregkh@linuxfoundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=nh-open-source@amazon.com \
    --cc=rafael@kernel.org \
    --cc=tj@kernel.org \
    --cc=xu.yang_2@nxp.com \
    /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®