The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Pavol Sakac <sakacpav@amazon.de>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
	"Rafael J . Wysocki" <rafael@kernel.org>,
	Danilo Krummrich <dakr@kernel.org>, Tejun Heo <tj@kernel.org>
Cc: <linux-kernel@vger.kernel.org>, <driver-core@lists.linux.dev>,
	"David Woodhouse" <dwmw@amazon.co.uk>,
	Pasha Tatashin <pasha.tatashin@soleen.com>,
	Mike Rapoport <rppt@kernel.org>,
	Pratyush Yadav <pratyush@kernel.org>,
	"David Matlack" <dmatlack@google.com>,
	Samiullah Khawaja <skhawaja@google.com>,
	Alexander Graf <graf@amazon.com>, <linux-mm@kvack.org>,
	<kexec@lists.infradead.org>, <nh-open-source@amazon.com>
Subject: [RFC PATCH 12/14] driver core: test: add KUnit tests for device_sysfs_apply
Date: Thu, 2 Jul 2026 19:51:12 +0200	[thread overview]
Message-ID: <20260702175114.24659-8-sakacpav@amazon.de> (raw)
In-Reply-To: <20260702175114.24659-1-sakacpav@amazon.de>

Add KUnit tests that exercise device_sysfs_apply() - the declarative
per-device sysfs walker introduced earlier in this series - in
isolation from real sysfs I/O, plus dedicated tests for the
kernfs_set_lazy() input-validation contract.

Each walker test programs a small struct device_sysfs_entry table
whose applies_to / create / remove callbacks are file-static mocks
that record invocations in a shared state block and return
programmable values. Race and fault-injection tests use freshly
allocated lazy platform_devices and drive populate via the device's
ktype function pointers. The kernfs_set_lazy() rejection-branch
tests build standalone kernfs roots (no platform_device) so each
branch of the input-validation predicate is exercised in isolation.

Coverage:
  - dispatch correctness: ADD_ONE name match, wildcard row -ENOENT
    fallthrough, ADD_ALL best-effort, REMOVE_ALL reverse-order,
    applies_to() filter
  - lock + populated-latch: per-device serialisation, latch set
    once, lockless fast-path
  - fault injection: create() -ENOMEM propagation, sysfs_warn_dup()
    expected to NOT fire under the lock-then-exists-then-create
    protocol
  - eager/lazy equivalence: a device that fully populated lazily
    ends up with the same kernfs structure as an eager device
  - kernfs_set_lazy() input-validation: -EINVAL rejection on
    namespaced kn (kn->ns set), KERNFS_NS-flagged kn, and non-DIR
    kn (KERNFS_FILE), plus the happy path on a plain DIR kn that
    must return 0 and set KERNFS_LAZY.  Defense-in-depth: each
    rejection test also asserts KERNFS_LAZY remained unset on the
    rejected kn.

fs/sysfs/dir.c gains a CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST-gated
atomic_t sysfs_warn_dup_kunit_count incremented inside
sysfs_warn_dup().  The race tests sample it before/after a
populate_one vs populate_all kthread storm and assert the delta
is zero - confirming that the lock-then-exists-then-create
protocol prevents duplicate-create WARNs in practice.  Production
builds (KUnit test off) carry no overhead.

End-to-end VFS -> kernfs -> sysfs -> ktype integration is covered
by the userspace selftest at tools/testing/selftests/sysfs-lazy/.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Brendan Higgins <brendan.higgins@linux.dev>
Cc: David Gow <davidgow@google.com>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: kunit-dev@googlegroups.com
Cc: driver-core@lists.linux.dev
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/test/.kunitconfig              |    7 +
 drivers/base/test/Kconfig                   |   13 +
 drivers/base/test/Makefile                  |    2 +
 drivers/base/test/device_sysfs_apply_test.c | 1601 +++++++++++++++++++
 fs/sysfs/dir.c                              |   18 +
 5 files changed, 1641 insertions(+)
 create mode 100644 drivers/base/test/device_sysfs_apply_test.c

diff --git a/drivers/base/test/.kunitconfig b/drivers/base/test/.kunitconfig
index 473923f0998b6..662e404797f14 100644
--- a/drivers/base/test/.kunitconfig
+++ b/drivers/base/test/.kunitconfig
@@ -1,2 +1,9 @@
 CONFIG_KUNIT=y
 CONFIG_DM_KUNIT_TEST=y
+CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST=y
+CONFIG_PROVE_LOCKING=y
+CONFIG_DEBUG_ATOMIC_SLEEP=y
+CONFIG_DEBUG_MUTEXES=y
+CONFIG_DEBUG_SPINLOCK=y
+CONFIG_DEBUG_LOCK_ALLOC=y
+CONFIG_LOCKDEP=y
diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 2756870615cca..43ef11f4100c4 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -18,3 +18,16 @@ config DRIVER_PE_KUNIT_TEST
 	tristate "KUnit Tests for property entry API" if !KUNIT_ALL_TESTS
 	depends on KUNIT
 	default KUNIT_ALL_TESTS
+
+config DEVICE_SYSFS_APPLY_KUNIT_TEST
+	bool "KUnit tests for device_sysfs_apply()" if !KUNIT_ALL_TESTS
+	depends on KUNIT=y
+	depends on SYSFS
+	default KUNIT_ALL_TESTS
+	help
+	  KUnit tests for the device_sysfs_apply() walker that
+	  backs declarative sysfs content via struct device_sysfs_entry.
+	  Covers walker semantics in isolation (empty table, applies_to
+	  gating, wildcard rows, two rows sharing a name, errno
+	  propagation, and reverse-order teardown). device_sysfs_apply()
+	  is not exported, so the test must be built in.
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index e321dfc7e9226..c061374f6c4f3 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -6,3 +6,5 @@ obj-$(CONFIG_DM_KUNIT_TEST)	+= platform-device-test.o
 
 obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) += property-entry-test.o
 CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
