[RFC PATCH 12/14] driver core: test: add KUnit tests for device_sysfs_apply

Pavol Sakac <[email protected]>
Newsgroups org.infradead.lists.kexec,dev.linux.lists.driver-core,org.kernel.vger.linux-kernel,org.kvack.linux-mm
Message-ID <[email protected]>
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 <[email protected]>
Cc: Rafael J. Wysocki <[email protected]>
Cc: Brendan Higgins <[email protected]>
Cc: David Gow <[email protected]>
Cc: Danilo Krummrich <[email protected]>
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <[email protected]>
---
 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.