Linux-mm Archive on lore.kernel.org
 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 14/14] selftests: sysfs-lazy: add tests for lazy sysfs initialization
Date: Thu, 2 Jul 2026 19:51:14 +0200	[thread overview]
Message-ID: <20260702175114.24659-10-sakacpav@amazon.de> (raw)
In-Reply-To: <20260702175114.24659-1-sakacpav@amazon.de>

Add a kselftest suite exercising the lazy sysfs path end to end:

  - test_mod/sysfs_lazy_test_mod.c: a minimal platform driver that
    registers a device opted into sysfs_lazy (attributes, groups, a
    named symlink and a binary attribute).  An eager_twin module
    parameter registers an identical non-lazy device so tests can
    compare lazy against eager.

  - sysfs-lazy.c: userspace tests (kselftest_harness) covering
    partial realize then module unload, leak-free full populate and
    unload, driver unbind/rebind, transient populate-error
    propagation to open(2) with no negative dentry cached, lazy
    readdir(3) order matching the eager tree, and full lazy/eager
    tree equivalence.

  - iommu_groups.c: stat()s the lazily-populated iommu_group 'type'
    and 'reserved_regions' attributes; skips cleanly where IOMMU
    groups are absent.

  - pci_resource.c: concurrently drives populate-one and
    populate-all on a live PCI device and fails on any WARN/BUG/
    KASAN/Oops splat in the race window, asserting resource0 stays
    openable; the userspace companion to the in-kernel idempotency
    KUnit case.  Skips cleanly without a suitable device or
    privileges.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Shuah Khan <shuah@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: linux-kselftest@vger.kernel.org
Cc: linux-api@vger.kernel.org
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: driver-core@lists.linux.dev
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 tools/testing/selftests/Makefile              |   1 +
 tools/testing/selftests/sysfs-lazy/.gitignore |   3 +
 tools/testing/selftests/sysfs-lazy/Makefile   |  24 +
 tools/testing/selftests/sysfs-lazy/README.rst |  52 ++
 tools/testing/selftests/sysfs-lazy/config     |   6 +
 .../selftests/sysfs-lazy/iommu_groups.c       | 162 ++++
 .../selftests/sysfs-lazy/kmsg_cursor.h        | 167 ++++
 .../selftests/sysfs-lazy/pci_resource.c       | 284 +++++++
 tools/testing/selftests/sysfs-lazy/settings   |   1 +
 .../testing/selftests/sysfs-lazy/sysfs-lazy.c | 777 ++++++++++++++++++
 .../selftests/sysfs-lazy/test_mod/Makefile    |  28 +
 .../sysfs-lazy/test_mod/sysfs_lazy_test_mod.c | 319 +++++++
 12 files changed, 1824 insertions(+)
 create mode 100644 tools/testing/selftests/sysfs-lazy/.gitignore
 create mode 100644 tools/testing/selftests/sysfs-lazy/Makefile
 create mode 100644 tools/testing/selftests/sysfs-lazy/README.rst
 create mode 100644 tools/testing/selftests/sysfs-lazy/config
 create mode 100644 tools/testing/selftests/sysfs-lazy/iommu_groups.c
 create mode 100644 tools/testing/selftests/sysfs-lazy/kmsg_cursor.h
 create mode 100644 tools/testing/selftests/sysfs-lazy/pci_resource.c
 create mode 100644 tools/testing/selftests/sysfs-lazy/settings
 create mode 100644 tools/testing/selftests/sysfs-lazy/sysfs-lazy.c
 create mode 100644 tools/testing/selftests/sysfs-lazy/test_mod/Makefile
 create mode 100644 tools/testing/selftests/sysfs-lazy/test_mod/sysfs_lazy_test_mod.c

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 6e59b8f63e416..f2ab852b9c2cc 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -116,6 +116,7 @@ TARGETS += static_keys
 TARGETS += sync
 TARGETS += syscall_user_dispatch
 TARGETS += sysctl
+TARGETS += sysfs-lazy
 TARGETS += tc-testing
 TARGETS += tdx
 TARGETS += thermal/intel/power_floor