+
+obj-$(CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST) += device_sysfs_apply_test.o
diff --git a/drivers/base/test/device_sysfs_apply_test.c b/drivers/base/test/device_sysfs_apply_test.c
new file mode 100644
index 0000000000000..a62d383bd3182
--- /dev/null
+++ b/drivers/base/test/device_sysfs_apply_test.c
@@ -0,0 +1,1601 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for device_sysfs_apply().
+ *
+ * Exercises the table walker contract in isolation: empty table,
+ * named-row ADD_ONE match / miss, applies_to gating, wildcard rows
+ * (name == NULL), two rows sharing a name (disambiguated by
+ * applies_to), -EEXIST and -ENOMEM propagation, and REMOVE_ALL
+ * reverse-order teardown.  Three integration-style cases
+ * (walk_lazy_device_has_no_eager_children, walk_eager_lazy_equivalence,
+ * walk_wildcard_row_idempotent) drive the walker on real
+ * platform_device fixtures.  Two kthread-pair race cases
+ * (walk_populate_one_vs_all_race, walk_populate_vs_device_del_race)
+ * stress the lock + populated-latch double-check protocol
+ * and the dev->p->dead re-check that gates populate against
+ * concurrent device_del.  One fault-injection-friendly case
+ * (walk_create_power_enomem) exercises the -ENOMEM error path for
+ * create_power()'s power_added latch: it uses a
+ * structurally-equivalent path -- a lazy device whose
+ * create_power() never ran -- to assert the remove_power() gate
+ * skips dpm_sysfs_remove() without a sysfs_remove_group() WARN,
+ * which is the same observable state create_power() leaves behind
+ * on -ENOMEM.
+ *
+ * Each walker test programs a small, purpose-built struct
+ * device_sysfs_entry table whose create / remove / applies_to
+ * callbacks are file-static mocks that record invocations in a
+ * per-test state block and return programmable values. A real
+ * struct device is held by a platform device fixture so callback
+ * signatures match production usage; the walker itself never
+ * dereferences @dev, which keeps the tests focused on dispatch
+ * semantics rather than sysfs I/O.
+ *
+ * Full VFS -> kernfs -> sysfs -> ktype -> walker integration is
+ * covered by the userspace kselftest at
+ * tools/testing/selftests/sysfs-lazy/.
+ */
+
+#include <kunit/test.h>
+#include <kunit/resource.h>
+
+#include <linux/atomic.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/kernfs.h>
+#include <linux/kthread.h>
+#include <linux/panic.h>
+#include <linux/platform_device.h>
+#include <linux/rbtree.h>
+#include <linux/rcupdate.h>
+#include <linux/sched.h>
+#include <linux/stat.h>
+#include <linux/string.h>
+
+/*
+ * Private driver-core header for the layout of struct sysfs_lazy_state.
+ * walk_create_power_enomem (Test 14) reads sysfs_lazy->power_added
+ * directly to assert the latch state that drives remove_power()'s
+ * dpm_sysfs_remove() gate. The struct's layout is intentionally
+ * file-private to drivers/base/; consumers outside this directory
+ * MUST use the device_is_sysfs_lazy() / device_sysfs_populated()
+ * accessors instead. This test sits inside drivers/base/test/ so it
+ * is part of the same build-locality scope as core.c and may peek
+ * at the struct.
+ */
+#include "../base.h"
+
+/*
+ * Counter exported from fs/sysfs/dir.c (under
+ * CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST). Bumped on every
+ * sysfs_warn_dup() invocation. The race tests
+ * (walk_populate_one_vs_all_race / walk_populate_vs_device_del_race)
+ * sample this before and after the kthread storm and assert delta == 0:
+ * The lock protocol makes the lazy populate paths race-free under lock,
+ * so any "cannot create duplicate filename" emission during the test
+ * window is a lock invariant violation.
+ *
+ * sysfs_warn_dup() uses pr_warn()+dump_stack(), neither of which sets
+ * TAINT_WARN, so the existing warn-taint check cannot observe the
+ * dup-warn condition; this counter closes that gap.
+ */
+extern atomic_t sysfs_warn_dup_kunit_count;
+
+#define APPLY_KUNIT_DEV_NAME	"device_sysfs_apply_kunit"
+
+/*
+ * Mock state: up to 8 rows per test, each with its own counters and
+ * programmable return value. Reset between tests by the init hook.
+ */
+
+#define MOCK_ROWS	8
+#define MOCK_CALLS	64
+
+enum mock_op {
+	MOCK_CREATE,
+	MOCK_REMOVE,
+};
+
+struct mock_call {
+	enum mock_op op;
+	int idx;
+	const char *name_arg;
+};
+
+static struct {
+	struct mock_call calls[MOCK_CALLS];
+	int n_calls;
+	int create_ret[MOCK_ROWS];
+	bool applies[MOCK_ROWS];
+	int applies_hits[MOCK_ROWS];
+} mock;
+
+static void mock_reset(void)
+{
+	int i;
+
+	memset(&mock, 0, sizeof(mock));
+	for (i = 0; i < MOCK_ROWS; i++)
+		mock.applies[i] = true;
+}
+
+static void mock_record(enum mock_op op, int idx, const char *name_arg)
+{
+	if (mock.n_calls >= MOCK_CALLS)
+		return;
+	mock.calls[mock.n_calls].op = op;
+	mock.calls[mock.n_calls].idx = idx;
+	mock.calls[mock.n_calls].name_arg = name_arg;
+	mock.n_calls++;
+}
+
+#define DEFINE_ROW_MOCKS(I)						\
+static bool __maybe_unused mock_applies_##I(struct device *dev)		\
+{									\
+	mock.applies_hits[I]++;						\
+	return mock.applies[I];						\
+}									\
+static int __maybe_unused mock_create_##I(struct device *dev,		\
+					   const char *name)		\
+{									\
+	mock_record(MOCK_CREATE, I, name);				\
+	return mock.create_ret[I];					\
+}									\
+static void __maybe_unused mock_remove_##I(struct device *dev)		\
+{									\
+	mock_record(MOCK_REMOVE, I, NULL);				\
+}
+
+DEFINE_ROW_MOCKS(0)
+DEFINE_ROW_MOCKS(1)
+DEFINE_ROW_MOCKS(2)
+DEFINE_ROW_MOCKS(3)
+DEFINE_ROW_MOCKS(4)
+DEFINE_ROW_MOCKS(5)
+DEFINE_ROW_MOCKS(6)
+DEFINE_ROW_MOCKS(7)
+
+/*
+ * Fixture: one platform device that supplies a real struct device * to
+ * the walker. No attrs are attached; rows above drive all behaviour.
+ */
+
+struct walk_test_priv {
+	struct platform_device *pdev;
+};
+
+static int walk_test_init(struct kunit *test)
+{
+	struct walk_test_priv *priv;
+	struct platform_device *pdev;
+	int ret;
+
+	mock_reset();
+
+	priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, priv);
+
+	pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME,
+				     PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev);
+
+	ret = platform_device_add(pdev);
+	if (ret) {
+		platform_device_put(pdev);
+		KUNIT_FAIL(test, "platform_device_add failed: %d", ret);
+		return ret;
+	}
+
+	priv->pdev = pdev;
+	test->priv = priv;
+	return 0;
+}
+
+static void walk_test_exit(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+
+	if (priv && priv->pdev)
+		platform_device_unregister(priv->pdev);
+}
+
+/*
+ * Shared kthread-pair harness for the race tests.
+ *
+ * Two kernel threads run user-supplied worker functions concurrently
+ * for a fixed wall-clock window. Each worker loops until
+ * kthread_should_stop() and increments its own iteration counter via
+ * atomic_inc(); the harness reports the counts so the test can assert
+ * both threads actually got CPU time. Cleanup is registered with
+ * kunit_add_action_or_reset() so kthread_stop() runs even if the test
+ * aborts via KUNIT_ASSERT_*.
+ *
+ * The same harness is reused by walk_populate_one_vs_all_race and
+ * walk_populate_vs_device_del_race; both worker pairs share the
+ * signature `int worker(void *priv)` so the harness need not know
+ * which population path is being exercised.
+ */
+
+struct walk_thread_pair {
+	struct task_struct	*t1;
+	struct task_struct	*t2;
+	atomic_t		iters1;
+	atomic_t		iters2;
+	atomic_t		bad_results;
+	void			*priv;
+};
+
+KUNIT_DEFINE_ACTION_WRAPPER(walk_kthread_cleanup, kthread_stop,
+			    struct task_struct *);
+
+static struct task_struct *walk_thread_start(struct kunit *test,
+					     int (*fn)(void *), void *priv,
+					     const char *name)
+{
+	struct task_struct *t;
+	int err;
+
+	t = kthread_run(fn, priv, "%s", name);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t);
+
+	/*
+	 * Register cleanup BEFORE returning to the caller.  If the
+	 * caller's later KUNIT_ASSERT_* aborts the test, the action
+	 * fires and reaps the thread instead of leaking it.  When the
+	 * caller cleanly stops the thread itself it must
+	 * kunit_remove_action() to avoid a double kthread_stop() on a
+	 * freed task_struct.
+	 */
+	err = kunit_add_action_or_reset(test, walk_kthread_cleanup, t);
+	KUNIT_ASSERT_EQ(test, err, 0);
+
+	return t;
+}
+
+static void walk_thread_pair_stop(struct kunit *test,
+				  struct walk_thread_pair *pair)
+{
+	if (pair->t1) {
+		kunit_remove_action(test, walk_kthread_cleanup, pair->t1);
+		kthread_stop(pair->t1);
+		pair->t1 = NULL;
+	}
+	if (pair->t2) {
+		kunit_remove_action(test, walk_kthread_cleanup, pair->t2);
+		kthread_stop(pair->t2);
+		pair->t2 = NULL;
+	}
+}
+
+/*
+ * Test 1: empty table
+ *
+ * NULL entries -> ADD_ONE returns -ENOENT, ADD_ALL and REMOVE_ALL
+ * succeed. Sentinel-only table behaves the same way.
+ */
+static void walk_empty_table(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry sentinel_only[] = {
+		{ }
+	};
+
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, NULL,
+						DEV_SYSFS_ADD_ONE, "x"),
+			-ENOENT);
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, NULL,
+						DEV_SYSFS_ADD_ALL, NULL), 0);
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, NULL,
+						DEV_SYSFS_REMOVE_ALL, NULL),
+			0);
+
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, sentinel_only,
+						DEV_SYSFS_ADD_ONE, "x"),
+			-ENOENT);
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, sentinel_only,
+						DEV_SYSFS_ADD_ALL, NULL), 0);
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, sentinel_only,
+						DEV_SYSFS_REMOVE_ALL, NULL),
+			0);
+
+	KUNIT_EXPECT_EQ(test, mock.n_calls, 0);
+}
+
+/*
+ * Test 2: single named row
+ *
+ * ADD_ONE with a matching name invokes create(); a miss skips it and
+ * returns -ENOENT. ADD_ALL invokes create() once with name == NULL.
+ * REMOVE_ALL invokes remove once.
+ */
+static void walk_single_row(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = "alpha",
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+			.remove = mock_remove_0,
+		},
+		{ }
+	};
+
+	/* ADD_ONE match */
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "alpha"),
+			0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 1);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].op, MOCK_CREATE);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 0);
+	KUNIT_EXPECT_STREQ(test, mock.calls[0].name_arg, "alpha");
+
+	/* ADD_ONE miss */
+	mock_reset();
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "other"),
+			-ENOENT);
+	KUNIT_EXPECT_EQ(test, mock.n_calls, 0);
+
+	/* ADD_ALL */
+	mock_reset();
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ALL, NULL), 0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 1);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].op, MOCK_CREATE);
+	KUNIT_EXPECT_PTR_EQ(test, mock.calls[0].name_arg, (const char *)NULL);
+
+	/* REMOVE_ALL */
+	mock_reset();
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_REMOVE_ALL, NULL),
+			0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 1);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].op, MOCK_REMOVE);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 0);
+}
+
+/*
+ * Test 3: applies_to == false gate
+ *
+ * A row whose applies_to returns false is skipped on every action.
+ */
+static void walk_applies_to_false(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = "gated",
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+			.remove = mock_remove_0,
+		},
+		{ }
+	};
+
+	mock.applies[0] = false;
+
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "gated"),
+			-ENOENT);
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ALL, NULL), 0);
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_REMOVE_ALL, NULL),
+			0);
+
+	KUNIT_EXPECT_EQ(test, mock.n_calls, 0);
+	KUNIT_EXPECT_GT(test, mock.applies_hits[0], 0);
+}
+
+/*
+ * Test 4: wildcard row
+ *
+ * A row with name == NULL dispatches to create() on every ADD_ONE
+ * regardless of the target name. The row signals "not my name" by
+ * returning -ENOENT; the walker must continue to the next row and
+ * terminate in the overall -ENOENT result.
+ */
+static void walk_wildcard_row(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = NULL,
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+		},
+		{
+			.name = NULL,
+			.applies_to = mock_applies_1,
+			.create = mock_create_1,
+		},
+		{ }
+	};
+
+	/* Both wildcards signal "not mine"; walker returns -ENOENT. */
+	mock.create_ret[0] = -ENOENT;
+	mock.create_ret[1] = -ENOENT;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "any"),
+			-ENOENT);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 2);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 0);
+	KUNIT_EXPECT_EQ(test, mock.calls[1].idx, 1);
+
+	/* Second wildcard claims the name; walker stops there. */
+	mock_reset();
+	mock.create_ret[0] = -ENOENT;
+	mock.create_ret[1] = 0;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "any"),
+			0);
+	KUNIT_EXPECT_EQ(test, mock.n_calls, 2);
+	KUNIT_EXPECT_EQ(test, mock.calls[1].idx, 1);
+}
+
+/*
+ * Test 5: two rows share a name, disambiguated by applies_to
+ *
+ * Two rows may carry the same .name as
+ * long as their applies_to predicates are mutually exclusive. On
+ * ADD_ONE the first match wins; in this table the second row is gated
+ * off so only the first row fires.
+ */
+static void walk_two_rows_same_name(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = "subsystem",
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+		},
+		{
+			.name = "subsystem",
+			.applies_to = mock_applies_1,
+			.create = mock_create_1,
+		},
+		{ }
+	};
+
+	/* Row 0 off, row 1 on -> only row 1 fires. */
+	mock.applies[0] = false;
+	mock.applies[1] = true;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE,
+						"subsystem"), 0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 1);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 1);
+
+	/* Row 0 on, row 1 on (ambiguous) -> first match wins. */
+	mock_reset();
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE,
+						"subsystem"), 0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 1);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 0);
+}
+
+/*
+ * Test 6: -EEXIST propagation on ADD_ONE
+ *
+ * The walker forwards create()'s return value for any ADD_ONE result
+ * other than -ENOENT (which means "not my row" for wildcard rows and
+ * "no match at all" for the overall walk). -EEXIST absorption is a
+ * row-level contract: create() converts -EEXIST from a racing
+ * populate_one to 0 before returning. This test documents the walker
+ * side -- a row that surfaces -EEXIST propagates it, giving reviewers
+ * a clear signal of a row-contract violation.
+ */
+static void walk_eexist_propagates(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = "dup",
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+		},
+		{ }
+	};
+
+	/* Row absorbed internally and returned 0 -> walker returns 0. */
+	mock.create_ret[0] = 0;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "dup"),
+			0);
+
+	/* Row leaked -EEXIST -> walker forwards it (contract violation). */
+	mock_reset();
+	mock.create_ret[0] = -EEXIST;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "dup"),
+			-EEXIST);
+}
+
+/*
+ * Test 7: -ENOMEM propagation
+ *
+ * Transient allocation failures must propagate unchanged to the caller
+ * so kernfs does not negatively cache the dentry.
+ */
+static void walk_enomem_propagates(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = "oom",
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+		},
+		{ }
+	};
+
+	mock.create_ret[0] = -ENOMEM;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "oom"),
+			-ENOMEM);
+	KUNIT_EXPECT_EQ(test, mock.n_calls, 1);
+}
+
+/*
+ * Test 8: REMOVE_ALL reverse order + ADD_ALL forward order
+ *
+ * ADD_ALL invokes every applicable create() in table order;
+ * REMOVE_ALL invokes every applicable remove() in reverse table order
+ * A gated-off middle row is skipped in both directions.
+ */
+static void walk_reverse_teardown(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			.name = "r0",
+			.applies_to = mock_applies_0,
+			.create = mock_create_0,
+			.remove = mock_remove_0,
+		},
+		{
+			.name = "r1",
+			.applies_to = mock_applies_1,
+			.create = mock_create_1,
+			.remove = mock_remove_1,
+		},
+		{
+			.name = "r2",
+			.applies_to = mock_applies_2,
+			.create = mock_create_2,
+			.remove = mock_remove_2,
+		},
+		{ }
+	};
+
+	/* Gate row 1 off; rows 0 and 2 remain active. */
+	mock.applies[1] = false;
+
+	/* ADD_ALL: forward order, rows 0 then 2. */
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ALL, NULL), 0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 2);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].op, MOCK_CREATE);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 0);
+	KUNIT_EXPECT_EQ(test, mock.calls[1].op, MOCK_CREATE);
+	KUNIT_EXPECT_EQ(test, mock.calls[1].idx, 2);
+
+	/* REMOVE_ALL: reverse order, rows 2 then 0. */
+	mock_reset();
+	mock.applies[1] = false;
+	KUNIT_EXPECT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_REMOVE_ALL, NULL),
+			0);
+	KUNIT_ASSERT_EQ(test, mock.n_calls, 2);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].op, MOCK_REMOVE);
+	KUNIT_EXPECT_EQ(test, mock.calls[0].idx, 2);
+	KUNIT_EXPECT_EQ(test, mock.calls[1].op, MOCK_REMOVE);
+	KUNIT_EXPECT_EQ(test, mock.calls[1].idx, 0);
+}
+
+/*
+ * Test 9: sysfs_lazy eager-leak regression guard
+ *
+ * Regression guard for the dpm_sysfs_add eager-leak: a
+ * sysfs_lazy device must have zero direct kernfs children at the
+ * device_add() return boundary.  Any sysfs_create_file,
+ * sysfs_create_group, or sysfs_create_link reached unconditionally
+ * from the device_add() path would materialize a child of
+ * dev->kobj.sd here and fail this test.
+ *
+ * This test uses its own freshly-allocated platform_device (not the
+ * fixture's pdev) so sysfs_lazy can be set BEFORE platform_device_add()
+ * -- the flag must be committed before device_add() per the
+ * Documentation/ABI/testing/sysfs-lazy contract.
+ *
+ * Phase 1: zero children immediately after device_add().
+ * Phase 2: children appear after device_sysfs_apply(ADD_ALL) -- proves
+ * the populate-triggered path still works.
+ */
+static void walk_lazy_device_has_no_eager_children(struct kunit *test)
+{
+	struct platform_device *lazy_pdev;
+	struct kernfs_node *sd, *kn;
+	struct rb_node *rb;
+	struct device *dev;
+	int child_count = 0;
+	int ret;
+
+	lazy_pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME "_lazy",
+					  PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_pdev);
+
+	/* MUST be set before device_add(); the alloc gates kernfs_set_lazy(). */
+	ret = device_set_sysfs_lazy(&lazy_pdev->dev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "device_set_sysfs_lazy failed: %d", ret);
+		return;
+	}
+
+	ret = platform_device_add(lazy_pdev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "platform_device_add failed: %d", ret);
+		return;
+	}
+
+	dev = &lazy_pdev->dev;
+	sd = dev->kobj.sd;
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sd);
+
+	/*
+	 * Phase 1: walk dev->kobj.sd's rbtree of direct children.
+	 * Expected: 0.  kernfs children are stored in kn->dir.children,
+	 * a struct rb_root; iterate via rb_first/rb_next.  No
+	 * kernfs_for_each_child() helper exists upstream as of this
+	 * commit.
+	 */
+	for (rb = rb_first(&sd->dir.children); rb; rb = rb_next(rb)) {
+		char nbuf[64];
+
+		kn = rb_entry(rb, struct kernfs_node, rb);
+		child_count++;
+		/*
+		 * Snapshot the kernfs node name into a stack buffer
+		 * via kernfs_name(), which takes its own guard(rcu)
+		 * internally. Emitting kunit_info() (which can
+		 * GFP_KERNEL-allocate inside the log path) directly
+		 * from inside an rcu_read_lock() section would
+		 * violate Documentation/RCU/checklist.rst section 7 ("anything
+		 * that the idle task does ... and especially nothing
+		 * that does GFP_KERNEL allocations") and trip
+		 * PROVE_RCU CI.
+		 */
+		kernfs_name(kn, nbuf, sizeof(nbuf));
+		kunit_info(test, "unexpected eager child: %s\n", nbuf);
+	}
+
+	KUNIT_EXPECT_EQ_MSG(test, child_count, 0,
+		"sysfs_lazy device has %d eager children; expected 0. Something bypasses the walker (grep call chain for sysfs_create_file / sysfs_create_group / sysfs_create_link called unconditionally from device_add path).",
+		child_count);
+
+	/*
+	 * Named-node checks for two rows that must stay absent on a lazy
+	 * device before populate: power/ (created by dpm_sysfs_add) and
+	 * the device-side driver symlink (created by driver_sysfs_add).
+	 * kernfs_find_and_get() does NOT trigger populate -- it walks
+	 * the existing rb_tree only -- so it is safe to use here for
+	 * the before-populate assertion.
+	 */
+	kn = kernfs_find_and_get(sd, "power");
+	KUNIT_EXPECT_PTR_EQ_MSG(test, kn, (struct kernfs_node *)NULL,
+		"sysfs_lazy device has eager power/ directory; dpm_sysfs_add() is bypassing the !sysfs_lazy gate in device_add().");
+	if (kn)
+		kernfs_put(kn);
+
+	kn = kernfs_find_and_get(sd, "driver");
+	KUNIT_EXPECT_PTR_EQ_MSG(test, kn, (struct kernfs_node *)NULL,
+		"sysfs_lazy device has eager driver symlink; driver_sysfs_add() is bypassing the !sysfs_lazy gate.");
+	if (kn)
+		kernfs_put(kn);
+
+	/*
+	 * Phase 2: simulate a readdir-triggered populate by invoking
+	 * the walker directly with the device ktype's entries table.
+	 * For device_ktype this resolves to driver_core_sysfs_entries[]
+	 * (static inside core.c; reached here via the public
+	 * kobj_type.entries pointer).  Children must now appear,
+	 * proving the lazy populate path is functional and the
+	 * zero-child assertion above is not just a dead-code artefact.
+	 */
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev->kobj.ktype);
+	/*
+	 * Production callers (device_ktype_populate_all() in core.c)
+	 * hold dev->sysfs_lazy->lock across device_sysfs_apply()
+	 * so the create()/remove() callbacks see the contract documented by
+	 * their lockdep_assert_held(&...->lock). Tests calling
+	 * the walker directly must hold the same mutex; otherwise the
+	 * assert WARNs (one per create() row touched).
+	 */
+	mutex_lock(&dev->sysfs_lazy->lock);
+	ret = device_sysfs_apply(dev, dev->kobj.ktype->entries,
+				DEV_SYSFS_ADD_ALL, NULL);
+	mutex_unlock(&dev->sysfs_lazy->lock);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+
+	child_count = 0;
+	for (rb = rb_first(&sd->dir.children); rb; rb = rb_next(rb))
+		child_count++;
+	KUNIT_EXPECT_GT_MSG(test, child_count, 0,
+		"device_sysfs_apply(ADD_ALL) on a lazy device created no children; the populate-triggered path is broken.");
+
+	/*
+	 * Spot-check a specific unconditional row (uevent) to catch a
+	 * mis-wired walker that created something but not the expected
+	 * rows.
+	 */
+	kn = kernfs_find_and_get(sd, "uevent");
+	KUNIT_EXPECT_PTR_NE_MSG(test, kn, (struct kernfs_node *)NULL,
+		"uevent attr missing after device_sysfs_apply(ADD_ALL); populate-triggered path created something else but not the expected driver_core_sysfs_entries[] rows.");
+	if (kn)
+		kernfs_put(kn);
+
+	/*
+	 * Teardown: device_del()'s built-in reverse REMOVE_ALL pass
+	 * (sole power-group teardown path per device_del() comment in
+	 * drivers/base/core.c) tears down everything Phase 2 added.
+	 * A manual REMOVE_ALL here would invoke remove_power() twice
+	 * on a lazy device whose power/ group was realised in Phase
+	 * 2: the second pass would WARN inside dpm_sysfs_remove()
+	 * because the remove_power() gate keys on the one-way
+	 * ->power_added latch (set in create_power(), never cleared
+	 * - see create_power()'s comment on lock
+	 * serialisation forbidding re-realise without device_del).
+	 */
+	platform_device_unregister(lazy_pdev);
+}
+
+/* Test 10: equivalence -- eager vs lazy produce the same kernfs children */
+
+static void walk_eager_lazy_equivalence(struct kunit *test)
+{
+	struct platform_device *eager_pdev, *lazy_pdev;
+	struct kernfs_node *eager_sd, *lazy_sd, *kn;
+	struct rb_node *rb;
+	int ret, mismatches = 0;
+
+	eager_pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME "_eager",
+					   PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, eager_pdev);
+
+	ret = platform_device_add(eager_pdev);
+	if (ret) {
+		platform_device_put(eager_pdev);
+		KUNIT_FAIL(test, "eager platform_device_add: %d", ret);
+		return;
+	}
+
+	lazy_pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME "_lazy2",
+					  PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_pdev);
+
+	ret = device_set_sysfs_lazy(&lazy_pdev->dev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		platform_device_unregister(eager_pdev);
+		KUNIT_FAIL(test, "device_set_sysfs_lazy: %d", ret);
+		return;
+	}
+
+	ret = platform_device_add(lazy_pdev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		platform_device_unregister(eager_pdev);
+		KUNIT_FAIL(test, "lazy platform_device_add: %d", ret);
+		return;
+	}
+
+	/* Trigger populate_all on lazy device */
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_pdev->dev.kobj.ktype);
+	/*
+	 * Production callers hold lock across
+	 * device_sysfs_apply() - see device_ktype_populate_all() in
+	 * core.c - so create()/remove() callbacks observe the lockdep contract
+	 * declared by their lockdep_assert_held(&...->lock).
+	 */
+	mutex_lock(&lazy_pdev->dev.sysfs_lazy->lock);
+	device_sysfs_apply(&lazy_pdev->dev,
+			  lazy_pdev->dev.kobj.ktype->entries,
+			  DEV_SYSFS_ADD_ALL, NULL);
+	mutex_unlock(&lazy_pdev->dev.sysfs_lazy->lock);
+
+	eager_sd = eager_pdev->dev.kobj.sd;
+	lazy_sd = lazy_pdev->dev.kobj.sd;
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, eager_sd);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_sd);
+
+	/* Every eager child must exist in lazy */
+	for (rb = rb_first(&eager_sd->dir.children); rb; rb = rb_next(rb)) {
+		struct kernfs_node *eager_kn =
+			rb_entry(rb, struct kernfs_node, rb);
+		char namebuf[64];
+
+		if (kernfs_name(eager_kn, namebuf, sizeof(namebuf)) <= 0)
+			continue;
+
+		kn = kernfs_find_and_get(lazy_sd, namebuf);
+		if (!kn) {
+			kunit_info(test, "eager '%s' missing from lazy",
+				   namebuf);
+			mismatches++;
+		} else {
+			if ((eager_kn->flags & KERNFS_TYPE_MASK) !=
+			    (kn->flags & KERNFS_TYPE_MASK)) {
+				kunit_info(test, "type mismatch '%s'",
+					   namebuf);
+				mismatches++;
+			}
+			kernfs_put(kn);
+		}
+	}
+
+	/* Reverse: lazy children not in eager */
+	for (rb = rb_first(&lazy_sd->dir.children); rb; rb = rb_next(rb)) {
+		struct kernfs_node *lazy_kn =
+			rb_entry(rb, struct kernfs_node, rb);
+		char namebuf[64];
+
+		if (kernfs_name(lazy_kn, namebuf, sizeof(namebuf)) <= 0)
+			continue;
+
+		kn = kernfs_find_and_get(eager_sd, namebuf);
+		if (!kn) {
+			kunit_info(test, "lazy '%s' not in eager",
+				   namebuf);
+			mismatches++;
+		} else {
+			kernfs_put(kn);
+		}
+	}
+
+	KUNIT_EXPECT_EQ_MSG(test, mismatches, 0,
+		"eager and lazy devices have different sysfs children after populate_all");
+
+	/*
+	 * No manual REMOVE_ALL: device_del() (via
+	 * platform_device_unregister) is the sole power-group
+	 * teardown path; manual REMOVE_ALL here would double-run
+	 * remove_power() and WARN. See test 9 teardown comment.
+	 */
+	platform_device_unregister(lazy_pdev);
+	platform_device_unregister(eager_pdev);
+}
+
+/*
+ * Test 11: wildcard-row idempotency under populate_one-then-populate_all
+ *
+ * Regression guard for the pci_create_resource_files() non-idempotency
+ * bug: a wildcard row whose
+ * create() materialises one slot on ADD_ONE("name") and all slots on
+ * ADD_ALL must leave the ADD_ONE-created slot intact when ADD_ALL fires
+ * afterwards.  kernfs's lookup-then-readdir order routes a stat("foo")
+ * through populate_one() before the subsequent readdir() reaches
+ * populate_all(); a non-idempotent ADD_ALL would tear down the
+ * already-created "foo" entry and make it user-visibly disappear.
+ *
+ * The mock simulates a row with two slots ("0" and "1").  create() with
+ * a non-NULL name parses the index and sets created[idx] = true.
+ * create() with NULL iterates both slots and sets the missing ones,
+ * mirroring pci_create_resource_files()'s post-fix shape.  A buggy
+ * create_all that re-creates already-set slots without skipping would
+ * be detected by leaving create_count incremented past the expected
+ * 2 - exactly the disappearance vector the production bug had.
+ */
+
+#define WILDCARD_SLOTS	2
+
+struct wildcard_state {
+	bool created[WILDCARD_SLOTS];
+	int create_count;	/* total successful realisations */
+};
+
+static struct wildcard_state wildcard_st;
+
+static int wildcard_create(struct device *dev, const char *name)
+{
+	int idx;
+
+	if (name) {
+		/* ADD_ONE: parse "0" or "1". */
+		if (kstrtoint(name, 10, &idx))
+			return -ENOENT;
+		if (idx < 0 || idx >= WILDCARD_SLOTS)
+			return -ENOENT;
+
+		if (wildcard_st.created[idx]) {
+			/*
+			 * Production rows like pci_create_attr() return
+			 * -EEXIST here; the row contract requires
+			 * absorption, but ADD_ONE will not revisit a slot
+			 * twice in a real run, so this branch is purely
+			 * defensive in the test.
+			 */
+			return 0;
+		}
+		wildcard_st.created[idx] = true;
+		wildcard_st.create_count++;
+		return 0;
+	}
+
+	/* ADD_ALL: walk both slots, skip already-created (idempotent). */
+	for (idx = 0; idx < WILDCARD_SLOTS; idx++) {
+		if (wildcard_st.created[idx])
+			continue;
+		wildcard_st.created[idx] = true;
+		wildcard_st.create_count++;
+	}
+	return 0;
+}
+
+static void wildcard_remove(struct device *dev)
+{
+	int idx;
+
+	for (idx = 0; idx < WILDCARD_SLOTS; idx++)
+		wildcard_st.created[idx] = false;
+}
+
+static void walk_wildcard_row_idempotent(struct kunit *test)
+{
+	struct walk_test_priv *priv = test->priv;
+	struct device *dev = &priv->pdev->dev;
+	static const struct device_sysfs_entry tbl[] = {
+		{
+			/* wildcard row: create() handles both modes */
+			.create = wildcard_create,
+			.remove = wildcard_remove,
+		},
+		{ }
+	};
+
+	memset(&wildcard_st, 0, sizeof(wildcard_st));
+
+	/* Step 1: populate_one("0") creates slot 0. */
+	KUNIT_ASSERT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ONE, "0"), 0);
+	KUNIT_EXPECT_TRUE(test, wildcard_st.created[0]);
+	KUNIT_EXPECT_FALSE(test, wildcard_st.created[1]);
+	KUNIT_EXPECT_EQ(test, wildcard_st.create_count, 1);
+
+	/* Step 2: populate_all walks both slots, must skip slot 0. */
+	KUNIT_ASSERT_EQ(test, device_sysfs_apply(dev, tbl,
+						DEV_SYSFS_ADD_ALL, NULL), 0);
+	KUNIT_EXPECT_TRUE_MSG(test, wildcard_st.created[0],
+		"slot 0 disappeared after populate_all - create_all is not idempotent (would correspond to pci_remove_resource_files() nuking already-created BAR slots after a populate_one + populate_all sequence).");
+	KUNIT_EXPECT_TRUE(test, wildcard_st.created[1]);
+	KUNIT_EXPECT_EQ_MSG(test, wildcard_st.create_count, 2,
+		"create_all re-created an already-populated slot; idempotency contract violated.");
+}
+
+/*
+ * Test 12: populate_one vs populate_all kthread race
+ *
+ * Stress the lock + populated-latch double-check protocol that
+ * serialises lazy population dispatched from kernfs's populate_one() /
+ * populate_all() hooks. Two kernel threads loop on the device's ktype
+ * function pointers concurrently for ~100 ms on a freshly-added lazy
+ * platform_device:
+ *
+ * T1: dev->kobj.ktype->populate(&dev->kobj, "uevent")
+ * T2: dev->kobj.ktype->populate_all(&dev->kobj)
+ *
+ * T1 reaches device_ktype_populate_one() and T2 reaches
+ * device_ktype_populate_all() in core.c. Both take
+ * dev->sysfs_lazy->lock and re-check the populated latch
+ * under the lock; the loser must short-circuit cleanly without
+ * re-walking entries the winner already realised. The lock protocol makes the
+ * lazy populate paths race-free: create callbacks check existence
+ * under the same lock that excludes other lazy creators, so
+ * sysfs_warn_dup() inside sysfs_add_file_mode_ns() must NOT fire at
+ * all (not even once, absorbed by create()'s -EEXIST handling).
+ *
+ * Asserts:
+ * - Zero new sysfs_warn_dup() invocations during the race window.
+ * This is the lock invariant: the lock fully excludes parallel
+ * creators, so no duplicate-create WARN can occur.
+ * - No new TAINT_WARN bit set during the race window (covers other
+ * WARN_ON paths -- e.g. a sysfs_remove_group() WARN -- since
+ * sysfs_warn_dup() itself is pr_warn-only and does not taint).
+ * - dev->sysfs_lazy->populated == true (latch committed).
+ * - "uevent" attribute present in the device's kernfs directory
+ * (the named row T1 was racing on actually materialised).
+ * - Both threads recorded > 0 iterations (proves both got CPU).
+ */
+
+struct race_state {
+	struct device		*dev;
+	struct walk_thread_pair	*pair;
+};
+
+static int populate_one_worker(void *data)
+{
+	struct race_state *st = data;
+	const struct kobj_type *ktype = st->dev->kobj.ktype;
+
+	while (!kthread_should_stop()) {
+		int ret = ktype->populate(&st->dev->kobj, "uevent");
+		/*
+		 * Allowed: 0 (created) or -ENOENT (already populated, or
+		 * dev->p->dead set by a concurrent device_del - covered
+		 * by walk_populate_vs_device_del_race rather than this
+		 * test, but the worker is shared so we tolerate both).
+		 */
+		if (ret != 0 && ret != -ENOENT)
+			atomic_inc(&st->pair->bad_results);
+		atomic_inc(&st->pair->iters1);
+		cond_resched();
+	}
+	return 0;
+}
+
+static int populate_all_worker(void *data)
+{
+	struct race_state *st = data;
+	const struct kobj_type *ktype = st->dev->kobj.ktype;
+
+	while (!kthread_should_stop()) {
+		ktype->populate_all(&st->dev->kobj);
+		atomic_inc(&st->pair->iters2);
+		cond_resched();
+	}
+	return 0;
+}
+
+static void walk_populate_one_vs_all_race(struct kunit *test)
+{
+	struct platform_device *lazy_pdev;
+	struct walk_thread_pair pair = {};
+	struct race_state st = { .pair = &pair };
+	struct kernfs_node *kn;
+	bool warn_taint_before;
+	int dup_warn_before;
+	int ret;
+
+	lazy_pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME "_race1",
+					  PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_pdev);
+
+	ret = device_set_sysfs_lazy(&lazy_pdev->dev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "device_set_sysfs_lazy: %d", ret);
+		return;
+	}
+
+	ret = platform_device_add(lazy_pdev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "platform_device_add: %d", ret);
+		return;
+	}
+
+	st.dev = &lazy_pdev->dev;
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, st.dev->kobj.ktype);
+	KUNIT_ASSERT_NOT_NULL(test, st.dev->kobj.ktype->populate);
+	KUNIT_ASSERT_NOT_NULL(test, st.dev->kobj.ktype->populate_all);
+
+	warn_taint_before = test_taint(TAINT_WARN);
+	dup_warn_before = atomic_read(&sysfs_warn_dup_kunit_count);
+
+	pair.t1 = walk_thread_start(test, populate_one_worker, &st,
+				    "walk_race1_one");
+	pair.t2 = walk_thread_start(test, populate_all_worker, &st,
+				    "walk_race1_all");
+
+	/* Run the race for ~100 ms. */
+	msleep(100);
+
+	walk_thread_pair_stop(test, &pair);
+
+	/*
+	 * Lock invariant: lazy populate paths under lock are
+	 * race-free. If this fires, sysfs_warn_dup() emitted at least one
+	 * "cannot create duplicate filename" warning in the race window,
+	 * meaning a create callback raced past its existence check and
+	 * collided with another lazy creator -- the lock
+	 * exclusivity contract is broken.
+	 */
+	KUNIT_EXPECT_EQ_MSG(test,
+		atomic_read(&sysfs_warn_dup_kunit_count), dup_warn_before,
+		"sysfs_warn_dup() fired during populate_one vs populate_all race; lock invariant violated.");
+
+	/* No WARN should have fired in the race window. */
+	KUNIT_EXPECT_FALSE_MSG(test,
+		!warn_taint_before && test_taint(TAINT_WARN),
+		"TAINT_WARN set during populate_one vs populate_all race; sysfs_warn_dup() likely fired (lock protocol violation).");
+
+	/* No worker observed an unexpected return value. */
+	KUNIT_EXPECT_EQ_MSG(test, atomic_read(&pair.bad_results), 0,
+		"populate_one returned an unexpected error during race");
+
+	/* populated latch must be set after at least one populate_all. */
+	KUNIT_EXPECT_TRUE_MSG(test, device_sysfs_populated(st.dev),
+		"populate_all completed but populated latch was not set");
+
+	/* "uevent" must exist in kernfs; populate_one was racing for it. */
+	kn = kernfs_find_and_get(st.dev->kobj.sd, "uevent");
+	KUNIT_EXPECT_PTR_NE_MSG(test, kn, (struct kernfs_node *)NULL,
+		"uevent attr missing after race; populate_one or populate_all failed to materialise it");
+	if (kn)
+		kernfs_put(kn);
+
+	/* Sanity: both threads got CPU time. */
+	KUNIT_EXPECT_GT(test, atomic_read(&pair.iters1), 0);
+	KUNIT_EXPECT_GT(test, atomic_read(&pair.iters2), 0);
+
+	platform_device_unregister(lazy_pdev);
+}
+
+/*
+ * Test 13: populate vs device_del kthread race
+ *
+ * Stress the lock wrap + dev->p->dead re-check that gates
+ * concurrent populate_one() against an in-flight device_del(). Worker A
+ * loops populate_one() on a lazy platform_device; Worker B sleeps
+ * briefly then calls platform_device_unregister(), which triggers
+ * device_del() and sets dev->p->dead under device_lock. Worker A holds
+ * an extra get_device() reference so its populate_one() callees can
+ * still safely dereference dev->sysfs_lazy after device_del() returns;
+ * dev->sysfs_lazy is freed by device_release(), which is gated on the
+ * refcount and therefore does not run until Worker A drops its ref.
+ *
+ * Asserts:
+ * - Zero new sysfs_warn_dup() invocations during the race window.
+ * The lock protocol makes the lazy populate paths race-free under
+ * lock; combined with the dev->p->dead re-check, even
+ * a populate_one() in flight when device_del() arrives must not
+ * emit a dup-warn (it either created cleanly before dead was set,
+ * or it observed dead and bailed without touching kernfs).
+ * - No new TAINT_WARN bit set (covers WARN_ON paths -- e.g. a
+ * sysfs_remove_group("power") WARN, or a use-after-free splat).
+ * - Every populate_one() observed AFTER the unregister returns
+ * -ENOENT (driven by the dev->p->dead re-check inside
+ * device_ktype_populate_one()).
+ * - Worker A made forward progress (iters > 0).
+ */
+
+struct dead_race_state {
+	struct device		*dev;
+	struct platform_device	*pdev;
+	struct walk_thread_pair	*pair;
+	bool			device_del_done;
+};
+
+static int populate_one_until_dead_worker(void *data)
+{
+	struct dead_race_state *st = data;
+	const struct kobj_type *ktype = st->dev->kobj.ktype;
+
+	while (!kthread_should_stop()) {
+		int ret = ktype->populate(&st->dev->kobj, "uevent");
+
+		/*
+		 * Permitted return values:
+		 *   0       - created (only legal before device_del)
+		 *   -ENOENT - already populated, OR dev->p->dead set
+		 * Any other return value is a protocol violation.
+		 */
+		if (ret != 0 && ret != -ENOENT)
+			atomic_inc(&st->pair->bad_results);
+
+		/*
+		 * After the unregister has been observed by Worker B,
+		 * dev->p->dead is true; populate_one MUST return
+		 * -ENOENT. A 0 here would mean the dead re-check failed
+		 * to fire (and we'd be racing with sysfs teardown).
+		 */
+		if (READ_ONCE(st->device_del_done) && ret == 0)
+			atomic_inc(&st->pair->bad_results);
+
+		atomic_inc(&st->pair->iters1);
+		cond_resched();
+	}
+	return 0;
+}
+
+static int unregister_worker(void *data)
+{
+	struct dead_race_state *st = data;
+
+	/* Let the populate worker rack up some iterations first. */
+	usleep_range(10 * USEC_PER_MSEC, 20 * USEC_PER_MSEC);
+	platform_device_unregister(st->pdev);
+	WRITE_ONCE(st->device_del_done, true);
+	atomic_inc(&st->pair->iters2);
+
+	while (!kthread_should_stop())
+		schedule_timeout_interruptible(HZ / 10);
+	return 0;
+}
+
+static void walk_populate_vs_device_del_race(struct kunit *test)
+{
+	struct platform_device *lazy_pdev;
+	struct walk_thread_pair pair = {};
+	struct dead_race_state st = { .pair = &pair };
+	bool warn_taint_before;
+	int dup_warn_before;
+	int ret;
+
+	lazy_pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME "_race2",
+					  PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_pdev);
+
+	ret = device_set_sysfs_lazy(&lazy_pdev->dev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "device_set_sysfs_lazy: %d", ret);
+		return;
+	}
+
+	ret = platform_device_add(lazy_pdev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "platform_device_add: %d", ret);
+		return;
+	}
+
+	st.pdev = lazy_pdev;
+	st.dev = &lazy_pdev->dev;
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, st.dev->kobj.ktype);
+	KUNIT_ASSERT_NOT_NULL(test, st.dev->kobj.ktype->populate);
+
+	/*
+	 * Hold an extra reference so the populate worker can keep
+	 * dereferencing dev->sysfs_lazy after platform_device_unregister
+	 * returns. device_release() (which kfree()'s sysfs_lazy) is
+	 * gated on the refcount and runs when this ref is dropped.
+	 */
+	get_device(st.dev);
+
+	warn_taint_before = test_taint(TAINT_WARN);
+	dup_warn_before = atomic_read(&sysfs_warn_dup_kunit_count);
+
+	pair.t1 = walk_thread_start(test, populate_one_until_dead_worker, &st,
+				    "walk_race2_pop");
+	pair.t2 = walk_thread_start(test, unregister_worker, &st,
+				    "walk_race2_del");
+
+	/* Run for ~100 ms; B finishes early, A keeps looping. */
+	msleep(100);
+
+	walk_thread_pair_stop(test, &pair);
+
+	/*
+	 * Drop the test's extra ref. device_release() now fires (frees
+	 * dev->sysfs_lazy and the platform_device); after this point
+	 * st.dev is invalid. KUnit assertions below must NOT touch it.
+	 */
+	put_device(st.dev);
+	st.dev = NULL;
+
+	/*
+	 * Lock invariant: lazy populate paths under lock are
+	 * race-free, including against concurrent device_del. If this
+	 * fires, sysfs_warn_dup() emitted at least one "cannot create
+	 * duplicate filename" warning -- meaning a populate_one() in
+	 * flight against device_del() either raced past the dev->p->dead
+	 * re-check or collided with another lazy creator. Both are
+	 * lock invariant violations.
+	 */
+	KUNIT_EXPECT_EQ_MSG(test,
+		atomic_read(&sysfs_warn_dup_kunit_count), dup_warn_before,
+		"sysfs_warn_dup() fired during populate vs device_del race; lock or dev->p->dead re-check invariant violated.");
+
+	KUNIT_EXPECT_FALSE_MSG(test,
+		!warn_taint_before && test_taint(TAINT_WARN),
+		"TAINT_WARN set during populate vs device_del race; check for sysfs_warn_dup, sysfs_remove_group WARN, or use-after-free splat in dmesg.");
+
+	KUNIT_EXPECT_EQ_MSG(test, atomic_read(&pair.bad_results), 0,
+		"populate_one returned an unexpected value during or after device_del; the dev->p->dead re-check is broken.");
+
+	KUNIT_EXPECT_TRUE_MSG(test, READ_ONCE(st.device_del_done),
+		"unregister worker did not complete platform_device_unregister within the race window");
+
+	KUNIT_EXPECT_GT(test, atomic_read(&pair.iters1), 0);
+	KUNIT_EXPECT_GT(test, atomic_read(&pair.iters2), 0);
+}
+
+/*
+ * Test 14: create_power() -ENOMEM teardown safety (gate equivalence)
+ *
+ * create_power() commits dev->sysfs_lazy->power_added = true ONLY on
+ * a successful dpm_sysfs_add(); on -ENOMEM the latch stays false.
+ * remove_power() consults the same latch on teardown:
+ *
+ * if (device_is_sysfs_lazy(dev) && !dev->sysfs_lazy->power_added)
+ * return;
+ * dpm_sysfs_remove(dev);
+ *
+ * When power_added is false, dpm_sysfs_remove() is skipped - without
+ * this gate, sysfs_remove_group("power") would WARN inside
+ * sysfs_remove_group() on a never-materialised group (and PM QoS
+ * constraints would never be torn down because dpm_sysfs_remove() is
+ * their sole release path).
+ *
+ * This test exercises the gate via a structurally-equivalent path: a
+ * lazy device whose populate path is never invoked. create_power()
+ * never runs, so power_added stays false - the same observable state
+ * a -ENOMEM from dpm_sysfs_add() would produce. device_del() then runs
+ * the walker in REMOVE_ALL direction; if remove_power() fails to skip
+ * dpm_sysfs_remove(), TAINT_WARN is set.
+ *
+ * A separate CONFIG_FAULT_INJECTION-gated test in a follow-up series
+ * can additionally inject -ENOMEM into dpm_sysfs_add() to cover the
+ * exact path described above; the gate logic itself is
+ * identical and is the load-bearing assertion here.
+ */
+static void walk_create_power_enomem(struct kunit *test)
+{
+	struct platform_device *lazy_pdev;
+	bool warn_taint_before;
+	int ret;
+
+	lazy_pdev = platform_device_alloc(APPLY_KUNIT_DEV_NAME "_powfail",
+					  PLATFORM_DEVID_NONE);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, lazy_pdev);
+
+	ret = device_set_sysfs_lazy(&lazy_pdev->dev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "device_set_sysfs_lazy: %d", ret);
+		return;
+	}
+
+	ret = platform_device_add(lazy_pdev);
+	if (ret) {
+		platform_device_put(lazy_pdev);
+		KUNIT_FAIL(test, "platform_device_add: %d", ret);
+		return;
+	}
+
+	/*
+	 * Pre-condition: lazy device, no populate_one or populate_all
+	 * has been invoked, so the walker has not run and
+	 * create_power() has not committed power_added. This is the
+	 * SAME observable state a -ENOMEM from dpm_sysfs_add() inside
+	 * create_power() would leave behind.
+	 */
+	KUNIT_ASSERT_TRUE(test, device_is_sysfs_lazy(&lazy_pdev->dev));
+	KUNIT_ASSERT_FALSE(test, lazy_pdev->dev.sysfs_lazy->power_added);
+
+	warn_taint_before = test_taint(TAINT_WARN);
+
+	/*
+	 * device_del() walks REMOVE_ALL; remove_power() must consult
+	 * the power_added latch and skip dpm_sysfs_remove(). If the
+	 * gate is broken, sysfs_remove_group("power") fires
+	 * sysfs_warn() because the group was never created, and
+	 * TAINT_WARN gets set.
+	 */
+	platform_device_unregister(lazy_pdev);
+
+	KUNIT_EXPECT_FALSE_MSG(test,
+		!warn_taint_before && test_taint(TAINT_WARN),
+		"TAINT_WARN set on device_del of lazy unpopulated device; remove_power() failed to skip dpm_sysfs_remove() when power_added==false (sysfs_remove_group(\"power\") WARN).");
+}
+
+/*
+ * kernfs_set_lazy() input-validation branch coverage.
+ *
+ * kernfs_set_lazy() rejects three classes of bad input by returning
+ * -EINVAL without modifying @kn:
+ * (1) namespaced kn   -- kn->ns != NULL
+ * (2) NS-enabled kn   -- kn->flags & KERNFS_NS
+ * (3) non-DIR kn      -- kernfs_type(kn) != KERNFS_DIR
+ *
+ * Each branch is exercised on a freshly created kernfs root (no shared
+ * state with the platform_device fixture in walk_test_init); successful
+ * rejection is confirmed by (a) the return value being -EINVAL and
+ * (b) KERNFS_LAZY remaining unset on the node.
+ *
+ * A fourth, positive case asserts that a plain DIR kn (no ns, no NS
+ * flag) is accepted: the call returns 0 and KERNFS_LAZY is set. This
+ * is the "happy path" gate; without it a refactor that turned the
+ * rejection check into an unconditional return would still pass the
+ * three negative tests.
+ */
+
+/*
+ * Non-NULL ns_common sentinel. kernfs_set_lazy() only tests @kn->ns for
+ * NULL-ness and never dereferences it, so a single byte of static storage
+ * whose address is reinterpreted as `const struct ns_common *` is a safe
+ * "namespace-tagged" marker without pulling in <linux/ns_common.h>
+ * (kernfs.h only forward-declares struct ns_common).
+ */
+static const u8 kernfs_set_lazy_dummy_ns_marker;
+#define KERNFS_SET_LAZY_DUMMY_NS \
+	((const struct ns_common *)&kernfs_set_lazy_dummy_ns_marker)
+
+/* Empty kernfs_ops for non-DIR file creation. */
+static const struct kernfs_ops kernfs_set_lazy_dummy_ops;
+
+static void walk_kernfs_set_lazy_rejects_namespaced_kn(struct kunit *test)
+{
+	struct kernfs_root *root;
+	struct kernfs_node *kn;
+	const struct ns_common *saved_ns;
+
+	root = kernfs_create_root(NULL, 0, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+
+	kn = kernfs_create_dir(kernfs_root_to_node(root), "ns_kn",
+			       S_IRUGO | S_IXUGO, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, kn);
+
+	/*
+	 * Stash a non-NULL ns tag on the node directly. kernfs_create_dir_ns()
+	 * would also accept @ns but only when the parent has KERNFS_NS set
+	 * (kernfs_add_one() WARNs otherwise); setting @kn->ns post-creation
+	 * exercises kernfs_set_lazy()'s namespaced-kn rejection in isolation
+	 * from any add-time validation.
+	 */
+	saved_ns = kn->ns;
+	kn->ns = KERNFS_SET_LAZY_DUMMY_NS;
+
+	KUNIT_EXPECT_EQ_MSG(test, kernfs_set_lazy(kn), -EINVAL,
+		"kernfs_set_lazy() did not return -EINVAL on a namespaced kn (kn->ns != NULL).");
+
+	/*
+	 * Defense in depth: KERNFS_LAZY must remain unset on a kn that
+	 * violated the precondition.  -EINVAL alone is necessary but not
+	 * sufficient; a refactor that returned -EINVAL after setting the
+	 * flag would still leave the kn in a half-armed state.
+	 */
+	KUNIT_EXPECT_FALSE_MSG(test,
+		kn->flags & KERNFS_LAZY,
+		"kernfs_set_lazy() set KERNFS_LAZY on a namespaced kn (kn->ns != NULL).");
+
+	/* Restore so kernfs_remove() / destroy_root see a clean state. */
+	kn->ns = saved_ns;
+
+	kernfs_destroy_root(root);
+}
+
+static void walk_kernfs_set_lazy_rejects_kernfs_ns_flag(struct kunit *test)
+{
+	struct kernfs_root *root;
+	struct kernfs_node *kn;
+
+	root = kernfs_create_root(NULL, 0, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+
+	kn = kernfs_create_dir(kernfs_root_to_node(root), "ns_flag_kn",
+			       S_IRUGO | S_IXUGO, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, kn);
+
+	/*
+	 * Set KERNFS_NS on this DIR via the public helper. kernfs_enable_ns()
+	 * has its own WARN_ON_ONCE for non-DIR / non-empty children; we are
+	 * a freshly created empty DIR so neither fires here.
+	 */
+	kernfs_enable_ns(kn);
+
+	KUNIT_EXPECT_EQ_MSG(test, kernfs_set_lazy(kn), -EINVAL,
+		"kernfs_set_lazy() did not return -EINVAL on a KERNFS_NS-flagged kn.");
+
+	/*
+	 * Defense in depth (see namespaced-kn test): -EINVAL alone is
+	 * necessary but not sufficient; a refactor that returned -EINVAL
+	 * after setting KERNFS_LAZY would still leave the kn half-armed.
+	 */
+	KUNIT_EXPECT_FALSE_MSG(test,
+		kn->flags & KERNFS_LAZY,
+		"kernfs_set_lazy() set KERNFS_LAZY on a KERNFS_NS-flagged kn.");
+
+	kernfs_destroy_root(root);
+}
+
+static void walk_kernfs_set_lazy_rejects_non_dir_kn(struct kunit *test)
+{
+	struct kernfs_root *root;
+	struct kernfs_node *file_kn;
+
+	root = kernfs_create_root(NULL, 0, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+
+	/*
+	 * Create a KERNFS_FILE node directly under the root. The empty
+	 * kernfs_ops is fine for this test -- we never open or read the
+	 * file, only call kernfs_set_lazy() on it.
+	 */
+	file_kn = __kernfs_create_file(kernfs_root_to_node(root), "file_kn",
+				       S_IRUGO, GLOBAL_ROOT_UID,
+				       GLOBAL_ROOT_GID, 0,
+				       &kernfs_set_lazy_dummy_ops, NULL,
+				       NULL, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file_kn);
+	KUNIT_ASSERT_NE_MSG(test, kernfs_type(file_kn), KERNFS_DIR,
+			    "Test fixture sanity: file_kn must not be a DIR.");
+
+	KUNIT_EXPECT_EQ_MSG(test, kernfs_set_lazy(file_kn), -EINVAL,
+		"kernfs_set_lazy() did not return -EINVAL on a non-DIR (KERNFS_FILE) kn.");
+
+	/*
+	 * Defense in depth (see namespaced-kn test): -EINVAL alone is
+	 * necessary but not sufficient; a refactor that returned -EINVAL
+	 * after setting KERNFS_LAZY would still leave the kn half-armed.
+	 */
+	KUNIT_EXPECT_FALSE_MSG(test,
+		file_kn->flags & KERNFS_LAZY,
+		"kernfs_set_lazy() set KERNFS_LAZY on a non-DIR (KERNFS_FILE) kn.");
+
+	kernfs_destroy_root(root);
+}
+
+static void walk_kernfs_set_lazy_accepts_plain_dir(struct kunit *test)
+{
+	struct kernfs_root *root;
+	struct kernfs_node *kn;
+
+	root = kernfs_create_root(NULL, 0, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+
+	kn = kernfs_create_dir(kernfs_root_to_node(root), "plain_kn",
+			       S_IRUGO | S_IXUGO, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, kn);
+	KUNIT_ASSERT_PTR_EQ(test, kn->ns, NULL);
+	KUNIT_ASSERT_FALSE(test, kn->flags & KERNFS_NS);
+	KUNIT_ASSERT_EQ(test, kernfs_type(kn), (unsigned int)KERNFS_DIR);
+
+	KUNIT_EXPECT_EQ_MSG(test, kernfs_set_lazy(kn), 0,
+		"kernfs_set_lazy() returned non-zero on a plain DIR kn.");
+	KUNIT_EXPECT_TRUE_MSG(test,
+		kn->flags & KERNFS_LAZY,
+		"kernfs_set_lazy() did not set KERNFS_LAZY on a plain DIR kn.");
+
+	kernfs_destroy_root(root);
+}
+
+
+static struct kunit_case device_sysfs_apply_cases[] = {
+	KUNIT_CASE(walk_empty_table),
+	KUNIT_CASE(walk_single_row),
+	KUNIT_CASE(walk_applies_to_false),
+	KUNIT_CASE(walk_wildcard_row),
+	KUNIT_CASE(walk_two_rows_same_name),
+	KUNIT_CASE(walk_eexist_propagates),
+	KUNIT_CASE(walk_enomem_propagates),
+	KUNIT_CASE(walk_reverse_teardown),
+	KUNIT_CASE(walk_lazy_device_has_no_eager_children),
+	KUNIT_CASE(walk_eager_lazy_equivalence),
+	KUNIT_CASE(walk_wildcard_row_idempotent),
+	KUNIT_CASE_SLOW(walk_populate_one_vs_all_race),
+	KUNIT_CASE_SLOW(walk_populate_vs_device_del_race),
+	KUNIT_CASE(walk_create_power_enomem),
+	KUNIT_CASE(walk_kernfs_set_lazy_rejects_namespaced_kn),
+	KUNIT_CASE(walk_kernfs_set_lazy_rejects_kernfs_ns_flag),
+	KUNIT_CASE(walk_kernfs_set_lazy_rejects_non_dir_kn),
+	KUNIT_CASE(walk_kernfs_set_lazy_accepts_plain_dir),
+	{ }
+};
+
+static struct kunit_suite device_sysfs_apply_suite = {
+	.name = "device_sysfs_apply",
+	.init = walk_test_init,
+	.exit = walk_test_exit,
+	.test_cases = device_sysfs_apply_cases,
+};
+
+kunit_test_suite(device_sysfs_apply_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for device_sysfs_apply()");
diff --git a/fs/sysfs/dir.c b/fs/sysfs/dir.c
index ae97ab7e41939..f1259995b075f 100644
--- a/fs/sysfs/dir.c
+++ b/fs/sysfs/dir.c
@@ -11,6 +11,7 @@
 
 #define pr_fmt(fmt)	"sysfs: " fmt
 
+#include <linux/atomic.h>
 #include <linux/fs.h>
 #include <linux/kobject.h>
 #include <linux/slab.h>
@@ -18,10 +19,27 @@
 
 DEFINE_SPINLOCK(sysfs_symlink_target_lock);
 
+#if IS_ENABLED(CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST)
+/*
+ * Built-in KUnit observability for the device_sysfs_apply race tests.
+ * The lazy populate paths under lock are designed to be
+ * race-free; this counter lets the in-tree race tests
+ * (walk_populate_one_vs_all_race / walk_populate_vs_device_del_race
+ * in drivers/base/test/device_sysfs_apply_test.c) sample the
+ * sysfs_warn_dup() invocation count before and after the kthread
+ * storm and assert the delta is zero. Gated on the KUnit test
+ * config so production builds carry no overhead.
+ */
+atomic_t sysfs_warn_dup_kunit_count;
+#endif
+
 void sysfs_warn_dup(struct kernfs_node *parent, const char *name)
 {
 	char *buf;
 
+#if IS_ENABLED(CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST)
+	atomic_inc(&sysfs_warn_dup_kunit_count);
+#endif
 	buf = kzalloc(PATH_MAX, GFP_KERNEL);
 	if (buf)
 		kernfs_path(parent, buf, PATH_MAX);
-- 
2.47.3




Amazon Web Services Development Center Germany GmbH
Tamara-Danz-Str. 13
10243 Berlin
Geschaeftsfuehrung: Christof Hellmis, Andreas Stieger
Eingetragen am Amtsgericht Charlottenburg unter HRB 257764 B
Sitz: Berlin
Ust-ID: DE 365 538 597


  parent reply	other threads:[~2026-07-02 17:54 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-02 17:40 [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Pavol Sakac
2026-07-02 17:40 ` [RFC PATCH 01/14] kernfs: add populate callbacks and KERNFS_LAZY flag for lazy dir population Pavol Sakac
2026-07-02 17:40 ` [RFC PATCH 02/14] sysfs: add existence-check helpers for lazy populate races Pavol Sakac
2026-07-02 17:40 ` [RFC PATCH 03/14] sysfs: introduce sysfs_kf_syscall_ops dispatching to kobj_type Pavol Sakac
2026-07-02 17:40 ` [RFC PATCH 04/14] driver core: add struct sysfs_lazy_state and device_set_sysfs_lazy() Pavol Sakac
2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
2026-07-02 17:51   ` [RFC PATCH 06/14] driver core: wire device_ktype populate to walker Pavol Sakac
2026-07-02 17:51   ` [RFC PATCH 07/14] driver core: migrate device sysfs to device_sysfs_entry table Pavol Sakac
2026-07-02 17:51   ` [RFC PATCH 08/14] PCI/sysfs: migrate to device_sysfs_entry, defer physfn symlink Pavol Sakac
2026-07-02 17:51   ` [RFC PATCH 09/14] iommu: lazy-populate iommu_group reserved_regions/type attrs Pavol Sakac
2026-07-10 14:19     ` Greg Kroah-Hartman
2026-07-02 17:51   ` [RFC PATCH 10/14] PCI/IOV: opt SR-IOV VFs into sysfs_lazy Pavol Sakac
2026-07-02 17:51   ` [RFC PATCH 11/14] vfio: opt vfio-dev and VFIO group devices " Pavol Sakac
2026-07-02 17:51   ` Pavol Sakac [this message]
2026-07-02 17:51   ` [RFC PATCH 13/14] Documentation: add lazy sysfs initialisation and device_sysfs_entry docs Pavol Sakac
2026-07-02 17:51   ` [RFC PATCH 14/14] selftests: sysfs-lazy: add tests for lazy sysfs initialization Pavol Sakac
2026-07-10 14:16 ` [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Greg Kroah-Hartman

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=20260702175114.24659-8-sakacpav@amazon.de \
    --to=sakacpav@amazon.de \
    --cc=dakr@kernel.org \
    --cc=dmatlack@google.com \
    --cc=driver-core@lists.linux.dev \
    --cc=dwmw@amazon.co.uk \
    --cc=graf@amazon.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=kexec@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=nh-open-source@amazon.com \
    --cc=pasha.tatashin@soleen.com \
    --cc=pratyush@kernel.org \
    --cc=rafael@kernel.org \
    --cc=rppt@kernel.org \
    --cc=skhawaja@google.com \
    --cc=tj@kernel.org \
    /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