diff --git a/tools/testing/selftests/sysfs-lazy/.gitignore b/tools/testing/selftests/sysfs-lazy/.gitignore
new file mode 100644
index 0000000000000..b2af0d124131b
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/.gitignore
@@ -0,0 +1,3 @@
+sysfs-lazy
+iommu_groups
+pci_resource
diff --git a/tools/testing/selftests/sysfs-lazy/Makefile b/tools/testing/selftests/sysfs-lazy/Makefile
new file mode 100644
index 0000000000000..5e5f06cc3deb6
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/Makefile
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += -Wall $(KHDR_INCLUDES)
+LDLIBS += -lpthread
+
+TEST_GEN_PROGS := sysfs-lazy iommu_groups pci_resource
+TEST_FILES := settings
+
+KDIR ?= $(if $(O),$(O),$(realpath ../../../..))
+ifneq (,$(wildcard $(KDIR)/Module.symvers))
+TEST_GEN_MODS_DIR := test_mod
+else
+SYSFS_LAZY_MOD_WARNING = "missing Module.symvers, please have the kernel built first"
+endif
+
+include ../lib.mk
+
+ifneq ($(SYSFS_LAZY_MOD_WARNING),)
+all: warn_missing_test_mod
+
+warn_missing_test_mod:
+	@echo ; \
+	echo "Warning: $(SYSFS_LAZY_MOD_WARNING). sysfs_lazy_test_mod will not be built." ; \
+	echo
+endif
diff --git a/tools/testing/selftests/sysfs-lazy/README.rst b/tools/testing/selftests/sysfs-lazy/README.rst
new file mode 100644
index 0000000000000..63c5571428f48
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/README.rst
@@ -0,0 +1,52 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==========================
+sysfs-lazy kselftest suite
+==========================
+
+Integration tests for the per-file lazy sysfs initialization mechanism.
+Per-call populate behaviour (visibility, mode, groups,
+idempotency, atomicity) is validated in-kernel by the KUnit suite at
+``drivers/base/test/device_sysfs_apply_test.c``
+(``CONFIG_DEVICE_SYSFS_APPLY_KUNIT_TEST``); this suite covers only
+invariants that require userspace (module load/unload, driver-core
+rebind) or a live IOMMU topology.
+
+Prerequisites
+=============
+
+- Kernel built with ``CONFIG_SYSFS=y``, ``CONFIG_DEBUG_FS=y``,
+  ``CONFIG_MODULES=y``.
+- The ``sysfs_lazy_test_mod`` module built and available for
+  ``modprobe`` (for the ``sysfs-lazy`` binary).
+- An IOMMU-enabled kernel with at least one group under
+  ``/sys/kernel/iommu_groups/`` (for the ``iommu_groups`` binary;
+  test SKIPs gracefully otherwise).
+
+Running
+=======
+
+Build and run::
+
+    make -C tools/testing/selftests TARGETS=sysfs-lazy run_tests
+
+Test binaries
+=============
+
+``sysfs-lazy``
+  Module-backed integration tests.  Loads/unloads
+  ``sysfs_lazy_test_mod`` automatically.  Test cases:
+
+  1. **teardown_partial** - partial populate + module unload leaves no
+     leaks.
+  2. **no_leaks_on_unload** - kmemleak scan after full populate + unload.
+  3. **bind_unbind_cycle** - driver rebind after lazy populate preserves
+     materialized attrs.
+
+``iommu_groups``
+  Smoke test for ``iommu: lazy-populate iommu_group
+  reserved_regions/type attrs``.  Walks
+  ``/sys/kernel/iommu_groups/<N>/`` and stat()s the deferred ``type``
+  and ``reserved_regions`` attributes to verify lazy materialization.
+  Also exercises ``readdir`` (``ls``) to drive the all-path materializer.
+  SKIPs cleanly when IOMMU is disabled or no groups are present.
diff --git a/tools/testing/selftests/sysfs-lazy/config b/tools/testing/selftests/sysfs-lazy/config
new file mode 100644
index 0000000000000..2152f1bf2a903
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/config
@@ -0,0 +1,6 @@
+CONFIG_SYSFS=y
+CONFIG_DEBUG_FS=y
+CONFIG_MODULES=y
+CONFIG_PROVE_LOCKING=y
+CONFIG_KCSAN=y
+CONFIG_KASAN=y
diff --git a/tools/testing/selftests/sysfs-lazy/iommu_groups.c b/tools/testing/selftests/sysfs-lazy/iommu_groups.c
new file mode 100644
index 0000000000000..3b0be6beea484
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/iommu_groups.c
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * iommu_groups - lazy-populate smoke test for iommu_group sysfs attrs
+ *
+ * Complements the integration tests in sysfs-lazy.c and the in-kernel
+ * KUnit suite drivers/base/test/device_sysfs_apply_test.c by exercising
+ * the lazy-populate opt-in added for the kernel commit
+ *   iommu: lazy-populate iommu_group reserved_regions/type attrs
+ *
+ * Walks /sys/kernel/iommu_groups/<N>/ and verifies that the two
+ * always-present attributes (reserved_regions, type) are accessible
+ * via both per-file stat() and group-level readdir().  On a system
+ * running the lazy-populate kernel, these attributes are materialized
+ * on first access; on a non-lazy kernel they already exist eagerly.
+ * Either way, this test passes when the kernel exposes a
+ * syntactically-complete iommu_group hierarchy.
+ *
+ * Uses kselftest harness.  Skips gracefully if:
+ *   - /sys/kernel/iommu_groups/ does not exist (IOMMU disabled); or
+ *   - no groups are present on the host.
+ *
+ * Does not require root.
+ */
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "../kselftest_harness.h"
+
+#define IOMMU_GROUPS_ROOT "/sys/kernel/iommu_groups"
+
+/*
+ * find_first_group: return heap-allocated path to the first
+ * numerically-named entry under IOMMU_GROUPS_ROOT, or NULL if none.
+ * Caller frees.
+ */
+static char *find_first_group(void)
+{
+	DIR *d = opendir(IOMMU_GROUPS_ROOT);
+	struct dirent *de;
+	char *path = NULL;
+
+	if (!d)
+		return NULL;
+
+	while ((de = readdir(d))) {
+		char *endp;
+		unsigned long id;
+
+		if (de->d_name[0] == '.')
+			continue;
+
+		id = strtoul(de->d_name, &endp, 10);
+		if (*endp != '\0')
+			continue;
+		(void)id;
+
+		path = malloc(PATH_MAX);
+		if (!path)
+			break;
+		snprintf(path, PATH_MAX, "%s/%s", IOMMU_GROUPS_ROOT,
+			 de->d_name);
+		break;
+	}
+
+	closedir(d);
+	return path;
+}
+
+TEST(iommu_groups_root_exists_or_skip)
+{
+	struct stat st;
+
+	if (stat(IOMMU_GROUPS_ROOT, &st) < 0) {
+		SKIP(return,
+		     "/sys/kernel/iommu_groups/ absent (IOMMU not enabled)");
+	}
+	ASSERT_TRUE(S_ISDIR(st.st_mode));
+}
+
+TEST(first_group_reserved_regions_and_type)
+{
+	char path[PATH_MAX];
+	char *group;
+	struct stat st;
+	int fd;
+	char buf[64];
+	ssize_t n;
+
+	group = find_first_group();
+	if (!group)
+		SKIP(return, "no iommu_groups present on this host");
+
+	/*
+	 * Step 1: stat() on the group's "type" attribute must succeed.
+	 * On a lazy-populate kernel, this is the first access and
+	 * triggers populate_one(name="type").  On a non-lazy kernel,
+	 * the attribute was created eagerly at group allocation time.
+	 */
+	snprintf(path, sizeof(path), "%s/type", group);
+	ASSERT_EQ(stat(path, &st), 0) {
+		TH_LOG("stat(%s) failed: %s", path, strerror(errno));
+	}
+	ASSERT_TRUE(S_ISREG(st.st_mode));
+
+	/* Step 2: read the "type" attribute.  Expect a non-empty short
+	 * string (e.g. "DMA", "identity", ...).
+	 */
+	fd = open(path, O_RDONLY);
+	ASSERT_GE(fd, 0);
+	n = read(fd, buf, sizeof(buf) - 1);
+	close(fd);
+	ASSERT_GT(n, 0);
+	buf[n] = '\0';
+	TH_LOG("iommu_group type: %s", buf);
+
+	/*
+	 * Step 3: stat() on "reserved_regions" must also succeed.  On
+	 * a lazy-populate kernel, this is potentially a second
+	 * populate_one() call.  The file may be empty (many groups
+	 * have no reserved regions) but must exist.
+	 */
+	snprintf(path, sizeof(path), "%s/reserved_regions", group);
+	ASSERT_EQ(stat(path, &st), 0) {
+		TH_LOG("stat(%s) failed: %s", path, strerror(errno));
+	}
+	ASSERT_TRUE(S_ISREG(st.st_mode));
+
+	/*
+	 * Step 4: readdir() the group directory.  On a lazy-populate
+	 * kernel this triggers populate_all() which must materialize
+	 * both attributes (if they were not materialized by step 1/3
+	 * already).  Verify both names appear.
+	 */
+	DIR *d;
+	struct dirent *de;
+	int saw_type = 0, saw_rr = 0;
+
+	d = opendir(group);
+	ASSERT_NE(d, NULL);
+	while ((de = readdir(d))) {
+		if (!strcmp(de->d_name, "type"))
+			saw_type = 1;
+		else if (!strcmp(de->d_name, "reserved_regions"))
+			saw_rr = 1;
+	}
+	closedir(d);
+	ASSERT_TRUE(saw_type);
+	ASSERT_TRUE(saw_rr);
+
+	free(group);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/sysfs-lazy/kmsg_cursor.h b/tools/testing/selftests/sysfs-lazy/kmsg_cursor.h
new file mode 100644
index 0000000000000..d85c1315cbf3d
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/kmsg_cursor.h
@@ -0,0 +1,167 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * kmsg_cursor.h - capture WARN/BUG kernel-log lines emitted within a
+ * bounded test window.
+ *
+ * Selftest builds catch userspace-visible regressions but miss the
+ * silent kernel-side ones - a WARN_ON splat, a UAF KASAN report, a
+ * RCU stall - that only show up in dmesg. The cursor provides a
+ * minimal wrapper around /dev/kmsg with two operations a TEST_F
+ * needs: snapshot at test start, extract WARN/BUG lines emitted
+ * since the snapshot.
+ *
+ * /dev/kmsg semantics (fs/printk/printk.c::devkmsg_*):
+ *   - open(/dev/kmsg, O_RDONLY) initially positions at the first
+ *     record retained in the printk buffer;
+ *   - lseek(fd, 0, SEEK_END) advances past the current end so
+ *     subsequent read()s see only newly-appended records;
+ *   - O_NONBLOCK makes read() return -EAGAIN at EOF rather than
+ *     blocking for the next record.
+ *
+ * Each record is one read() call; the textual line begins after the
+ * first ',' delimiters in the metadata prefix
+ * "<prio>,<seq>,<ts>,<flag>;<text>\n".
+ *
+ * Requires CAP_SYS_ADMIN (or CAP_SYSLOG with kernel.dmesg_restrict
+ * relaxed). Selftests run as root in the kselftest harness so this
+ * is satisfied; kmsg_cursor_snapshot() returns -EPERM if not, and
+ * callers may SKIP the WARN/BUG assertion in that case.
+ */
+#ifndef SYSFS_LAZY_KMSG_CURSOR_H
+#define SYSFS_LAZY_KMSG_CURSOR_H
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define KMSG_CURSOR_BUF_SZ	8192	/* one /dev/kmsg record max */
+
+struct kmsg_cursor {
+	int fd;
+};
+
+/*
+ * kmsg_cursor_snapshot - open /dev/kmsg and seek past the current
+ * end so subsequent extract() returns only records appended after
+ * this call. Returns 0 on success, -errno on failure (e.g. -EPERM
+ * if the caller lacks CAP_SYS_ADMIN/CAP_SYSLOG, -ENOENT if /dev/kmsg
+ * is absent in this filesystem image).
+ */
+static inline int kmsg_cursor_snapshot(struct kmsg_cursor *c)
+{
+	c->fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+	if (c->fd < 0)
+		return -errno;
+	if (lseek(c->fd, 0, SEEK_END) == (off_t)-1) {
+		int err = -errno;
+
+		close(c->fd);
+		c->fd = -1;
+		return err;
+	}
+	return 0;
+}
+
+/*
+ * kmsg_cursor_extract_warnings - drain records appended since
+ * kmsg_cursor_snapshot() and return a heap-allocated newline-joined
+ * string of those whose textual body contains "WARNING:", "BUG:",
+ * "KASAN:", "UBSAN:", or "Oops:" (case-sensitive - these are the
+ * established splat prefixes from kernel/panic.c::__warn() and
+ * lib/ubsan.c). Returns NULL when no matches were found OR when
+ * extraction was not possible (no kmsg fd). Callers must free()
+ * the returned string.
+ *
+ * Reads are non-blocking: on -EAGAIN the loop terminates. -EPIPE
+ * (records overrun before we drained them) is treated as "no
+ * matches"; the harness will not synthesise a false-positive WARN
+ * on its own log saturation.
+ */
+static inline char *kmsg_cursor_extract_warnings(struct kmsg_cursor *c)
+{
+	char rec[KMSG_CURSOR_BUF_SZ];
+	char *out = NULL;
+	size_t out_len = 0, out_cap = 0;
+	ssize_t n;
+
+	if (!c || c->fd < 0)
+		return NULL;
+
+	for (;;) {
+		n = read(c->fd, rec, sizeof(rec) - 1);
+		if (n < 0) {
+			if (errno == EPIPE) {
+				/* Overrun - skip and keep draining. */
+				continue;
+			}
+			if (errno == EAGAIN || errno == EWOULDBLOCK)
+				break;
+			/* Other errors: stop, return what we have. */
+			break;
+		}
+		if (n == 0)
+			break;
+		rec[n] = '\0';
+
+		/* Split the metadata prefix off at the first ';'. */
+		char *body = strchr(rec, ';');
+
+		if (!body)
+			continue;
+		body++;
+
+		/* Trim a trailing newline so concatenation stays tidy. */
+		size_t blen = strlen(body);
+
+		while (blen && (body[blen - 1] == '\n' ||
+				body[blen - 1] == '\r'))
+			body[--blen] = '\0';
+
+		if (!strstr(body, "WARNING:") &&
+		    !strstr(body, "BUG:") &&
+		    !strstr(body, "KASAN:") &&
+		    !strstr(body, "UBSAN:") &&
+		    !strstr(body, "Oops:"))
+			continue;
+
+		/* Append "<body>\n" to the output buffer. */
+		size_t need = out_len + blen + 2; /* '\n' + NUL */
+
+		if (need > out_cap) {
+			size_t new_cap = out_cap ? out_cap * 2 : 1024;
+			char *p;
+
+			while (new_cap < need)
+				new_cap *= 2;
+			p = realloc(out, new_cap);
+			if (!p)
+				break;
+			out = p;
+			out_cap = new_cap;
+		}
+		memcpy(out + out_len, body, blen);
+		out_len += blen;
+		out[out_len++] = '\n';
+		out[out_len] = '\0';
+	}
+
+	return out;
+}
+
+static inline void kmsg_cursor_close(struct kmsg_cursor *c)
+{
+	if (c && c->fd >= 0) {
+		close(c->fd);
+		c->fd = -1;
+	}
+}
+
+#endif /* SYSFS_LAZY_KMSG_CURSOR_H */
diff --git a/tools/testing/selftests/sysfs-lazy/pci_resource.c b/tools/testing/selftests/sysfs-lazy/pci_resource.c
new file mode 100644
index 0000000000000..b54bcd40c7949
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/pci_resource.c
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pci_resource - live-race idempotency stress test for lazy PCI
+ * resource sysfs files.
+ *
+ * Exercises the populate_one() vs populate_all() concurrency contract
+ * for pci_create_resource_files() after migration to the
+ * device_sysfs_entry walker.  Two threads attack the same PCI device
+ * directory for a bounded wall-clock window:
+ *
+ *   A: open(<dev>/resource0) + close, looped - drives
+ *      populate_one("resource0") through kernfs_iop_lookup() and the
+ *      device's ->populate hook.
+ *   B: opendir(<dev>) + readdir + closedir, looped - drives
+ *      populate_all() through the kernfs ->iterate dispatcher and the
+ *      device's ->populate_all hook.
+ *
+ * The fixed pre-walker pci_create_resource_files() created a single
+ * kernfs entry per BAR and assumed the post-create state was its own;
+ * called twice (e.g. once for resource0 by populate_one, once for the
+ * full set by populate_all racing immediately after), the second call
+ * would attempt to re-create the already-created entry and trip
+ * sysfs_warn_dup() / kernfs_link_sibling()'s "cannot create duplicate
+ * filename" WARN.
+ *
+ * The walker post-fix shape has the wildcard PCI resource row absorb
+ * the populate-already-done case (idempotent) under the per-device
+ * lock + populated-latch protocol added with the lazy
+ * series.  This selftest is the userspace boundary observable: race
+ * the two paths, then drain /dev/kmsg via kmsg_cursor and assert no
+ * WARN/BUG splat fired in the window.  The companion KUnit test
+ * walk_wildcard_row_idempotent() covers the in-kernel single-thread
+ * sequencing of the same contract; this one covers the multi-thread
+ * race that the production bug actually expressed under.
+ *
+ * Skips when:
+ *   - /sys/bus/pci/devices/ is empty (e.g. virtio-only builds);
+ *   - no PCI device has a resource0 attribute (BAR0 absent);
+ *   - /dev/kmsg is unreadable (lack of CAP_SYS_ADMIN/CAP_SYSLOG -
+ *     selftests usually run as root, but harness execution outside
+ *     a kselftest runner may not).
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "../kselftest_harness.h"
+#include "kmsg_cursor.h"
+
+#define PCI_DEVICES_ROOT	"/sys/bus/pci/devices"
+#define RACE_DURATION_MS	100
+
+/*
+ * find_pci_dev_with_resource0 - return heap-allocated absolute path
+ * to the first PCI device under PCI_DEVICES_ROOT whose "resource0"
+ * attribute is openable for read by the calling process, or NULL if
+ * none.  Caller frees.
+ *
+ * On a lazy-populate kernel, the open(2) probe below performs a
+ * populate_one("resource0") on the candidate device - which is the
+ * very contract this test races concurrently elsewhere.  This single
+ * up-front lookup is benign: there is no second writer at this point
+ * in the program, and the created attr stays present for the race
+ * window that follows.
+ *
+ * The probe uses open() rather than access(F_OK) so that a process
+ * without read permission on PCI resource files (selftests usually
+ * run as root, but ad-hoc invocations may not) skips gracefully via
+ * the caller's NULL-path SKIP rather than failing the post-race
+ * readability check.
+ */
+static char *find_pci_dev_with_resource0(void)
+{
+	DIR *d = opendir(PCI_DEVICES_ROOT);
+	struct dirent *de;
+	char *path = NULL;
+
+	if (!d)
+		return NULL;
+
+	while ((de = readdir(d))) {
+		char probe[PATH_MAX];
+		int fd;
+
+		if (de->d_name[0] == '.')
+			continue;
+
+		snprintf(probe, sizeof(probe), "%s/%s/resource0",
+			 PCI_DEVICES_ROOT, de->d_name);
+		fd = open(probe, O_RDONLY);
+		if (fd < 0)
+			continue;
+		close(fd);
+
+		path = malloc(PATH_MAX);
+		if (!path)
+			break;
+		snprintf(path, PATH_MAX, "%s/%s",
+			 PCI_DEVICES_ROOT, de->d_name);
+		break;
+	}
+
+	closedir(d);
+	return path;
+}
+
+struct race_ctx {
+	char			dev_dir[PATH_MAX];
+	char			res0_path[PATH_MAX];
+	atomic_int		stop;
+	atomic_long		iters_open;
+	atomic_long		iters_readdir;
+};
+
+static void *thread_open_close(void *arg)
+{
+	struct race_ctx *ctx = arg;
+
+	while (!atomic_load_explicit(&ctx->stop, memory_order_acquire)) {
+		int fd = open(ctx->res0_path, O_RDONLY);
+
+		if (fd >= 0)
+			close(fd);
+		atomic_fetch_add(&ctx->iters_open, 1);
+	}
+	return NULL;
+}
+
+static void *thread_readdir(void *arg)
+{
+	struct race_ctx *ctx = arg;
+
+	while (!atomic_load_explicit(&ctx->stop, memory_order_acquire)) {
+		DIR *d = opendir(ctx->dev_dir);
+
+		if (d) {
+			struct dirent *de;
+
+			while ((de = readdir(d)))
+				;
+			closedir(d);
+		}
+		atomic_fetch_add(&ctx->iters_readdir, 1);
+	}
+	return NULL;
+}
+
+static long elapsed_ms(const struct timespec *start)
+{
+	struct timespec now;
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	return (now.tv_sec - start->tv_sec) * 1000 +
+	       (now.tv_nsec - start->tv_nsec) / 1000000;
+}
+
+/*
+ * pci_resource_concurrent_open_readdir: stress the populate_one vs
+ * populate_all live-race for ~RACE_DURATION_MS milliseconds on the
+ * first PCI device that exposes resource0.  Capture /dev/kmsg
+ * WARN/BUG splats with kmsg_cursor; assert none fired during the
+ * race window.  resource0 must remain openable post-race.
+ */
+TEST(pci_resource_concurrent_open_readdir)
+{
+	struct race_ctx ctx = { 0 };
+	char *dev_path;
+	pthread_t t_open, t_dir;
+	bool t_open_started = false, t_dir_started = false;
+	struct kmsg_cursor kc;
+	char *warns;
+	int kc_ret, ret, fd;
+	struct timespec start;
+	long ms;
+
+	dev_path = find_pci_dev_with_resource0();
+	if (!dev_path)
+		SKIP(return,
+		     "no PCI device with readable resource0 found under "
+		     PCI_DEVICES_ROOT
+		     " (no PCI bus, no BAR0, or insufficient privileges)");
+
+	strncpy(ctx.dev_dir, dev_path, sizeof(ctx.dev_dir) - 1);
+	snprintf(ctx.res0_path, sizeof(ctx.res0_path), "%s/resource0",
+		 dev_path);
+	atomic_init(&ctx.stop, 0);
+	atomic_init(&ctx.iters_open, 0);
+	atomic_init(&ctx.iters_readdir, 0);
+
+	kc_ret = kmsg_cursor_snapshot(&kc);
+	if (kc_ret) {
+		free(dev_path);
+		SKIP(return,
+		     "kmsg cursor unavailable (%s); cannot detect WARN/dup splat",
+		     strerror(-kc_ret));
+	}
+
+	ret = pthread_create(&t_open, NULL, thread_open_close, &ctx);
+	ASSERT_EQ(0, ret) {
+		kmsg_cursor_close(&kc);
+		free(dev_path);
+		TH_LOG("pthread_create(open_close) failed: %s",
+		       strerror(ret));
+	}
+	t_open_started = true;
+
+	ret = pthread_create(&t_dir, NULL, thread_readdir, &ctx);
+	if (ret) {
+		atomic_store(&ctx.stop, 1);
+		pthread_join(t_open, NULL);
+		kmsg_cursor_close(&kc);
+		free(dev_path);
+		ASSERT_EQ(0, ret) {
+			TH_LOG("pthread_create(readdir) failed: %s",
+			       strerror(ret));
+		}
+	}
+	t_dir_started = true;
+
+	clock_gettime(CLOCK_MONOTONIC, &start);
+	do {
+		struct timespec ts = { 0, 5 * 1000 * 1000 }; /* 5 ms */
+
+		nanosleep(&ts, NULL);
+		ms = elapsed_ms(&start);
+	} while (ms < RACE_DURATION_MS);
+
+	atomic_store(&ctx.stop, 1);
+	if (t_open_started)
+		pthread_join(t_open, NULL);
+	if (t_dir_started)
+		pthread_join(t_dir, NULL);
+
+	/*
+	 * Drain WARN/BUG splats accumulated during the race window.
+	 * The signature of the bug under test is:
+	 *   "sysfs: cannot create duplicate filename '/devices/.../resource0'"
+	 * inside a "WARNING:" splat, but any WARN/BUG/KASAN/UBSAN/Oops
+	 * fault during the window is also a regression of the
+	 * lock + populated-latch protocol or its callees and
+	 * fails the test.
+	 */
+	warns = kmsg_cursor_extract_warnings(&kc);
+	EXPECT_EQ(NULL, warns) {
+		TH_LOG("kernel WARN/BUG during pci_resource race on %s:\n%s",
+		       dev_path, warns);
+	}
+	free(warns);
+	kmsg_cursor_close(&kc);
+
+	/* resource0 must still be openable post-race. */
+	fd = open(ctx.res0_path, O_RDONLY);
+	EXPECT_GE(fd, 0) {
+		TH_LOG("open(%s) failed post-race: %s",
+		       ctx.res0_path, strerror(errno));
+	}
+	if (fd >= 0)
+		close(fd);
+
+	/* Sanity: both threads got CPU time. */
+	EXPECT_GT(atomic_load(&ctx.iters_open), 0);
+	EXPECT_GT(atomic_load(&ctx.iters_readdir), 0);
+	TH_LOG("race ran %ldms: open_close=%ld readdir=%ld on %s",
+	       ms,
+	       atomic_load(&ctx.iters_open),
+	       atomic_load(&ctx.iters_readdir),
+	       dev_path);
+
+	free(dev_path);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/sysfs-lazy/settings b/tools/testing/selftests/sysfs-lazy/settings
new file mode 100644
index 0000000000000..6091b45d226ba
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/settings
@@ -0,0 +1 @@
+timeout=120
diff --git a/tools/testing/selftests/sysfs-lazy/sysfs-lazy.c b/tools/testing/selftests/sysfs-lazy/sysfs-lazy.c
new file mode 100644
index 0000000000000..6599a589366b3
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/sysfs-lazy.c
@@ -0,0 +1,777 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * sysfs-lazy.c - kselftest for sysfs lazy initialization
+ *
+ * Integration-level coverage of the per-file lazy sysfs populate
+ * mechanism by loading the sysfs_lazy_test_mod module and exercising
+ * teardown, leak, and bind/unbind invariants that can only be
+ * observed from userspace (module load/unload + driver-core rebind).
+ * The per-call populate behaviour (visibility, mode, groups,
+ * idempotency, atomicity) is validated in-kernel by the
+ * KUnit suite drivers/base/test/device_sysfs_apply_test.c; duplicating
+ * those invariants here would just add maintenance overhead.
+ *
+ * Requires: sysfs_lazy_test_mod.ko loaded (or loadable).
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "../kselftest_harness.h"
+#include "kmsg_cursor.h"
+
+/*
+ * getdents64(2) is exposed by glibc on most distros but not all
+ * (notably some musl builds and older glibc). Selftests build against
+ * KHDR_INCLUDES which gives us the syscall number; wrap it directly to
+ * avoid a libc-version dependency. The struct definition matches
+ * include/uapi/linux/dirent.h::linux_dirent64.
+ */
+struct sysfs_lazy_linux_dirent64 {
+	uint64_t	d_ino;
+	int64_t		d_off;
+	unsigned short	d_reclen;
+	unsigned char	d_type;
+	char		d_name[];
+};
+
+static inline ssize_t sysfs_lazy_getdents64(int fd, void *buf, size_t len)
+{
+	return syscall(SYS_getdents64, fd, buf, len);
+}
+
+#define MOD_NAME		"sysfs_lazy_test_mod"
+#define DEV_NAME		"sysfs_lazy_test"
+#define EAGER_NAME		"sysfs_lazy_test_eager"
+#define DEV_DIR			"/sys/devices/platform/" DEV_NAME
+#define EAGER_DIR		"/sys/devices/platform/" EAGER_NAME
+#define DRV_DIR			"/sys/bus/platform/drivers/" DEV_NAME
+
+/* ---- helpers ----------------------------------------------------------- */
+
+static bool file_exists(const char *dir, const char *name)
+{
+	char path[PATH_MAX];
+	struct stat st;
+
+	snprintf(path, sizeof(path), "%s/%s", dir, name);
+	return stat(path, &st) == 0;
+}
+
+static int read_file_str(const char *dir, const char *name,
+			 char *buf, size_t len)
+{
+	char path[PATH_MAX];
+	int fd, n;
+
+	snprintf(path, sizeof(path), "%s/%s", dir, name);
+	fd = open(path, O_RDONLY);
+	if (fd < 0)
+		return -errno;
+	n = read(fd, buf, len - 1);
+	close(fd);
+	if (n < 0)
+		return -errno;
+	buf[n] = '\0';
+	return 0;
+}
+
+static int write_file_str(const char *path, const char *val)
+{
+	int fd, ret;
+
+	fd = open(path, O_WRONLY);
+	if (fd < 0)
+		return -errno;
+	ret = write(fd, val, strlen(val));
+	close(fd);
+	return ret < 0 ? -errno : 0;
+}
+
+static int count_dir_entries(const char *path)
+{
+	struct dirent *de;
+	DIR *d;
+	int n = 0;
+
+	d = opendir(path);
+	if (!d)
+		return -errno;
+	while ((de = readdir(d)) != NULL) {
+		if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
+			continue;
+		n++;
+	}
+	closedir(d);
+	return n;
+}
+
+static int load_module(void)
+{
+	return system("modprobe " MOD_NAME);
+}
+
+static int load_module_with_twin(void)
+{
+	return system("modprobe " MOD_NAME " eager_twin=1");
+}
+
+/*
+ * load_module_with_inject - load test_mod with inject_errno=@val so
+ * dev->type->populate(dev, "inject_target") returns @val unchanged.
+ * The lookup path then forwards that errno to userspace open(2)
+ * without caching a negative dentry.
+ */
+static int load_module_with_inject(int val)
+{
+	char cmd[128];
+
+	snprintf(cmd, sizeof(cmd),
+		 "modprobe " MOD_NAME " inject_errno=%d", val);
+	return system(cmd);
+}
+
+static int unload_module(void)
+{
+	return system("rmmod " MOD_NAME " 2>/dev/null");
+}
+
+/* ---- fixture ----------------------------------------------------------- */
+
+FIXTURE(sysfs_lazy) {
+	int dummy;
+};
+
+FIXTURE_SETUP(sysfs_lazy)
+{
+	unload_module();
+	ASSERT_EQ(0, load_module());
+	/* Give udev a moment */
+	usleep(100000);
+}
+
+FIXTURE_TEARDOWN(sysfs_lazy)
+{
+	unload_module();
+}
+
+/* ---- test 1: teardown_partial ------------------------------------------ */
+
+/*
+ * Realize a subset of attrs, then unload the module and confirm the
+ * partial state tears down cleanly (no leaks, no crashes, device dir
+ * gone).  Integration-only: requires module unload.
+ *
+ * kmsg cursor: snapshot at start, drain WARN/BUG splats at end.
+ * Test fails if any kernel-side warning fires during the window.
+ */
+TEST_F(sysfs_lazy, teardown_partial)
+{
+	struct kmsg_cursor kc;
+	char *warns;
+	int kc_ret;
+	char buf[64];
+
+	kc_ret = kmsg_cursor_snapshot(&kc);
+
+	/* Realize only a few attrs */
+	ASSERT_EQ(0, read_file_str(DEV_DIR, "basic0", buf, sizeof(buf)));
+	ASSERT_EQ(0, read_file_str(DEV_DIR, "extra0", buf, sizeof(buf)));
+
+	/* Unload module - should not leak or crash */
+	ASSERT_EQ(0, unload_module());
+
+	/* Device dir should be gone */
+	EXPECT_FALSE(file_exists("/sys/devices/platform", DEV_NAME));
+
+	/* No WARN/BUG splats in the captured kmsg window. */
+	if (kc_ret == 0) {
+		warns = kmsg_cursor_extract_warnings(&kc);
+		EXPECT_EQ(NULL, warns) {
+			TH_LOG("kernel WARN/BUG during teardown_partial:\n%s",
+			       warns);
+		}
+		free(warns);
+		kmsg_cursor_close(&kc);
+	} else {
+		TH_LOG("kmsg cursor unavailable (%s); skipping splat check",
+		       strerror(-kc_ret));
+	}
+
+	/* Reload for teardown fixture */
+	ASSERT_EQ(0, load_module());
+	usleep(100000);
+}
+
+/* ---- test 2: no_leaks_on_unload ---------------------------------------- */
+
+/*
+ * Realize everything, then unload with kmemleak scanning to catch any
+ * lazy-populate allocation that escapes teardown.  Integration-only:
+ * requires kmemleak and module unload.
+ *
+ * kmsg cursor: snapshot at start, drain WARN/BUG splats at end. The
+ * kmemleak read below is best-effort; a kmemleak hit is reported as
+ * the buffer mismatch, while any other WARN/BUG (e.g. lockdep, KASAN
+ * use-after-free in the unload path) surfaces via the cursor.
+ */
+TEST_F(sysfs_lazy, no_leaks_on_unload)
+{
+	struct kmsg_cursor kc;
+	char *warns;
+	int kc_ret;
+
+	kc_ret = kmsg_cursor_snapshot(&kc);
+
+	/* Realize all attrs */
+	count_dir_entries(DEV_DIR);
+
+	/* Unload */
+	ASSERT_EQ(0, unload_module());
+
+	/* Trigger kmemleak scan if available */
+	write_file_str("/sys/kernel/debug/kmemleak", "scan");
+
+	/* Check kmemleak (best-effort - may not be enabled) */
+	char buf[256];
+	int ret;
+
+	ret = read_file_str("/sys/kernel/debug", "kmemleak",
+			    buf, sizeof(buf));
+	if (ret == 0)
+		EXPECT_STREQ("", buf);
+
+	/* Device dir must be gone */
+	EXPECT_FALSE(file_exists("/sys/devices/platform", DEV_NAME));
+
+	/* No WARN/BUG splats in the captured kmsg window. */
+	if (kc_ret == 0) {
+		warns = kmsg_cursor_extract_warnings(&kc);
+		EXPECT_EQ(NULL, warns) {
+			TH_LOG("kernel WARN/BUG during no_leaks_on_unload:\n%s",
+			       warns);
+		}
+		free(warns);
+		kmsg_cursor_close(&kc);
+	} else {
+		TH_LOG("kmsg cursor unavailable (%s); skipping splat check",
+		       strerror(-kc_ret));
+	}
+
+	/* Reload for teardown fixture */
+	ASSERT_EQ(0, load_module());
+	usleep(100000);
+}
+
+/* ---- test 3: bind_unbind_cycle ----------------------------------------- */
+
+/*
+ * Realize one attr, unbind the driver, rebind, and confirm the
+ * created attr is still reachable.  Integration-only: requires
+ * driver-core bind/unbind plumbing.
+ *
+ * kmsg cursor: snapshot at start, drain WARN/BUG splats at end. The
+ * unbind/rebind path is the most likely site for a lockdep splat or
+ * a refcount imbalance, so the splat check is high-value here.
+ */
+TEST_F(sysfs_lazy, bind_unbind_cycle)
+{
+	struct kmsg_cursor kc;
+	char *warns;
+	int kc_ret;
+	char buf[64];
+
+	kc_ret = kmsg_cursor_snapshot(&kc);
+
+	/* Realize some attrs */
+	ASSERT_EQ(0, read_file_str(DEV_DIR, "basic0", buf, sizeof(buf)));
+	EXPECT_STREQ("42\n", buf);
+
+	/* Unbind driver */
+	ASSERT_EQ(0, write_file_str(DRV_DIR "/unbind", DEV_NAME));
+	usleep(50000);
+
+	/* Verify driver symlink gone */
+	EXPECT_FALSE(file_exists(DEV_DIR, "driver"));
+
+	/* Rebind */
+	ASSERT_EQ(0, write_file_str(DRV_DIR "/bind", DEV_NAME));
+	usleep(50000);
+
+	/* Verify driver symlink restored */
+	EXPECT_TRUE(file_exists(DEV_DIR, "driver"));
+
+	/* Previously-created attrs still accessible */
+	ASSERT_EQ(0, read_file_str(DEV_DIR, "basic0", buf, sizeof(buf)));
+	EXPECT_STREQ("42\n", buf);
+
+	/* No WARN/BUG splats in the captured kmsg window. */
+	if (kc_ret == 0) {
+		warns = kmsg_cursor_extract_warnings(&kc);
+		EXPECT_EQ(NULL, warns) {
+			TH_LOG("kernel WARN/BUG during bind_unbind_cycle:\n%s",
+			       warns);
+		}
+		free(warns);
+		kmsg_cursor_close(&kc);
+	} else {
+		TH_LOG("kmsg cursor unavailable (%s); skipping splat check",
+		       strerror(-kc_ret));
+	}
+}
+
+/* ---- test 4: transient_populate_error_propagates ----------------------- */
+
+/*
+ * The transient-populate-error contract documented in
+ * Documentation/driver-api/sysfs-lazy.rst guarantees that an errno
+ * returned by a driver's populate hook propagates verbatim through
+ * the kernfs lookup path to userspace open(2), and that the lookup
+ * does NOT cache a negative dentry (so a subsequent attempt - once
+ * the underlying condition has cleared - succeeds). The companion
+ * KUnit case walk_enomem_propagates() covers the walker boundary;
+ * this case extends coverage to the userspace open(2) boundary.
+ *
+ * Mechanism: the test_mod's dev->type->populate(dev, "inject_target")
+ * returns inject_errno when negative. inject_target is intentionally
+ * absent from dev->groups, so the standard create_dev_own_groups()
+ * row of driver_core_sysfs_entries[] returns -ENOENT and the
+ * dispatch falls through to the type hook.
+ *
+ * Step 1: load with inject_errno=-ENOMEM, open inject_target, expect
+ *         open(2) == -1 with errno == ENOMEM.
+ * Step 2: reload without injection, open inject_target, expect
+ *         open(2) >= 0 (success).
+ *
+ * Cannot share the FIXTURE_SETUP load because that call uses the
+ * default modprobe arguments. Reload explicitly.
+ */
+TEST_F(sysfs_lazy, transient_populate_error_propagates)
+{
+	int fd;
+
+	/* Step 1: reload with -ENOMEM injection. */
+	ASSERT_EQ(0, unload_module());
+	ASSERT_EQ(0, load_module_with_inject(-ENOMEM));
+	usleep(100000);
+
+	errno = 0;
+	fd = open(DEV_DIR "/inject_target", O_RDONLY);
+	EXPECT_EQ(-1, fd);
+	EXPECT_EQ(ENOMEM, errno);
+	if (fd >= 0)
+		close(fd);
+
+	/* Step 2: reload without injection; the same open must succeed. */
+	ASSERT_EQ(0, unload_module());
+	ASSERT_EQ(0, load_module());
+	usleep(100000);
+
+	errno = 0;
+	fd = open(DEV_DIR "/inject_target", O_RDONLY);
+	EXPECT_GE(fd, 0) {
+		TH_LOG("open(inject_target) failed after clearing injection: %s",
+		       strerror(errno));
+	}
+	if (fd >= 0)
+		close(fd);
+}
+
+/* ---- test 5: readdir_order_abi_guard ----------------------------------- */
+
+/*
+ * Readdir-time ABI guard. Confirms that the lazy populate_all path
+ * surfaces the same set of top-level entries as the eager device_add
+ * path would, by reading the device directory via getdents64(2)
+ * directly (no opendir(3)/readdir(3) layer that some libcs sort
+ * post-hoc, no qsort).
+ *
+ * Order pinning note: kernfs orders sibling entries by name hash in
+ * an rb-tree (fs/kernfs/dir.c::kernfs_link_sibling /
+ * kernfs_dir_pos), NOT by insertion order. The on-disk readdir order
+ * is therefore deterministic but a function of the entry names'
+ * hashes, not of driver_core_sysfs_entries[] table order. Pinning a
+ * literal hash-ordered list would brittle-couple the test to the
+ * kernfs hash function. Instead this guard pins the SET of expected
+ * names: every name produced by the test_mod's groups plus the
+ * stable driver-core / platform-bus contributions must appear, and
+ * the diagnostic logs the actual on-disk order so a regression that
+ * reorders entries (e.g. a kernfs hash change) shows up in the
+ * artifact.
+ *
+ * Skipped names (noise out of scope for this guard):
+ *   - ".", ".." - directory pseudo-entries
+ *   - "driver" - symlink whose presence depends on bind state
+ *   - "subsystem" - bus symlink, presence depends on bus state
+ *   - "numa_node" - platform_bus_type dev_groups attr hidden when
+ *     dev_to_node() == NUMA_NO_NODE (host-config dependent)
+ *   - "uevent" - present universally; not asserted here because
+ *     it's covered by the eager_lazy_tree_match invariant test
+ */
+TEST_F(sysfs_lazy, readdir_order_abi_guard)
+{
+	/*
+	 * Required names: every entry the test_mod's group tables MUST
+	 * produce. These are an upstream-stable contract: changing any
+	 * of these implies a deliberate change to the test fixture and
+	 * this list together.
+	 */
+	static const char * const required_names[] = {
+		/* lazy_test_basic_group */
+		"basic0", "basic1",
+		/* lazy_test_extra_group */
+		"extra0", "extra1",
+		/* lazy_test_link_group (named subdir) */
+		"link",
+		/* lazy_test_bin_group */
+		"bin0", "bin1",
+		/* driver-core stable rows */
+		"power",
+		"uevent",
+	};
+	static const char * const skip_names[] = {
+		".", "..",
+		"driver", "subsystem",
+		"numa_node",
+		/*
+		 * driver_override comes from bus_add_device() via
+		 * device_add_group(driver_override_dev_group) when
+		 * dev->bus->driver_override is true. Platform bus sets
+		 * it; skip rather than require so the test stays
+		 * portable to buses that don't.
+		 */
+		"driver_override",
+		/*
+		 * modalias is a platform_bus_type dev_groups attribute.
+		 * Required by the platform-bus contract but skipped
+		 * here because we don't pin bus-specific names - the
+		 * test_mod-contributed names are the load-bearing
+		 * surface for this guard.
+		 */
+		"modalias",
+	};
+	char buf[8192];
+	bool seen_required[ARRAY_SIZE(required_names)] = { false };
+	int dir_fd, i;
+	ssize_t nread;
+	size_t total_seen = 0;
+	char order_log[1024];
+	size_t order_log_len = 0;
+
+	/*
+	 * Trigger populate_all on the lazy device's top-level dir.
+	 * The opendir(3)+readdir(3) loop performs the readdir(2) that
+	 * drives the kernfs populate_all() callback; we intentionally
+	 * use opendir for *triggering* and getdents64 for *reading*
+	 * to avoid any libc sort.
+	 */
+	count_dir_entries(DEV_DIR);
+
+	dir_fd = open(DEV_DIR, O_RDONLY | O_DIRECTORY);
+	ASSERT_GE(dir_fd, 0) {
+		TH_LOG("open(%s, O_DIRECTORY) failed: %s",
+		       DEV_DIR, strerror(errno));
+	}
+
+	for (;;) {
+		struct sysfs_lazy_linux_dirent64 *de;
+		off_t off;
+
+		nread = sysfs_lazy_getdents64(dir_fd, buf, sizeof(buf));
+		ASSERT_GE(nread, 0) {
+			TH_LOG("getdents64(%s) failed: %s",
+			       DEV_DIR, strerror(errno));
+		}
+		if (nread == 0)
+			break;
+
+		for (off = 0; off < nread; ) {
+			const char *name;
+			bool skip = false;
+
+			de = (struct sysfs_lazy_linux_dirent64 *)(buf + off);
+			name = de->d_name;
+			off += de->d_reclen;
+
+			for (i = 0; i < (int)ARRAY_SIZE(skip_names); i++) {
+				if (!strcmp(name, skip_names[i])) {
+					skip = true;
+					break;
+				}
+			}
+			if (skip)
+				continue;
+
+			total_seen++;
+
+			/* Append to diagnostic order log. */
+			if (order_log_len + strlen(name) + 2 <
+			    sizeof(order_log)) {
+				if (order_log_len)
+					order_log[order_log_len++] = ' ';
+				memcpy(order_log + order_log_len, name,
+				       strlen(name));
+				order_log_len += strlen(name);
+				order_log[order_log_len] = '\0';
+			}
+
+			for (i = 0; i < (int)ARRAY_SIZE(required_names);
+			     i++) {
+				if (!strcmp(name, required_names[i])) {
+					seen_required[i] = true;
+					break;
+				}
+			}
+		}
+	}
+	close(dir_fd);
+
+	TH_LOG("getdents64 raw order (post-skip, %zu entries): %s",
+	       total_seen, order_log);
+
+	/* Every required name must have been seen. */
+	for (i = 0; i < (int)ARRAY_SIZE(required_names); i++) {
+		EXPECT_TRUE(seen_required[i]) {
+			TH_LOG("required entry '%s' missing from %s",
+			       required_names[i], DEV_DIR);
+		}
+	}
+}
+
+/* ---- test 6: eager_lazy_equivalence ------------------------------------ */
+
+/*
+ * The core contract: after populate_all (triggered by readdir), a lazy
+ * device's sysfs tree must be equivalent to an eager device's tree.
+ * "Equivalent" = same entry names, same entry types (file/dir/link),
+ * same permissions.  We compare both trees recursively, skipping
+ * entries known to differ by identity (driver symlink target contains
+ * the device name, power/ runtime_status may differ by timing).
+ */
+
+#define MAX_ENTRIES	128
+
+struct sysfs_entry {
+	char name[256];
+	char subpath[512]; /* relative path from device root */
+	unsigned char d_type;
+	mode_t mode;
+};
+
+static int cmp_entry(const void *a, const void *b)
+{
+	return strcmp(((const struct sysfs_entry *)a)->subpath,
+		     ((const struct sysfs_entry *)b)->subpath);
+}
+
+/*
+ * Recursively collect all entries under @base into @entries[].
+ * @prefix is the relative path from the device root (empty for top level).
+ * Returns number of entries collected, or negative on error.
+ */
+static int collect_tree(const char *base, const char *prefix,
+			struct sysfs_entry *entries, int max, int idx)
+{
+	char path[PATH_MAX];
+	struct dirent *de;
+	struct stat st;
+	DIR *d;
+
+	snprintf(path, sizeof(path), "%s/%s", base, prefix);
+	d = opendir(path);
+	if (!d)
+		return -errno;
+
+	while ((de = readdir(d)) != NULL) {
+		if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
+			continue;
+		if (idx >= max)
+			break;
+
+		snprintf(entries[idx].name, sizeof(entries[idx].name),
+			 "%s", de->d_name);
+		if (prefix[0])
+			snprintf(entries[idx].subpath,
+				 sizeof(entries[idx].subpath),
+				 "%s/%s", prefix, de->d_name);
+		else
+			snprintf(entries[idx].subpath,
+				 sizeof(entries[idx].subpath),
+				 "%s", de->d_name);
+
+		entries[idx].d_type = de->d_type;
+
+		snprintf(path, sizeof(path), "%s/%s",
+			 base, entries[idx].subpath);
+		if (lstat(path, &st) == 0)
+			entries[idx].mode = st.st_mode;
+
+		idx++;
+
+		/* Recurse into subdirectories */
+		if (de->d_type == DT_DIR) {
+			char sub[512];
+
+			if (prefix[0])
+				snprintf(sub, sizeof(sub), "%s/%s",
+					 prefix, de->d_name);
+			else
+				snprintf(sub, sizeof(sub), "%s", de->d_name);
+			idx = collect_tree(base, sub, entries, max, idx);
+			if (idx < 0) {
+				closedir(d);
+				return idx;
+			}
+		}
+	}
+	closedir(d);
+	return idx;
+}
+
+/* Skip entries that legitimately differ between the two devices */
+static bool skip_entry(const char *subpath)
+{
+	/* The 'driver' symlink target differs (points to different driver) */
+	if (!strcmp(subpath, "driver"))
+		return true;
+	/* power/runtime_status is timing-dependent */
+	if (!strcmp(subpath, "power/runtime_status"))
+		return true;
+	/*
+	 * inject_target is a fault-injection knob materialised by
+	 * sysfs_lazy_test_type_populate{,_all}() and is intentionally
+	 * lazy-only - the eager twin's attribute tables don't expose
+	 * it, so a comparison would always show it as a lazy-only
+	 * extra. The transient_populate_error_propagates test exercises
+	 * it directly; eager_lazy_tree_match has no business asserting
+	 * its presence equivalence.
+	 */
+	if (!strcmp(subpath, "inject_target"))
+		return true;
+	return false;
+}
+
+FIXTURE(sysfs_lazy_equiv) {
+	int dummy;
+};
+
+FIXTURE_SETUP(sysfs_lazy_equiv)
+{
+	unload_module();
+	ASSERT_EQ(0, load_module_with_twin());
+	usleep(100000);
+}
+
+FIXTURE_TEARDOWN(sysfs_lazy_equiv)
+{
+	unload_module();
+}
+
+TEST_F(sysfs_lazy_equiv, eager_lazy_tree_match)
+{
+	struct sysfs_entry *lazy_entries, *eager_entries;
+	int lazy_count, eager_count;
+	int i, j, mismatches = 0;
+
+	lazy_entries = calloc(MAX_ENTRIES, sizeof(*lazy_entries));
+	eager_entries = calloc(MAX_ENTRIES, sizeof(*eager_entries));
+	ASSERT_NE(NULL, lazy_entries);
+	ASSERT_NE(NULL, eager_entries);
+
+	/*
+	 * Trigger populate_all on the lazy device via readdir.
+	 * The eager device already has all entries from device_add().
+	 */
+	count_dir_entries(DEV_DIR);
+
+	/* Collect both trees */
+	lazy_count = collect_tree(DEV_DIR, "", lazy_entries, MAX_ENTRIES, 0);
+	ASSERT_GT(lazy_count, 0);
+
+	eager_count = collect_tree(EAGER_DIR, "", eager_entries,
+				   MAX_ENTRIES, 0);
+	ASSERT_GT(eager_count, 0);
+
+	/* Sort both by subpath for comparison */
+	qsort(lazy_entries, lazy_count, sizeof(*lazy_entries), cmp_entry);
+	qsort(eager_entries, eager_count, sizeof(*eager_entries), cmp_entry);
+
+	/* Compare: every eager entry must exist in lazy with same type/mode */
+	for (i = 0; i < eager_count; i++) {
+		if (skip_entry(eager_entries[i].subpath))
+			continue;
+
+		bool found = false;
+
+		for (j = 0; j < lazy_count; j++) {
+			if (!strcmp(eager_entries[i].subpath,
+				   lazy_entries[j].subpath)) {
+				found = true;
+
+				if (eager_entries[i].d_type !=
+				    lazy_entries[j].d_type) {
+					TH_LOG("type mismatch: %s (eager=%d lazy=%d)",
+					       eager_entries[i].subpath,
+					       eager_entries[i].d_type,
+					       lazy_entries[j].d_type);
+					mismatches++;
+				}
+				if ((eager_entries[i].mode & 07777) !=
+				    (lazy_entries[j].mode & 07777)) {
+					TH_LOG("mode mismatch: %s (eager=%04o lazy=%04o)",
+					       eager_entries[i].subpath,
+					       eager_entries[i].mode & 07777,
+					       lazy_entries[j].mode & 07777);
+					mismatches++;
+				}
+				break;
+			}
+		}
+		if (!found) {
+			TH_LOG("eager entry '%s' missing from lazy tree",
+			       eager_entries[i].subpath);
+			mismatches++;
+		}
+	}
+
+	/* Check reverse: lazy entries not in eager (should be none) */
+	for (j = 0; j < lazy_count; j++) {
+		if (skip_entry(lazy_entries[j].subpath))
+			continue;
+
+		bool found = false;
+
+		for (i = 0; i < eager_count; i++) {
+			if (!strcmp(lazy_entries[j].subpath,
+				   eager_entries[i].subpath)) {
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			TH_LOG("lazy entry '%s' not in eager tree (extra)",
+			       lazy_entries[j].subpath);
+			mismatches++;
+		}
+	}
+
+	EXPECT_EQ(0, mismatches);
+	TH_LOG("compared %d eager entries vs %d lazy entries",
+	       eager_count, lazy_count);
+
+	free(lazy_entries);
+	free(eager_entries);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/sysfs-lazy/test_mod/Makefile b/tools/testing/selftests/sysfs-lazy/test_mod/Makefile
new file mode 100644
index 0000000000000..f7f9bbc896531
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/test_mod/Makefile
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0
+SYSFS_LAZY_TEST_MOD_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
+KDIR ?= $(if $(O),$(O),$(abspath $(SYSFS_LAZY_TEST_MOD_DIR)/../../../../..))
+
+ifeq ($(V),1)
+Q =
+else
+Q = @
+endif
+
+obj-m += sysfs_lazy_test_mod.o
+
+# -rR forces no built-in rules/variables: avoids GNU Make's Modula-2 implicit
+# rule (%.o: %.mod) firing on kbuild's $(obj)/%.mod pseudo-targets when the
+# parent kselftest harness chain did not propagate MAKEFLAGS=-rR.
+#
+# KBUILD_EXTMOD= is set in addition to M= because the kernel Makefile's
+# sub_make_done re-exec changes the origin of M= from "command line" to
+# "environment", and the kernel Makefile only honours M= if its origin is
+# "command line".  Without KBUILD_EXTMOD, kbuild silently falls back to a
+# full in-tree modules build instead of building the external test module.
+all:
+	+$(Q)$(MAKE) -rR -C $(KDIR) M=$(SYSFS_LAZY_TEST_MOD_DIR) \
+		KBUILD_EXTMOD=$(SYSFS_LAZY_TEST_MOD_DIR) modules
+
+clean:
+	+$(Q)$(MAKE) -rR -C $(KDIR) M=$(SYSFS_LAZY_TEST_MOD_DIR) \
+		KBUILD_EXTMOD=$(SYSFS_LAZY_TEST_MOD_DIR) clean
diff --git a/tools/testing/selftests/sysfs-lazy/test_mod/sysfs_lazy_test_mod.c b/tools/testing/selftests/sysfs-lazy/test_mod/sysfs_lazy_test_mod.c
new file mode 100644
index 0000000000000..7a44e220c640f
--- /dev/null
+++ b/tools/testing/selftests/sysfs-lazy/test_mod/sysfs_lazy_test_mod.c
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * sysfs_lazy_test_mod - test module for sysfs lazy initialization
+ *
+ * Registers a lazy platform device (opted in via
+ * device_set_sysfs_lazy()) with 8 attributes across four groups
+ * (basic, extra, named "link", bin) so the userspace selftest can
+ * exercise the integration-level invariants (teardown, leak,
+ * bind/unbind) over realistic attribute shapes.
+ *
+ * When loaded with eager_twin=1, a second identical device is
+ * registered without the lazy opt-in (eager).  The selftest uses
+ * this to assert that lazy populate produces a sysfs tree
+ * equivalent to eager create.
+ *
+ * Per-call populate behaviour (visibility, mode, groups,
+ * idempotency, atomicity) is validated by the in-kernel KUnit suite
+ * drivers/base/test/device_sysfs_apply_test.c.  The selftest no longer
+ * wraps device_ktype to observe populate invocations; that avoids
+ * exporting a driver-core internal type purely for test observability.
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/module.h>
+#include <linux/platform_device.h>
+
+#define MOD_NAME	"sysfs_lazy_test"
+#define EAGER_NAME	"sysfs_lazy_test_eager"
+
+static bool eager_twin;
+module_param(eager_twin, bool, 0444);
+MODULE_PARM_DESC(eager_twin, "Also register an eager (non-lazy) twin device");
+
+/*
+ * Fault-injection knob for the selftest harness: when negative,
+ * dev->type->populate(dev, "inject_target") returns this value
+ * unchanged. The kernfs lookup path forwards a non-(-ENOENT) error
+ * to userspace as the open(2) errno without caching a negative
+ * dentry (see kernfs_iop_lookup() in fs/kernfs/dir.c), exercising
+ * the transient-populate-error contract documented in
+ * Documentation/driver-api/sysfs-lazy.rst end-to-end.
+ *
+ * The companion in-kernel KUnit case walk_enomem_propagates() in
+ * drivers/base/test/device_sysfs_apply_test.c covers the same
+ * contract at the walker boundary; this knob extends coverage to
+ * the userspace open(2) boundary.
+ *
+ * 0 disables injection; "inject_target" then materialises as a
+ * normal sysfs file via dev_attr_inject_target on first lookup.
+ */
+static int inject_errno;
+module_param(inject_errno, int, 0644);
+MODULE_PARM_DESC(inject_errno,
+	"On lazy populate of 'inject_target', return this errno (e.g. -12 = -ENOMEM, -5 = -EIO). 0 disables injection.");
+
+static struct platform_device *test_pdev;
+static struct platform_device *eager_pdev;
+
+/* --- sysfs show helper -------------------------------------------------- */
+
+static ssize_t test_attr_show(struct device *dev,
+			      struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "42\n");
+}
+
+/* --- basic group (2 attrs) ---------------------------------------------- */
+
+static DEVICE_ATTR(basic0, 0444, test_attr_show, NULL);
+static DEVICE_ATTR(basic1, 0444, test_attr_show, NULL);
+
+static struct attribute *lazy_test_basic_attrs[] = {
+	&dev_attr_basic0.attr, &dev_attr_basic1.attr,
+	NULL,
+};
+
+static const struct attribute_group lazy_test_basic_group = {
+	.attrs = lazy_test_basic_attrs,
+};
+
+/* --- extra group (2 attrs) ---------------------------------------------- */
+
+static DEVICE_ATTR(extra0, 0444, test_attr_show, NULL);
+static DEVICE_ATTR(extra1, 0444, test_attr_show, NULL);
+
+static struct attribute *lazy_test_extra_attrs[] = {
+	&dev_attr_extra0.attr, &dev_attr_extra1.attr,
+	NULL,
+};
+
+static const struct attribute_group lazy_test_extra_group = {
+	.attrs = lazy_test_extra_attrs,
+};
+
+/* --- named subdir group "link" (2 attrs) -------------------------------- */
+
+static DEVICE_ATTR(link0, 0444, test_attr_show, NULL);
+static DEVICE_ATTR(link1, 0444, test_attr_show, NULL);
+
+static struct attribute *lazy_test_link_attrs[] = {
+	&dev_attr_link0.attr, &dev_attr_link1.attr,
+	NULL,
+};
+
+static const struct attribute_group lazy_test_link_group = {
+	.name	= "link",
+	.attrs	= lazy_test_link_attrs,
+};
+
+/* --- binary group (2 bin_attrs) ----------------------------------------- */
+
+static ssize_t lazy_bin_read(struct file *f, struct kobject *kobj,
+			     const struct bin_attribute *attr, char *buf,
+			     loff_t off, size_t count)
+{
+	static const char data[] = "binary\n";
+	size_t len = sizeof(data) - 1;
+
+	if (off >= len)
+		return 0;
+	if (off + count > len)
+		count = len - off;
+	memcpy(buf, data + off, count);
+	return count;
+}
+
+static const struct bin_attribute bin_attr_bin0 =
+	__BIN_ATTR(bin0, 0444, lazy_bin_read, NULL, 7);
+static const struct bin_attribute bin_attr_bin1 =
+	__BIN_ATTR(bin1, 0444, lazy_bin_read, NULL, 7);
+
+static const struct bin_attribute *const lazy_test_bin_attrs[] = {
+	&bin_attr_bin0, &bin_attr_bin1,
+	NULL,
+};
+
+static const struct attribute_group lazy_test_bin_group = {
+	.bin_attrs = lazy_test_bin_attrs,
+};
+
+/* --- device groups array ------------------------------------------------ */
+
+static const struct attribute_group *lazy_test_groups[] = {
+	&lazy_test_basic_group,
+	&lazy_test_extra_group,
+	&lazy_test_link_group,
+	&lazy_test_bin_group,
+	NULL,
+};
+
+/* --- fault-injection target (NOT in dev->groups) ------------------------ */
+
+/*
+ * "inject_target" lives outside dev->groups so the standard
+ * create_dev_own_groups() row of driver_core_sysfs_entries[]
+ * cannot match it. The walker therefore returns -ENOENT for this
+ * name and the dispatch falls through to dev->type->populate (see
+ * device_ktype_populate_one() in drivers/base/core.c). That gives
+ * the test a clean injection point at the type->populate boundary
+ * without perturbing the standard group machinery.
+ */
+static DEVICE_ATTR(inject_target, 0444, test_attr_show, NULL);
+
+static int sysfs_lazy_test_type_populate(struct device *dev, const char *name)
+{
+	if (strcmp(name, "inject_target") != 0)
+		return -ENOENT;
+	if (inject_errno < 0)
+		return inject_errno;
+	return sysfs_create_file(&dev->kobj, &dev_attr_inject_target.attr);
+}
+
+static void sysfs_lazy_test_type_populate_all(struct device *dev)
+{
+	/*
+	 * On readdir-driven populate_all, materialise inject_target
+	 * unconditionally when injection is disabled. On injection
+	 * the attribute remains absent so a subsequent lookup still
+	 * exercises the populate-one error path; this matches how a
+	 * real driver whose populate fails would surface as a missing
+	 * entry in readdir output.
+	 */
+	if (inject_errno < 0)
+		return;
+	/*
+	 * Best-effort populate_all: the walker contract discards
+	 * per-row return values on ADD_ALL, and any -EEXIST from a
+	 * concurrent populate_one is harmless here.  Capture the
+	 * return value so GCC's warn_unused_result is satisfied
+	 * (a (void) cast does not silence that attribute).
+	 */
+	if (sysfs_create_file(&dev->kobj, &dev_attr_inject_target.attr))
+		/* Tolerated: best-effort path; populate_one will retry. */
+		;
+}
+
+static const struct device_type sysfs_lazy_test_type = {
+	.name		= "sysfs_lazy_test_type",
+	.populate	= sysfs_lazy_test_type_populate,
+	.populate_all	= sysfs_lazy_test_type_populate_all,
+};
+
+/* --- platform driver ---------------------------------------------------- */
+
+static int lazy_test_probe(struct platform_device *pdev)
+{
+	/* sysfs_lazy is set before platform_device_add in init, NOT here */
+	return 0;
+}
+
+static void lazy_test_remove(struct platform_device *pdev)
+{
+}
+
+static struct platform_driver lazy_test_driver = {
+	.probe		= lazy_test_probe,
+	.remove		= lazy_test_remove,
+	.driver		= {
+		.name = MOD_NAME,
+	},
+};
+
+static struct platform_driver eager_test_driver = {
+	.probe		= lazy_test_probe,
+	.remove		= lazy_test_remove,
+	.driver		= {
+		.name = EAGER_NAME,
+	},
+};
+
+/* --- init / exit -------------------------------------------------------- */
+
+static int __init sysfs_lazy_test_init(void)
+{
+	int ret;
+
+	ret = platform_driver_register(&lazy_test_driver);
+	if (ret)
+		return ret;
+
+	test_pdev = platform_device_alloc(MOD_NAME, PLATFORM_DEVID_NONE);
+	if (!test_pdev) {
+		ret = -ENOMEM;
+		goto err_drv;
+	}
+
+	test_pdev->dev.groups = lazy_test_groups;
+	/*
+	 * Set ->type before device_set_sysfs_lazy() so the type's
+	 * populate / populate_all hooks (sysfs_lazy_test_type) are
+	 * visible to the very first kernfs lookup that races the
+	 * platform_device_add() bring-up.
+	 */
+	test_pdev->dev.type = &sysfs_lazy_test_type;
+	ret = device_set_sysfs_lazy(&test_pdev->dev);  /* ONLY here */
+	if (ret)
+		goto err_pdev;
+
+	ret = platform_device_add(test_pdev);
+	if (ret)
+		goto err_pdev;
+
+	if (eager_twin) {
+		ret = platform_driver_register(&eager_test_driver);
+		if (ret)
+			goto err_lazy_dev;
+
+		eager_pdev = platform_device_alloc(EAGER_NAME,
+						   PLATFORM_DEVID_NONE);
+		if (!eager_pdev) {
+			ret = -ENOMEM;
+			goto err_eager_drv;
+		}
+
+		eager_pdev->dev.groups = lazy_test_groups;
+		/* sysfs_lazy defaults to NULL - eager device */
+
+		ret = platform_device_add(eager_pdev);
+		if (ret)
+			goto err_eager_pdev;
+	}
+
+	pr_info("loaded: /sys/devices/platform/%s (sysfs_lazy=%d)%s\n",
+		MOD_NAME, device_is_sysfs_lazy(&test_pdev->dev),
+		eager_twin ? " + eager twin" : "");
+	return 0;
+
+err_eager_pdev:
+	platform_device_put(eager_pdev);
+	eager_pdev = NULL;
+err_eager_drv:
+	platform_driver_unregister(&eager_test_driver);
+err_lazy_dev:
+	platform_device_unregister(test_pdev);
+	goto err_drv;
+err_pdev:
+	platform_device_put(test_pdev);
+err_drv:
+	platform_driver_unregister(&lazy_test_driver);
+	return ret;
+}
+
+static void __exit sysfs_lazy_test_exit(void)
+{
+	if (eager_pdev) {
+		platform_device_unregister(eager_pdev);
+		platform_driver_unregister(&eager_test_driver);
+	}
+	platform_device_unregister(test_pdev);
+	platform_driver_unregister(&lazy_test_driver);
+	pr_info("unloaded\n");
+}
+
+module_init(sysfs_lazy_test_init);
+module_exit(sysfs_lazy_test_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Test module for sysfs lazy initialization");
+MODULE_AUTHOR("Kernel selftest");
-- 
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-03  1:04 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   ` [RFC PATCH 12/14] driver core: test: add KUnit tests for device_sysfs_apply Pavol Sakac
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   ` Pavol Sakac [this message]
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-10-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