Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up
@ 2026-07-02 17:40 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
                   ` (5 more replies)
  0 siblings, 6 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:40 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Virtualization hardware keeps increasing in CPU-core and VF density.
Kernel is trending towards preserving VFs using LUO across a kexec
which will put SR-IOV in live-update hotpath. VFs must be re-added to
the sysfs tree, along with its supporting devices (e.g. vfio,
iommu_groups). On production hardware a single VF can consume ~80 kernfs
nodes. With thousands of VFs on modern hardware, hundreds of thousands
of kernfs nodes need to be created, which can add 100ms+ to the boot
time and to guest downtime.

Proposed here is a PoC of deferred materialization of sysfs files until
first access to avoid cost of kernfs node creation in hot path. In the
reproducer below using a synthetic VF to isolate the sysfs overhead,
the per-device sysfs-creation time is reduced by ~74%. It exploits the
expected access pattern of hypervisors, where only a subset of device
files need to be accessed to attach a VF to a VM on the hot path.

To allow lazy init scheme, a minimal set of files and directories is
built per device: a device directory resolves directly to the device's
kobject and is used to materialize attributes at access time. The
access is trapped in kernfs at the dir-read or path-walk stage and
depending on the trigger, either the full directory or a single file is
materialized.

The changes are two logically distinct parts:

  1. Refactor of hardcoded device_add / iommu_group sysfs attribute
     creation to a table-driven form to allow walkability of all
     attributes and single point of definition for attributes
     regardless of lazy opt-in to avoid duplicate definitions and
     associated maintainability issues with it.
  2. Lazy-init infra to front-run access to device files at lookup/
     readdir time to trigger materialization. Opt-in per device: PCI VFs
     and VFIO devices opt in via device_set_sysfs_lazy(); iommu_group
     opts in at the kobject level.

As a PoC, the code does not yet follow this logical structure in the
commit sequence and likely does not yet cover all corner cases.

Synthetic reproducer measurements
---------------------------------
  devices   per-dev time   total time       kernfs nodes (base -> lazy)
  -------   ------------   --------------   ----------------------------
     500    31.8->8.3 us    15.9-> 4.1 ms     36,417 ->  3,922  (72.8->7.8/dev)
    1000    33.2->8.7 us    33.2-> 8.7 ms     73,427 ->  8,432  (73.4->8.4/dev)
    2500    32.8->9.1 us    82.0->22.7 ms    184,421 -> 21,925  (73.8->8.8/dev)
    5000    33.0->8.7 us   165.1->43.5 ms    369,401 -> 44,406  (73.9->8.9/dev)

The reproducer is synthetic: an in-kernel module registers N unbound
platform devices (60 attributes / 4 groups, ~74 kernfs nodes each; no
config space, BARs, or probe) in one timed burst that models SR-IOV
enablement. It runs as a two-kernel A/B from one vanilla 7.1-rc2 tree
(eager baseline vs patched, plus a patched/opt-in-OFF arm), 20 runs per
point. Measures only sysfs-creation slice.

Deferral removes ~74% of the sysfs-creation time and ~88% of the kernfs
nodes. At 5000 devices that is 121.6 ms removed (165.1 -> 43.5 ms) and
~325,000 fewer kernfs nodes (369,401 -> 44,406). The table-driven rework
adds a +7..+11% eager-path time overhead to non-opted devices.

Available as a docker image that orchestrates builds in qemu guest and
prints results as a table (x86_64 Linux host with /dev/kvm assumed):

   docker run --device /dev/kvm ghcr.io/pavsa/linux-lazy-sysfs-vf-bench:7.1-rc2

Assumptions / Trade-offs
------------------------
The saving depends on the access pattern of hypervisors, and on no
component walking the whole device tree on the hot path to avoid full
materialization. Both are expected to hold on optimized hypervisors.

An actual production hypervisor was observed in local testing to
materialize an additional 5-10% of total nodes on average on the hot
path.

Overhead: +7..+11% eager-path time overhead applies to every non-opted
device going through device_add() as all struct devices traverse the
walker and can't be gated under a config option.
The lazy opt-in infra can be gated under config option to have zero
impact if disabled.

Final words
-----------
This is an RFC to first get feedback on the decisions taken at driver
core + kernfs level before engaging maintainers of other subsystems
if we want to pursue this further or pivot to something else.

The code is at a proof-of-concept quality written with AI assistance
with some known limitations with main target to gather potential
savings and initiate conversation.

Feedback appreciated for:
- table-driven attribute refactor targeting common device add path /
  suggestions for more efficient/less invasive approach
- mechanism of interception in kernfs, materialization, locking scheme
- fit into context of LUO - complementary or is there some other
  mechanism already considered to mitigate the sysfs impact

Pavol Sakac (14):
  kernfs: add populate callbacks and KERNFS_LAZY flag for lazy dir
    population
  sysfs: add existence-check helpers for lazy populate races
  sysfs: introduce sysfs_kf_syscall_ops dispatching to kobj_type
  driver core: add struct sysfs_lazy_state and device_set_sysfs_lazy()
  driver core: add struct device_sysfs_entry and walker
  driver core: wire device_ktype populate to walker
  driver core: migrate device sysfs to device_sysfs_entry table
  PCI/sysfs: migrate to device_sysfs_entry, defer physfn symlink
  iommu: lazy-populate iommu_group reserved_regions/type attrs
  PCI/IOV: opt SR-IOV VFs into sysfs_lazy
  vfio: opt vfio-dev and VFIO group devices into sysfs_lazy
  driver core: test: add KUnit tests for device_sysfs_apply
  Documentation: add lazy sysfs initialisation and device_sysfs_entry
    docs
  selftests: sysfs-lazy: add tests for lazy sysfs initialization

 Documentation/ABI/testing/sysfs-lazy          |   77 +
 Documentation/driver-api/index.rst            |    1 +
 Documentation/driver-api/sysfs-lazy.rst       |  146 ++
 MAINTAINERS                                   |    4 +
 drivers/base/bus.c                            |   41 +-
 drivers/base/core.c                           | 1047 +++++++++--
 drivers/base/test/.kunitconfig                |    7 +
 drivers/base/test/Kconfig                     |   13 +
 drivers/base/test/Makefile                    |    2 +
 drivers/base/test/device_sysfs_apply_test.c   | 1601 +++++++++++++++++
 drivers/iommu/iommu.c                         |  132 +-
 drivers/pci/iov.c                             |   24 +-
 drivers/pci/pci-sysfs.c                       |  158 +-
 drivers/pci/pci.h                             |    5 +
 drivers/pci/probe.c                           |    4 +-
 drivers/vfio/group.c                          |    5 +
 drivers/vfio/vfio_main.c                      |    5 +
 fs/kernfs/dir.c                               |   43 +-
 fs/sysfs/dir.c                                |   43 +
 fs/sysfs/file.c                               |   39 +
 fs/sysfs/group.c                              |   70 +
 fs/sysfs/mount.c                              |   60 +-
 fs/sysfs/sysfs.h                              |    7 +-
 include/linux/device.h                        |   90 +-
 include/linux/kernfs.h                        |   28 +
 include/linux/kobject.h                       |   15 +
 include/linux/sysfs.h                         |   39 +
 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 ++++
 39 files changed, 5341 insertions(+), 189 deletions(-)
 create mode 100644 Documentation/ABI/testing/sysfs-lazy
 create mode 100644 Documentation/driver-api/sysfs-lazy.rst
 create mode 100644 drivers/base/test/device_sysfs_apply_test.c
 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


base-commit: ec89572766744e844df24c27d31c97b4c00f4e07
-- 
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



^ permalink raw reply	[flat|nested] 17+ messages in thread

* [RFC PATCH 01/14] kernfs: add populate callbacks and KERNFS_LAZY flag for lazy dir population
  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 ` Pavol Sakac
  2026-07-02 17:40 ` [RFC PATCH 02/14] sysfs: add existence-check helpers for lazy populate races Pavol Sakac
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:40 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Add populate() and populate_all() callbacks to struct
kernfs_syscall_ops:

  - populate() runs on a KERNFS_LAZY parent from
    kernfs_iop_lookup() before kernfs_rwsem is taken; the locked
    lookup then proceeds and its result is authoritative. The
    subsystem may materialize the named child during the call.

  - populate_all() runs from kernfs_fop_readdir() before
    kernfs_rwsem is taken. The subsystem may materialize all
    deferred children.

Both are invoked with the parent's active reference held by the
kernfs lookup path, so parent->priv stays live across the
(sleeping) callback. The callback must not drop this pin.

Add KERNFS_LAZY (0x8000) to enum kernfs_node_flag; non-lazy
directories skip the indirect call entirely.

Add kernfs_set_lazy() to mark a directory lazy between linking
into the parent tree and kernfs_activate(). It returns -EINVAL
for namespaced and non-directory nodes: populate dispatch does
not propagate the namespace tag, so a namespaced lazy node would
silently fail tagged lookups. The error code lets callers detect
contract violations without polluting dmesg with a stack trace
and lets KUnit cover the rejection paths via return-value
assertions rather than triggering WARN_ON_ONCE.

Add matching populate/populate_all fields to struct kobj_type.
These are sysfs-only; non-sysfs kernfs roots do not invoke them.

No behavior change: no subsystem sets these callbacks yet.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: linux-kernel@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>
---
 fs/kernfs/dir.c         | 43 +++++++++++++++++++++++++++++++++++++++--
 include/linux/kernfs.h  | 28 +++++++++++++++++++++++++++
 include/linux/kobject.h | 10 ++++++++++
 3 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
index 4f9ade82b08ab..9c5ec8b82d940 100644
--- a/fs/kernfs/dir.c
+++ b/fs/kernfs/dir.c
@@ -975,6 +975,20 @@ struct kernfs_node *kernfs_find_and_get_ns(struct kernfs_node *parent,
 }
 EXPORT_SYMBOL_GPL(kernfs_find_and_get_ns);
 
+int kernfs_set_lazy(struct kernfs_node *kn)
+{
+	struct kernfs_root *root = kernfs_root(kn);
+
+	if (kn->ns || (kn->flags & KERNFS_NS) ||
+	    kernfs_type(kn) != KERNFS_DIR)
+		return -EINVAL;
+
+	down_write(&root->kernfs_rwsem);
+	kn->flags |= KERNFS_LAZY;
+	up_write(&root->kernfs_rwsem);
+	return 0;
+}
+
 /**
  * kernfs_walk_and_get_ns - find and get kernfs_node with the given path
  * @parent: kernfs_node to search under
@@ -1255,6 +1269,19 @@ static struct dentry *kernfs_iop_lookup(struct inode *dir,
 	struct kernfs_root *root;
 	struct inode *inode = NULL;
 	const struct ns_common *ns = NULL;
+	struct kernfs_syscall_ops *scops = kernfs_root(parent)->syscall_ops;
+	int populate_ret = 0;
+
+	/* Dispatch populate before taking kernfs_rwsem; kernfs_get_active()
+	 * pins @parent so @parent->priv stays live across the sleepable call.
+	 */
+	if (scops && scops->populate && (READ_ONCE(parent->flags) & KERNFS_LAZY)) {
+		if (kernfs_get_active(parent)) {
+			populate_ret = scops->populate(parent,
+						       dentry->d_name.name);
+			kernfs_put_active(parent);
+		}
+	}
 
 	root = kernfs_root(parent);
 	down_read(&root->kernfs_rwsem);
@@ -1285,6 +1312,10 @@ static struct dentry *kernfs_iop_lookup(struct inode *dir,
 		kernfs_set_rev(parent, dentry);
 	up_read(&root->kernfs_rwsem);
 
+	/* Transient populate error: do not cache a negative dentry. */
+	if (!kn && populate_ret && populate_ret != -ENOENT)
+		return ERR_PTR(populate_ret);
+
 	/* instantiate and hash (possibly negative) dentry */
 	return d_splice_alias(inode, dentry);
 }
@@ -1973,13 +2004,21 @@ static int kernfs_fop_readdir(struct file *file, struct dir_context *ctx)
 	struct dentry *dentry = file->f_path.dentry;
 	struct kernfs_node *parent = kernfs_dentry_node(dentry);
 	struct kernfs_node *pos = file->private_data;
-	struct kernfs_root *root;
+	struct kernfs_root *root = kernfs_root(parent);
+	struct kernfs_syscall_ops *scops = root->syscall_ops;
 	const struct ns_common *ns = NULL;
 
 	if (!dir_emit_dots(file, ctx))
 		return 0;
 
-	root = kernfs_root(parent);
+	/* Same pinning invariant as kernfs_iop_lookup. */
+	if (scops && scops->populate_all && (READ_ONCE(parent->flags) & KERNFS_LAZY)) {
+		if (kernfs_get_active(parent)) {
+			scops->populate_all(parent);
+			kernfs_put_active(parent);
+		}
+	}
+
 	down_read(&root->kernfs_rwsem);
 
 	if (kernfs_ns_enabled(parent))
diff --git a/include/linux/kernfs.h b/include/linux/kernfs.h
index e21b2f7f4159f..cd529fd843831 100644
--- a/include/linux/kernfs.h
+++ b/include/linux/kernfs.h
@@ -113,6 +113,7 @@ enum kernfs_node_flag {
 	KERNFS_EMPTY_DIR	= 0x1000,
 	KERNFS_HAS_RELEASE	= 0x2000,
 	KERNFS_REMOVING		= 0x4000,
+	KERNFS_LAZY		= 0x8000,  /* subsystem defers child creation */
 };
 
 /* @flags for kernfs_create_root() */
@@ -239,6 +240,7 @@ struct kernfs_node {
  * kernfs_node parameter.
  */
 struct kernfs_syscall_ops {
+	/* Syscall-reactive callbacks */
 	int (*show_options)(struct seq_file *sf, struct kernfs_root *root);
 
 	int (*mkdir)(struct kernfs_node *parent, const char *name,
@@ -248,6 +250,12 @@ struct kernfs_syscall_ops {
 		      const char *new_name);
 	int (*show_path)(struct seq_file *sf, struct kernfs_node *kn,
 			 struct kernfs_root *root);
+
+	/* Lazy dispatch for KERNFS_LAZY directories; @parent is pinned
+	 * via kernfs_get_active() across the call. Both may sleep.
+	 */
+	int (*populate)(struct kernfs_node *parent, const char *name);
+	void (*populate_all)(struct kernfs_node *parent);
 };
 
 struct kernfs_node *kernfs_root_to_node(struct kernfs_root *root);
@@ -398,6 +406,24 @@ static inline bool kernfs_ns_enabled(struct kernfs_node *kn)
 	return kn->flags & KERNFS_NS;
 }
 
+/**
+ * kernfs_set_lazy - mark a kernfs directory for lazy population
+ * @kn: directory kernfs_node (must not be namespaced)
+ *
+ * Sets KERNFS_LAZY under kernfs_rwsem to serialize with other
+ * kn->flags writers.  The flag is set once and never cleared.
+ *
+ * Populate dispatch does not propagate the namespace tag, so a
+ * namespaced lazy node would silently fail tagged lookups.  The
+ * function therefore rejects namespaced and non-directory nodes.
+ *
+ * Return:
+ * * %0           - on success.
+ * * %-EINVAL     - @kn is namespaced (@kn->ns non-NULL or KERNFS_NS
+ *                  flag set) or is not a directory.
+ */
+int kernfs_set_lazy(struct kernfs_node *kn);
+
 int kernfs_name(struct kernfs_node *kn, char *buf, size_t buflen);
 int kernfs_path_from_node(struct kernfs_node *kn_to, struct kernfs_node *kn_from,
 			  char *buf, size_t buflen);
@@ -481,6 +507,8 @@ static inline void kernfs_enable_ns(struct kernfs_node *kn) { }
 static inline bool kernfs_ns_enabled(struct kernfs_node *kn)
 { return false; }
 
+static inline int kernfs_set_lazy(struct kernfs_node *kn) { return 0; }
+
 static inline int kernfs_name(struct kernfs_node *kn, char *buf, size_t buflen)
 { return -ENOSYS; }
 
diff --git a/include/linux/kobject.h b/include/linux/kobject.h
index bcb5d4e320015..cec6de1720076 100644
--- a/include/linux/kobject.h
+++ b/include/linux/kobject.h
@@ -120,6 +120,16 @@ struct kobj_type {
 	const struct kobj_ns_type_operations *(*child_ns_type)(const struct kobject *kobj);
 	const struct ns_common *(*namespace)(const struct kobject *kobj);
 	void (*get_ownership)(const struct kobject *kobj, kuid_t *uid, kgid_t *gid);
+
+	/*
+	 * Lazy sysfs population. Invoked from kernfs lookup / readdir
+	 * paths only when the directory is flagged KERNFS_LAZY. May
+	 * sleep; @kobj is pinned across the call. populate() returns
+	 * 0 / -ENOENT / -errno (kernfs propagates non-ENOENT errors
+	 * to userspace). populate_all() is best-effort.
+	 */
+	int (*populate)(struct kobject *kobj, const char *name);
+	void (*populate_all)(struct kobject *kobj);
 };
 
 struct kobj_uevent_env {
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 02/14] sysfs: add existence-check helpers for lazy populate races
  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 ` Pavol Sakac
  2026-07-02 17:40 ` [RFC PATCH 03/14] sysfs: introduce sysfs_kf_syscall_ops dispatching to kobj_type Pavol Sakac
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:40 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Lazy populate paths (introduced in subsequent commits) acquire a
per-device lock and may walk a table of create callbacks.
A populate_one("X") and populate_all() racing on the same device
serialize through that lock, but each create callback must verify
the entry doesn't already exist before calling sysfs_create_*().
Otherwise the second caller hits __kernfs_create_* which fires
sysfs_warn_dup() before the create callback can absorb -EEXIST.

Add two read-only helpers that wrap kernfs_find_and_get() so
create callbacks can perform the existence check under the same
lock that excludes other lazy create paths. Pairing exists() with
create() under lock makes the check-and-act atomic with respect
to all lazy populate paths on the device, and eager paths are
temporally separated (eager creates run during device_add, before
lazy is enabled).

  - sysfs_kn_exists(kobj, name): true iff @kobj has a child kernfs
    node named @name (any type).

  - sysfs_group_exists(kobj, grp):
    * grp->name != NULL: true iff a KERNFS_DIR child named
      grp->name exists under @kobj->sd (per-attribute presence is
      not inspected; mirrors how internal_create_group()
      short-circuits on -EEXIST).
    * grp->name == NULL: true iff every visible attribute in
      grp->attrs and grp->bin_attrs is present; a partially
      populated unnamed group returns false so a follow-up create
      pass can fill the missing entries.

Both take kernfs_rwsem(read) briefly via kernfs_find_and_get and
return false on bad input or non-existent name; sysfs_group_exists
additionally returns false on a node-type mismatch.

This commit only adds the helpers; subsequent commits convert
create callbacks to use them.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: linux-kernel@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>
---
 fs/sysfs/dir.c        | 25 ++++++++++++++++
 fs/sysfs/file.c       |  2 ++
 fs/sysfs/group.c      | 70 +++++++++++++++++++++++++++++++++++++++++++
 include/linux/sysfs.h | 15 ++++++++++
 4 files changed, 112 insertions(+)

diff --git a/fs/sysfs/dir.c b/fs/sysfs/dir.c
index ffdcd4153c584..ae97ab7e41939 100644
--- a/fs/sysfs/dir.c
+++ b/fs/sysfs/dir.c
@@ -159,3 +159,28 @@ void sysfs_remove_mount_point(struct kobject *parent_kobj, const char *name)
 	kernfs_remove_by_name_ns(parent, name, NULL);
 }
 EXPORT_SYMBOL_GPL(sysfs_remove_mount_point);
+
+
+/**
+ * sysfs_kn_exists - check whether any sysfs entry with @name exists
+ * @kobj: kobject under which to look
+ * @name: name to look up
+ *
+ * Existence probe under per-device sysfs_lazy_state.lock; lets a
+ * caller skip a redundant sysfs_create_*() and the resulting
+ * sysfs_warn_dup() WARN.
+ *
+ * Return: true if a kernfs node of any type with @name exists.
+ */
+bool sysfs_kn_exists(struct kobject *kobj, const char *name)
+{
+	struct kernfs_node *kn;
+
+	if (!kobj || !kobj->sd || !name)
+		return false;
+	kn = kernfs_find_and_get(kobj->sd, name);
+	if (!kn)
+		return false;
+	kernfs_put(kn);
+	return true;
+}
diff --git a/fs/sysfs/file.c b/fs/sysfs/file.c
index 5709cede1d756..5f3144e52ab72 100644
--- a/fs/sysfs/file.c
+++ b/fs/sysfs/file.c
@@ -816,3 +816,5 @@ ssize_t sysfs_bin_attr_simple_read(struct file *file, struct kobject *kobj,
 	return count;
 }
 EXPORT_SYMBOL_GPL(sysfs_bin_attr_simple_read);
+
+
diff --git a/fs/sysfs/group.c b/fs/sysfs/group.c
index 182e54e575ee9..d6b2034cc22c9 100644
--- a/fs/sysfs/group.c
+++ b/fs/sysfs/group.c
@@ -636,3 +636,73 @@ int sysfs_groups_change_owner(struct kobject *kobj,
 	return error;
 }
 EXPORT_SYMBOL_GPL(sysfs_groups_change_owner);
+
+
+/**
+ * sysfs_group_exists - check whether an attribute_group is materialised
+ * @kobj: kobject under which to look
+ * @grp: attribute_group descriptor (may NOT be NULL)
+ *
+ * Read-only existence probe used by lazy populate paths.
+ *
+ * Two shapes:
+ *  - @grp->name != NULL: the group owns its own subdirectory.
+ *    Returns true iff a child kernfs_node of type KERNFS_DIR named
+ *    @grp->name exists under @kobj->sd. Per-attribute presence inside
+ *    the subdirectory is NOT inspected - once the subdir exists the
+ *    create path treats the named group as already materialised
+ *    (consistent with how internal_create_group() short-circuits its
+ *    create on -EEXIST).
+ *  - @grp->name == NULL: the group's attributes live directly under
+ *    @kobj->sd. Returns true iff every visible attribute in
+ *    @grp->attrs and @grp->bin_attrs is already present. A partially-
+ *    populated unnamed group returns false so a follow-up create
+ *    pass can fill the missing entries.
+ *
+ * Locking expectations: see sysfs_kn_exists().
+ *
+ * Return: true if the group is fully present per the rules above,
+ * false otherwise (including @grp == NULL).
+ */
+bool sysfs_group_exists(struct kobject *kobj, const struct attribute_group *grp)
+{
+	struct attribute *const *a;
+	const struct bin_attribute *const *ba;
+	struct kernfs_node *kn;
+	bool exists;
+	int i;
+
+	if (!kobj || !kobj->sd || !grp)
+		return false;
+
+	if (grp->name) {
+		kn = kernfs_find_and_get(kobj->sd, grp->name);
+		if (!kn)
+			return false;
+		exists = kernfs_type(kn) == KERNFS_DIR;
+		kernfs_put(kn);
+		return exists;
+	}
+
+	if (grp->attrs) {
+		for (i = 0, a = grp->attrs; *a; i++, a++) {
+			if (grp->is_visible && !grp->is_visible(kobj, *a, i))
+				continue;
+			kn = kernfs_find_and_get(kobj->sd, (*a)->name);
+			if (!kn)
+				return false;
+			kernfs_put(kn);
+		}
+	}
+	if (grp->bin_attrs) {
+		for (i = 0, ba = grp->bin_attrs; *ba; i++, ba++) {
+			if (grp->is_bin_visible && !grp->is_bin_visible(kobj, *ba, i))
+				continue;
+			kn = kernfs_find_and_get(kobj->sd, (*ba)->attr.name);
+			if (!kn)
+				return false;
+			kernfs_put(kn);
+		}
+	}
+	return true;
+}
diff --git a/include/linux/sysfs.h b/include/linux/sysfs.h
index b1a3a1e6ad09c..de211563f3dec 100644
--- a/include/linux/sysfs.h
+++ b/include/linux/sysfs.h
@@ -428,6 +428,8 @@ int __must_check sysfs_create_bin_file(struct kobject *kobj,
 void sysfs_remove_bin_file(struct kobject *kobj,
 			   const struct bin_attribute *attr);
 
+bool sysfs_kn_exists(struct kobject *kobj, const char *name);
+
 int __must_check sysfs_create_link(struct kobject *kobj, struct kobject *target,
 				   const char *name);
 int __must_check sysfs_create_link_nowarn(struct kobject *kobj,
@@ -454,6 +456,8 @@ void sysfs_remove_group(struct kobject *kobj,
 			const struct attribute_group *grp);
 void sysfs_remove_groups(struct kobject *kobj,
 			 const struct attribute_group *const *groups);
+bool sysfs_group_exists(struct kobject *kobj,
+			const struct attribute_group *grp);
 int sysfs_add_file_to_group(struct kobject *kobj,
 			const struct attribute *attr, const char *group);
 void sysfs_remove_file_from_group(struct kobject *kobj,
@@ -593,6 +597,11 @@ static inline void sysfs_remove_bin_file(struct kobject *kobj,
 {
 }
 
+static inline bool sysfs_kn_exists(struct kobject *kobj, const char *name)
+{
+	return false;
+}
+
 static inline int sysfs_create_link(struct kobject *kobj,
 				    struct kobject *target, const char *name)
 {
@@ -656,6 +665,12 @@ static inline void sysfs_remove_groups(struct kobject *kobj,
 {
 }
 
+static inline bool sysfs_group_exists(struct kobject *kobj,
+				      const struct attribute_group *grp)
+{
+	return false;
+}
+
 static inline int sysfs_add_file_to_group(struct kobject *kobj,
 		const struct attribute *attr, const char *group)
 {
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 03/14] sysfs: introduce sysfs_kf_syscall_ops dispatching to kobj_type
  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 ` 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
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:40 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Wire the sysfs side of the lazy populate chain.  sysfs_init()
previously passed NULL as syscall_ops to kernfs_create_root();
install sysfs_kf_syscall_ops with populate and populate_all
that dispatch to the owning kobject's kobj_type.

The kernfs lookup path pins the parent's active reference, so
parent->priv is safe to read; sysfs_populate() additionally
pins the dereferenced kobject with kobject_get_unless_zero()
across the sleeping ktype->populate call.  sysfs_populate_all()
does the same for ktype->populate_all on readdir.

Both callbacks refuse dispatch in namespaced directories: the
initial consumers (PCI, VFIO) are namespace-agnostic, and tag
propagation is out of scope.  Transient errors propagate without
negative-dentry caching so the next lookup retries populate.

No behavior change: no kobj_type sets populate/populate_all yet.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: linux-api@vger.kernel.org
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: driver-core@lists.linux.dev
Cc: Tejun Heo <tj@kernel.org>
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 fs/sysfs/mount.c | 60 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 59 insertions(+), 1 deletion(-)

diff --git a/fs/sysfs/mount.c b/fs/sysfs/mount.c
index b199e8ff79b1f..9e0a37b60d6e7 100644
--- a/fs/sysfs/mount.c
+++ b/fs/sysfs/mount.c
@@ -10,6 +10,7 @@
  */
 
 #include <linux/fs.h>
+#include <linux/kobject.h>
 #include <linux/magic.h>
 #include <linux/mount.h>
 #include <linux/init.h>
@@ -96,11 +97,68 @@ static struct file_system_type sysfs_fs_type = {
 	.fs_flags		= FS_USERNS_MOUNT,
 };
 
+/*
+ * sysfs_populate - dispatch a lookup miss to the owning kobj_type.
+ *
+ * kernfs pins @parent across the call; we pin @kobj while the
+ * (sleepable) ktype->populate callback runs.
+ */
+static int sysfs_populate(struct kernfs_node *parent, const char *name)
+{
+	struct kobject *kobj;
+	const struct kobj_type *ktype;
+	int ret;
+
+	if (kernfs_ns_enabled(parent))
+		return -ENOENT;
+
+	kobj = parent->priv;
+	ktype = kobj ? kobj->ktype : NULL;
+	if (!ktype || !ktype->populate)
+		return -ENOENT;
+
+	if (!kobject_get_unless_zero(kobj))
+		return -ENOENT;
+
+	ret = ktype->populate(kobj, name);
+
+	kobject_put(kobj);
+	return ret;
+}
+
+/* sysfs_populate_all - readdir-time variant; best-effort, retry-safe. */
+static void sysfs_populate_all(struct kernfs_node *parent)
+{
+	struct kobject *kobj;
+	const struct kobj_type *ktype;
+
+	if (kernfs_ns_enabled(parent))
+		return;
+
+	kobj = parent->priv;
+	ktype = kobj ? kobj->ktype : NULL;
+	if (!ktype || !ktype->populate_all)
+		return;
+
+	if (!kobject_get_unless_zero(kobj))
+		return;
+
+	ktype->populate_all(kobj);
+
+	kobject_put(kobj);
+}
+
+static struct kernfs_syscall_ops sysfs_kf_syscall_ops = {
+	.populate	= sysfs_populate,
+	.populate_all	= sysfs_populate_all,
+};
+
 int __init sysfs_init(void)
 {
 	int err;
 
-	sysfs_root = kernfs_create_root(NULL, KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK,
+	sysfs_root = kernfs_create_root(&sysfs_kf_syscall_ops,
+					KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK,
 					NULL);
 	if (IS_ERR(sysfs_root))
 		return PTR_ERR(sysfs_root);
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 04/14] driver core: add struct sysfs_lazy_state and device_set_sysfs_lazy()
  2026-07-02 17:40 [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Pavol Sakac
                   ` (2 preceding siblings ...)
  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 ` Pavol Sakac
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
  2026-07-10 14:16 ` [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Greg Kroah-Hartman
  5 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:40 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Add the per-device lazy-sysfs bookkeeping struct and accessors that
subsystems use to opt their devices into lazy population:

  - struct sysfs_lazy_state { mutex lock; bool populated;
    bool power_added; } in include/linux/device.h.
  - dev->sysfs_lazy pointer field on struct device. Non-NULL is the
    opt-in signal device_add() consults to mark the kobj's kernfs
    directory KERNFS_LAZY.
  - Public accessors device_is_sysfs_lazy(), device_sysfs_populated(),
    device_sysfs_set_populated().
    device_sysfs_populated() uses smp_load_acquire() to pair with
    device_sysfs_set_populated()'s smp_store_release(); this lets
    the lockless fast-path in device_ktype_populate_all() (added in
    the next commit) skip the mutex while still observing all
    kernfs_create_*() side-effects performed by populate_all under
    @lock. The canonical write is under @lock.
  - device_set_sysfs_lazy() allocates the bookkeeping; idempotent and
    must be called BEFORE device_add(). Freed by device_release().

device_add() consults dev->sysfs_lazy after kobject_add(); if set,
kernfs_set_lazy(dev->kobj.sd) gates kernfs lookups and readdirs to
device_ktype.populate / populate_all.  No driver-core attribute
content is yet routed through the walker - that lands in the next
commit.  No existing caller opts in here; the plumbing is in place
for subsystems that follow in this series.

The "blob whose pointer doubles as the opt-in signal" pattern
mirrors dev_iommu_get() in drivers/iommu/iommu.c.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: driver-core@lists.linux.dev
Cc: linux-api@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/core.c    | 74 ++++++++++++++++++++++++++++++++++++++++++
 include/linux/device.h | 28 ++++++++++++++++
 2 files changed, 102 insertions(+)

diff --git a/drivers/base/core.c b/drivers/base/core.c
index bd2ddf2aab505..6d0d917d4b1ff 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -2555,6 +2555,10 @@ static void device_release(struct kobject *kobj)
 	 */
 	devres_release_all(dev);
 
+	if (dev->sysfs_lazy)
+		mutex_destroy(&dev->sysfs_lazy->lock);
+	kfree(dev->sysfs_lazy);
+
 	kfree(dev->dma_range_map);
 	kfree(dev->driver_override.name);
 
@@ -2588,6 +2592,67 @@ static void device_get_ownership(const struct kobject *kobj, kuid_t *uid, kgid_t
 		dev->class->get_ownership(dev, uid, gid);
 }
 
+/*
+ * True once populate_all has completed; lockless.  False for non-lazy.
+ *
+ * Memory-ordering: pairs with device_sysfs_set_populated()'s
+ * smp_store_release().  smp_load_acquire ensures any kernfs_create_*()
+ * side-effects performed by populate_all under sysfs_lazy.lock become
+ * visible to readers that observe @populated == true.  The kernfs
+ * lookup path also takes kernfs_rwsem(read) which provides an
+ * independent memory-barrier; the explicit acquire here is belt-and-
+ * braces for callers that may bypass kernfs (e.g. internal sysfs_*
+ * helpers and the KUnit suite).
+ */
+bool device_sysfs_populated(const struct device *dev)
+{
+	return dev->sysfs_lazy &&
+	       smp_load_acquire(&dev->sysfs_lazy->populated);
+}
+
+/*
+ * Latch @dev as fully populated. Caller holds @dev->sysfs_lazy->lock.
+ *
+ * Memory-ordering: smp_store_release() publishes all kernfs_create_*()
+ * side-effects performed under sysfs_lazy.lock to lockless readers
+ * via device_sysfs_populated()'s smp_load_acquire().
+ */
+void device_sysfs_set_populated(struct device *dev)
+{
+	smp_store_release(&dev->sysfs_lazy->populated, true);
+}
+
+/**
+ * device_set_sysfs_lazy - opt @dev into lazy sysfs (call before device_add())
+ * @dev: device to opt in
+ *
+ * Allocates @dev->sysfs_lazy.  Idempotent: a second call on a
+ * device that already opted in is a no-op.  Must be called before
+ * device_add().  After device_add() returns, eager attribute creation
+ * is skipped and walker dispatch lazily materializes attributes on
+ * first sysfs lookup or readdir.
+ *
+ * Return:
+ * * %0       - on success (or if already opted in).
+ * * %-ENOMEM - allocation failure.
+ */
+int device_set_sysfs_lazy(struct device *dev)
+{
+	struct sysfs_lazy_state *lazy;
+
+	if (dev->sysfs_lazy)
+		return 0;
+
+	lazy = kzalloc_obj(*lazy);
+	if (!lazy)
+		return -ENOMEM;
+
+	mutex_init(&lazy->lock);
+	dev->sysfs_lazy = lazy;
+	return 0;
+}
+EXPORT_SYMBOL_GPL(device_set_sysfs_lazy);
+
 static const struct kobj_type device_ktype = {
 	.release	= device_release,
 	.sysfs_ops	= &dev_sysfs_ops,
@@ -3642,6 +3707,15 @@ int device_add(struct device *dev)
 	error = device_add_class_symlinks(dev);
 	if (error)
 		goto SymlinkError;
+	/*
+	 * device_set_sysfs_lazy() only allocates ->sysfs_lazy_state for
+	 * non-namespaced devices, and device_is_sysfs_lazy() guards opt-in
+	 * here, so kernfs_set_lazy() should always succeed.  WARN_ON catches
+	 * a regression in those preconditions.
+	 */
+	if (device_is_sysfs_lazy(dev))
+		WARN_ON(kernfs_set_lazy(dev->kobj.sd));
+
 	error = device_add_attrs(dev);
 	if (error)
 		goto AttrsError;
diff --git a/include/linux/device.h b/include/linux/device.h
index 9c8fde6a3d866..39e08e8c950c6 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -45,6 +45,18 @@ struct fwnode_handle;
 struct iommu_group;
 struct dev_pin_info;
 struct dev_iommu;
+/**
+ * struct sysfs_lazy_state - per-device lazy sysfs population state
+ * @lock: serialises populate callbacks (held across device_sysfs_apply())
+ * @populated: full-populate latch
+ * @power_added: dpm_sysfs_add() latch
+ */
+struct sysfs_lazy_state {
+	struct mutex	lock;
+	bool		populated;
+	bool		power_added;
+};
+
 struct msi_device_data;
 
 /**
@@ -739,9 +751,25 @@ struct device {
 	bool			dma_iommu:1;
 #endif
 
+	/* Lazy-sysfs opt-in (NULL = eager). Set via device_set_sysfs_lazy(). */
+	struct sysfs_lazy_state	*sysfs_lazy;
+
 	DECLARE_BITMAP(flags, DEV_FLAG_COUNT);
 };
 
+/**
+ * device_is_sysfs_lazy - true if @dev opted into lazy sysfs
+ * @dev: device to query
+ */
+static inline bool device_is_sysfs_lazy(const struct device *dev)
+{
+	return !!dev->sysfs_lazy;
+}
+
+bool device_sysfs_populated(const struct device *dev);
+void device_sysfs_set_populated(struct device *dev);
+int device_set_sysfs_lazy(struct device *dev);
+
 #define __create_dev_flag_accessors(accessor_name, flag_name) \
 static inline bool dev_##accessor_name(const struct device *dev) \
 { \
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker
  2026-07-02 17:40 [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Pavol Sakac
                   ` (3 preceding siblings ...)
  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 ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 06/14] driver core: wire device_ktype populate to walker Pavol Sakac
                     ` (8 more replies)
  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
  5 siblings, 9 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Introduce the primitive for declaring per-device sysfs content as
table rows and a single walker that dispatches across eager,
lazy-populate, and teardown paths.

struct device_sysfs_entry { name, applies_to, create, remove }

  - name == NULL: wildcard row (create() does internal name
    dispatch; used for attribute_group-source rows).
  - applies_to: NULL or a pure, cheap, non-sleeping predicate.
  - create: implements ADD_ONE (name != NULL) and ADD_ALL
    (name == NULL) semantics; absorbs -EEXIST from concurrent
    racers to 0.
  - remove: reverse-order teardown.

device_sysfs_apply(dev, entries, action, name) dispatches
DEV_SYSFS_ADD_ONE / DEV_SYSFS_ADD_ALL / DEV_SYSFS_REMOVE_ALL across
the table. -ENOENT from a wildcard row's create() is treated as
"not my name" and the walker continues; any other error propagates
to the caller on ADD_ONE and is absorbed on ADD_ALL (best-effort).
REMOVE_ALL reverses the table and never aborts. The full contract
is documented in Documentation/driver-api/sysfs-lazy.rst (added
later in this series).

Shape follows cftype + cgroup_addrm_files (kernel/cgroup/cgroup.c)
as the in-tree precedent for table-driven attribute creation.
struct kobj_type grows a trailing .entries pointer; ktypes opt in
by setting it and wiring their populate / populate_all callbacks
to the walker. The field is at end of struct for source-
compatibility with positional initialisers of out-of-tree ktypes.

sysfs_add_file_mode_ns() and sysfs_add_bin_file_mode_ns() are
exported (kerneldoc added) so the walker can absorb -EEXIST from
concurrent create racers without going through sysfs_warn_dup().

Two static helpers - create_attr_in_groups() and
create_all_in_groups() - are added for use by attribute-group
source rows that the migration commit later wires up; they search
@groups by @name and walk every visible attribute respectively.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: Jonathan Corbet <corbet@lwn.net>
Cc: Bjorn Helgaas <bhelgaas@google.com>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: linux-api@vger.kernel.org
Cc: driver-core@lists.linux.dev
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/core.c     | 319 ++++++++++++++++++++++++++++++++++++++++
 fs/sysfs/file.c         |  41 +++++-
 fs/sysfs/sysfs.h        |   6 -
 include/linux/device.h  |  56 +++++++
 include/linux/kobject.h |   5 +
 include/linux/sysfs.h   |  24 +++
 6 files changed, 443 insertions(+), 8 deletions(-)

diff --git a/drivers/base/core.c b/drivers/base/core.c
index 6d0d917d4b1ff..aeb4985eb8838 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -2574,6 +2574,325 @@ static void device_release(struct kobject *kobj)
 	kfree(p);
 }
 
+/*
+ * device_sysfs_apply() - declarative per-device sysfs dispatch
+ *
+ * Single walker over a sentinel-terminated struct device_sysfs_entry
+ * table. Intended to be invoked on every dispatch path (device_add
+ * eager, lazy populate_one, lazy populate_all, device_del teardown)
+ * so adding a new per-device file is one row, not four open-coded
+ * branches. Shape follows cftype + cgroup_addrm_files
+ * (kernel/cgroup/cgroup.c) and pci_sysfs_entries[]
+ * (drivers/pci/pci-sysfs.c).
+ *
+ * ADD_ONE:  first row whose applies_to passes and whose name matches
+ *	     (or a wildcard row whose create() does not return
+ *	     -ENOENT) terminates the walk. -ENOENT from a wildcard
+ *	     row's create() signals "not my name"; the walker
+ *	     continues. Realize callbacks MUST check existence (via
+ *	     sysfs_*_exists()) before calling sysfs_create_*() and
+ *	     return 0 if the entry already exists. Reaching
+ *	     __kernfs_create_*() for a name that another lazy path
+ *	     just created is forbidden under lock
+ *	     serialization. If sysfs_warn_dup() ever fires from a
+ *	     lazy path, it indicates a lock invariant
+ *	     violation or a non-lazy path creating lazy attrs (bug).
+ * ADD_ALL:  every applicable row's create() fires with name = NULL;
+ *	     per-row errors are best-effort - the walker discards
+ *	     return values and rows log via their own diagnostics.
+ * REMOVE_ALL: reverse row order; remove() fires for every row whose
+ *	     applies_to passes. Teardown never aborts.
+ *
+ * The full walker contract (error-absorption matrix, interaction
+ * with negative-dentry caching, lifecycle caveats) is documented in
+ * Documentation/driver-api/sysfs-lazy.rst, added later in this
+ * series.
+ */
+/*
+ * Create one named attr inside @grp; honours grp->is_visible.
+ * Returns 0 on success/hidden/already-present, -ENOENT on no-match.
+ */
+static int create_attr_in_group(struct device *dev,
+				 const struct attribute_group *grp,
+				 const char *name)
+{
+	struct attribute **a;
+	const struct bin_attribute *const *ba;
+	kuid_t uid;
+	kgid_t gid;
+	umode_t mode;
+	int i;
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	kobject_get_ownership(&dev->kobj, &uid, &gid);
+
+	if (grp->name) {
+		if (!strcmp(grp->name, name)) {
+			if (sysfs_group_exists(&dev->kobj, grp))
+				return 0;
+			return sysfs_create_group(&dev->kobj, grp);
+		}
+		return -ENOENT;
+	}
+
+	if (grp->attrs) {
+		for (i = 0, a = grp->attrs; *a; i++, a++) {
+			if (strcmp((*a)->name, name))
+				continue;
+
+			mode = (*a)->mode;
+			if (grp->is_visible || grp->is_visible_const) {
+				if (grp->is_visible)
+					mode = grp->is_visible(&dev->kobj, *a, i);
+				else
+					mode = grp->is_visible_const(&dev->kobj, *a, i);
+				mode &= ~SYSFS_GROUP_INVISIBLE;
+				if (!mode)
+					return 0;	/* hidden */
+			}
+			mode &= SYSFS_PREALLOC | 0664;
+
+			if (sysfs_kn_exists(&dev->kobj, (*a)->name))
+				return 0;
+
+			return sysfs_add_file_mode_ns(dev->kobj.sd, *a,
+						      mode, uid, gid, NULL);
+		}
+	}
+	if (grp->bin_attrs) {
+		for (i = 0, ba = grp->bin_attrs; *ba; i++, ba++) {
+			if (strcmp((*ba)->attr.name, name))
+				continue;
+			mode = (*ba)->attr.mode;
+			if (grp->is_bin_visible) {
+				mode = grp->is_bin_visible(&dev->kobj, *ba, i);
+				mode &= ~SYSFS_GROUP_INVISIBLE;
+				if (!mode)
+					return 0;
+			}
+			mode &= SYSFS_PREALLOC | 0664;
+
+			if (sysfs_kn_exists(&dev->kobj, (*ba)->attr.name))
+				return 0;
+
+			return sysfs_add_bin_file_mode_ns(dev->kobj.sd,
+							  *ba, mode,
+							  (*ba)->size,
+							  uid, gid, NULL);
+		}
+	}
+	return -ENOENT;
+}
+
+/*
+ * Per-group incremental creation. Tolerates partial state from
+ * concurrent populate_one. Best-effort: absorbs per-attr failures.
+ */
+static int create_group_incremental(struct device *dev,
+				     const struct attribute_group *grp)
+{
+	struct attribute **a;
+	const struct bin_attribute *const *ba;
+	int ret;
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (grp->name) {
+		struct kernfs_node *subdir;
+		kuid_t uid;
+		kgid_t gid;
+		umode_t mode;
+		int i;
+
+		subdir = kernfs_find_and_get(dev->kobj.sd, grp->name);
+		if (!subdir)
+			return sysfs_create_group(&dev->kobj, grp);
+
+		kobject_get_ownership(&dev->kobj, &uid, &gid);
+
+		if (grp->attrs) {
+			for (i = 0, a = grp->attrs; *a; i++, a++) {
+				struct kernfs_node *kn;
+
+				mode = (*a)->mode;
+				if (grp->is_visible || grp->is_visible_const) {
+					if (grp->is_visible)
+						mode = grp->is_visible(&dev->kobj,
+								       *a, i);
+					else
+						mode = grp->is_visible_const(&dev->kobj,
+									     *a, i);
+					mode &= ~SYSFS_GROUP_INVISIBLE;
+					if (!mode)
+						continue;
+				}
+				mode &= SYSFS_PREALLOC | 0664;
+
+				/* Existence check inside named @subdir (not a kobject). */
+				kn = kernfs_find_and_get(subdir, (*a)->name);
+				if (kn) {
+					kernfs_put(kn);
+					continue;
+				}
+
+				ret = sysfs_add_file_mode_ns(subdir, *a,
+							     mode, uid,
+							     gid, NULL);
+				if (ret)
+					dev_dbg(dev,
+						"lazy %s/%s: %d\n",
+						grp->name, (*a)->name, ret);
+			}
+		}
+		if (grp->bin_attrs) {
+			for (i = 0, ba = grp->bin_attrs; *ba; i++, ba++) {
+				struct kernfs_node *kn;
+
+				mode = (*ba)->attr.mode;
+				if (grp->is_bin_visible) {
+					mode = grp->is_bin_visible(&dev->kobj,
+								   *ba, i);
+					mode &= ~SYSFS_GROUP_INVISIBLE;
+					if (!mode)
+						continue;
+				}
+				mode &= SYSFS_PREALLOC | 0664;
+
+				kn = kernfs_find_and_get(subdir, (*ba)->attr.name);
+				if (kn) {
+					kernfs_put(kn);
+					continue;
+				}
+
+				ret = sysfs_add_bin_file_mode_ns(subdir,
+								 *ba,
+								 mode,
+								 (*ba)->size,
+								 uid,
+								 gid,
+								 NULL);
+				if (ret)
+					dev_dbg(dev,
+						"lazy %s/%s: %d\n",
+						grp->name,
+						(*ba)->attr.name, ret);
+			}
+		}
+
+		kernfs_put(subdir);
+		return 0;
+	}
+
+	if (grp->attrs) {
+		for (a = grp->attrs; *a; a++)
+			(void)create_attr_in_group(dev, grp, (*a)->name);
+	}
+	if (grp->bin_attrs) {
+		for (ba = grp->bin_attrs; *ba; ba++)
+			(void)create_attr_in_group(dev, grp,
+						    (*ba)->attr.name);
+	}
+	return 0;
+}
+
+/* Search @groups for @name. -ENOENT = not in this source. */
+static int __maybe_unused create_attr_in_groups(struct device *dev,
+				  const struct attribute_group *const *groups,
+				  const char *name)
+{
+	const struct attribute_group *const *g;
+	int ret;
+
+	if (!groups)
+		return -ENOENT;
+
+	for (g = groups; *g; g++) {
+		ret = create_attr_in_group(dev, *g, name);
+		if (ret != -ENOENT)
+			return ret;
+	}
+	return -ENOENT;
+}
+
+/* Walk @groups creating every visible attribute. Best-effort. */
+static int __maybe_unused create_all_in_groups(struct device *dev,
+				 const struct attribute_group *const *groups)
+{
+	const struct attribute_group *const *g;
+
+	if (!groups)
+		return 0;
+
+	for (g = groups; *g; g++)
+		(void)create_group_incremental(dev, *g);
+	return 0;
+}
+
+static const struct device_sysfs_entry *
+device_sysfs_entries_end(const struct device_sysfs_entry *entries)
+{
+	const struct device_sysfs_entry *e = entries;
+
+	while (e->create)
+		e++;
+	return e;
+}
+
+static __maybe_unused int
+device_sysfs_apply(struct device *dev,
+		  const struct device_sysfs_entry *entries,
+		  enum dev_sysfs_action action, const char *name)
+{
+	const struct device_sysfs_entry *e;
+
+	if (!entries)
+		return action == DEV_SYSFS_ADD_ONE ? -ENOENT : 0;
+
+	if (action == DEV_SYSFS_REMOVE_ALL) {
+		for (e = device_sysfs_entries_end(entries) - 1;
+		     e >= entries; e--) {
+			if (e->applies_to && !e->applies_to(dev))
+				continue;
+			if (e->remove)
+				e->remove(dev);
+		}
+		return 0;
+	}
+
+	for (e = entries; e->create; e++) {
+		int ret;
+
+		if (e->applies_to && !e->applies_to(dev))
+			continue;
+		if (action == DEV_SYSFS_ADD_ONE && e->name &&
+		    strcmp(e->name, name))
+			continue;
+
+		ret = e->create(dev, action == DEV_SYSFS_ADD_ONE
+					      ? name : NULL);
+		if (action == DEV_SYSFS_ADD_ONE) {
+			/*
+			 * A named row is the authoritative handler for its
+			 * name: return its result even on -ENOENT. A wildcard
+			 * row (e->name == NULL) may decline with -ENOENT, so
+			 * keep scanning for another handler in that case.
+			 */
+			if (e->name || ret != -ENOENT)
+				return ret;
+		}
+		/*
+		 * FIXME: ADD_ALL is best-effort and never returns the first create() failure to
+		 * the caller, so device_ktype_populate_all() latches populated=true after a
+		 * transient failure - the missing attribute is negative-cached forever (a later
+		 * lookup returns -ENOENT and never retries), contradicting the sysfs-lazy ABI.
+		 */
+	}
+	return action == DEV_SYSFS_ADD_ONE ? -ENOENT : 0;
+}
+
 static const struct ns_common *device_namespace(const struct kobject *kobj)
 {
 	const struct device *dev = kobj_to_dev(kobj);
diff --git a/fs/sysfs/file.c b/fs/sysfs/file.c
index 5f3144e52ab72..8d0b8c2c2ee4d 100644
--- a/fs/sysfs/file.c
+++ b/fs/sysfs/file.c
@@ -270,6 +270,25 @@ static const struct kernfs_ops sysfs_bin_kfops_mmap = {
 	.llseek		= sysfs_kf_bin_llseek,
 };
 
+/**
+ * sysfs_add_file_mode_ns - create a sysfs attribute file under @parent
+ * @parent: kernfs node for the parent directory (typically @kobj->sd)
+ * @attr:   attribute descriptor; @attr->name is the file name
+ * @mode:   file mode (low bits) optionally OR'd with %SYSFS_PREALLOC
+ * @uid: file owner UID
+ * @gid: file owner GID
+ * @ns:  namespace tag (may be %NULL)
+ *
+ * Picks a kernfs_ops dispatch row from (prealloc?, has show, has store)
+ * routing read()/write() through @parent->priv->ktype->sysfs_ops.
+ *
+ * Return: 0 on success, or -errno. -EEXIST means the name already
+ * exists and is treated as a caller error (it triggers sysfs_warn_dup()).
+ * The lazy populate path must not reach this: it checks sysfs_kn_exists()/
+ * sysfs_group_exists() under the populate lock before creating, so a
+ * concurrent ADD_ONE/ADD_ALL race resolves to a no-op rather than a
+ * duplicate create.
+ */
 int sysfs_add_file_mode_ns(struct kernfs_node *parent,
 		const struct attribute *attr, umode_t mode, kuid_t uid,
 		kgid_t gid, const struct ns_common *ns)
@@ -320,6 +339,26 @@ int sysfs_add_file_mode_ns(struct kernfs_node *parent,
 	return 0;
 }
 
+/**
+ * sysfs_add_bin_file_mode_ns - create a sysfs binary-attribute file
+ * @parent: kernfs node for the parent directory
+ * @battr:  binary attribute descriptor
+ * @mode:   file mode (low bits)
+ * @size:   in-core size of the binary attribute (passed to __kernfs_create_file())
+ * @uid: file owner UID
+ * @gid: file owner GID
+ * @ns:  namespace tag (may be %NULL)
+ *
+ * Picks a kernfs_ops dispatch row from a 5-row matrix on @battr's
+ * callbacks (mmap > rw > read > write > empty).
+ *
+ * Return: 0 on success, or -errno. -EEXIST means the name already
+ * exists and is treated as a caller error (it triggers sysfs_warn_dup()).
+ * The lazy populate path must not reach this: it checks sysfs_kn_exists()/
+ * sysfs_group_exists() under the populate lock before creating, so a
+ * concurrent ADD_ONE/ADD_ALL race resolves to a no-op rather than a
+ * duplicate create.
+ */
 int sysfs_add_bin_file_mode_ns(struct kernfs_node *parent,
 		const struct bin_attribute *battr, umode_t mode, size_t size,
 		kuid_t uid, kgid_t gid, const struct ns_common *ns)
@@ -816,5 +855,3 @@ ssize_t sysfs_bin_attr_simple_read(struct file *file, struct kobject *kobj,
 	return count;
 }
 EXPORT_SYMBOL_GPL(sysfs_bin_attr_simple_read);
-
-
diff --git a/fs/sysfs/sysfs.h b/fs/sysfs/sysfs.h
index f4583dcafcd1e..94b8aca20bffc 100644
--- a/fs/sysfs/sysfs.h
+++ b/fs/sysfs/sysfs.h
@@ -27,12 +27,6 @@ void sysfs_warn_dup(struct kernfs_node *parent, const char *name);
 /*
  * file.c
  */
-int sysfs_add_file_mode_ns(struct kernfs_node *parent,
-		const struct attribute *attr, umode_t amode, kuid_t uid,
-		kgid_t gid, const struct ns_common *ns);
-int sysfs_add_bin_file_mode_ns(struct kernfs_node *parent,
-		const struct bin_attribute *battr, umode_t mode, size_t size,
-		kuid_t uid, kgid_t gid, const struct ns_common *ns);
 
 /*
  * symlink.c
diff --git a/include/linux/device.h b/include/linux/device.h
index 39e08e8c950c6..e5485bfbc6c84 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -88,6 +88,62 @@ int subsys_system_register(const struct bus_type *subsys,
 int subsys_virtual_register(const struct bus_type *subsys,
 			    const struct attribute_group **groups);
 
+/**
+ * enum dev_sysfs_action - verb selector for device_sysfs_apply()
+ * @DEV_SYSFS_ADD_ONE: realize a single named entry (lazy populate_one
+ *		       miss path); walker stops at the first row whose
+ *		       @applies_to passes and whose create() returns
+ *		       anything other than -ENOENT.
+ * @DEV_SYSFS_ADD_ALL: realize every applicable entry (eager device_add
+ *		       or lazy populate_all); best-effort, per-row errors
+ *		       do not abort the walk.
+ * @DEV_SYSFS_REMOVE_ALL: tear down every applicable entry in reverse
+ *		       row order (device_del teardown).
+ */
+enum dev_sysfs_action {
+	DEV_SYSFS_ADD_ONE,
+	DEV_SYSFS_ADD_ALL,
+	DEV_SYSFS_REMOVE_ALL,
+};
+
+/**
+ * struct device_sysfs_entry - declarative per-device sysfs content row
+ * @name:	Attribute or symlink name at the top level of the device's
+ *		sysfs directory, or %NULL for a wildcard row whose
+ *		create() performs internal name dispatch (typically a
+ *		group-source row iterating an attribute_group array).
+ * @applies_to: Optional predicate gating row eligibility for this
+ *		device. Must be cheap, non-sleeping, side-effect-free, and
+ *		monotonic for a given device state. MUST NOT acquire any
+ *		lock the walker's caller may already hold (notably
+ *		device_lock()). %NULL means unconditional.
+ * @create:	Required. Creates the row's sysfs content. For a named
+ *		row (@name != NULL) the @name argument equals @e->name on
+ *		ADD_ONE and %NULL on ADD_ALL. For a wildcard row the @name
+ *		argument is the ADD_ONE target (or %NULL on ADD_ALL) and
+ *		the row's create() matches internally, returning -ENOENT
+ *		to signal "not my name" so the walker continues. Must
+ *		convert -EEXIST from concurrent racers to 0.
+ * @remove:	Optional reverse-of-realize teardown. Called on
+ *		REMOVE_ALL in reverse row order. MUST NOT fail the walk.
+ *
+ * Rows are declared in file-static, sentinel-terminated tables whose
+ * base pointer is published to the walker via struct kobj_type.entries
+ * (device ktype shares a single driver_core table and dispatches to
+ * dev->type->entries as well). The row shape mirrors cftype +
+ * cgroup_addrm_files (kernel/cgroup/cgroup.c) and the
+ * pci_sysfs_entries[] at drivers/pci/pci-sysfs.c (added later in
+ * the series); see Documentation/driver-api/sysfs-lazy.rst (also
+ * added later in the series) for the full walker contract and
+ * error-handling matrix.
+ */
+struct device_sysfs_entry {
+	const char *name;
+	bool (*applies_to)(struct device *dev);
+	int  (*create)(struct device *dev, const char *name);
+	void (*remove)(struct device *dev);
+};
+
 /*
  * The type of device, "struct device" is embedded in. A class
  * or bus can contain devices of different types
diff --git a/include/linux/kobject.h b/include/linux/kobject.h
index cec6de1720076..c724d86e1a049 100644
--- a/include/linux/kobject.h
+++ b/include/linux/kobject.h
@@ -61,6 +61,8 @@ enum kobject_action {
 	KOBJ_UNBIND,
 };
 
+struct device_sysfs_entry;
+
 struct kobject {
 	const char		*name;
 	struct list_head	entry;
@@ -130,6 +132,9 @@ struct kobj_type {
 	 */
 	int (*populate)(struct kobject *kobj, const char *name);
 	void (*populate_all)(struct kobject *kobj);
+
+	/* Optional table of per-attribute entries (see device_sysfs_apply()). */
+	const struct device_sysfs_entry *entries;
 };
 
 struct kobj_uevent_env {
diff --git a/include/linux/sysfs.h b/include/linux/sysfs.h
index de211563f3dec..aac2f773d5378 100644
--- a/include/linux/sysfs.h
+++ b/include/linux/sysfs.h
@@ -415,6 +415,12 @@ int __must_check sysfs_create_files(struct kobject *kobj,
 				   const struct attribute * const *attr);
 int __must_check sysfs_chmod_file(struct kobject *kobj,
 				  const struct attribute *attr, umode_t mode);
+int __must_check sysfs_add_file_mode_ns(struct kernfs_node *parent,
+		const struct attribute *attr, umode_t amode, kuid_t uid,
+		kgid_t gid, const struct ns_common *ns);
+int __must_check sysfs_add_bin_file_mode_ns(struct kernfs_node *parent,
+		const struct bin_attribute *battr, umode_t mode, size_t size,
+		kuid_t uid, kgid_t gid, const struct ns_common *ns);
 struct kernfs_node *sysfs_break_active_protection(struct kobject *kobj,
 						  const struct attribute *attr);
 void sysfs_unbreak_active_protection(struct kernfs_node *kn);
@@ -558,6 +564,24 @@ static inline int sysfs_chmod_file(struct kobject *kobj,
 	return 0;
 }
 
+static inline int sysfs_add_file_mode_ns(struct kernfs_node *parent,
+					 const struct attribute *attr,
+					 umode_t amode, kuid_t uid,
+					 kgid_t gid,
+					 const struct ns_common *ns)
+{
+	return 0;
+}
+
+static inline int sysfs_add_bin_file_mode_ns(struct kernfs_node *parent,
+					     const struct bin_attribute *battr,
+					     umode_t mode, size_t size,
+					     kuid_t uid, kgid_t gid,
+					     const struct ns_common *ns)
+{
+	return 0;
+}
+
 static inline struct kernfs_node *
 sysfs_break_active_protection(struct kobject *kobj,
 			      const struct attribute *attr)
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 06/14] driver core: wire device_ktype populate to walker
  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   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 07/14] driver core: migrate device sysfs to device_sysfs_entry table Pavol Sakac
                     ` (7 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Wire the kernfs syscall callback shape (kobject *, const char *)
to the device-level walker. Add device_ktype_populate_one() and
device_ktype_populate_all() adapters that dispatch through
device_sysfs_apply() over dev->kobj.ktype->entries (currently
NULL; the migration commit installs driver_core_sysfs_entries[]).

The adapters early-return when dev->p->dead is set so a populate
racing with device_del() is a no-op.

No behaviour change: the table is empty until the migration commit.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: driver-core@lists.linux.dev
Cc: linux-api@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/core.c | 50 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 46 insertions(+), 4 deletions(-)

diff --git a/drivers/base/core.c b/drivers/base/core.c
index aeb4985eb8838..975b6e0c4dabd 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -33,6 +33,7 @@
 #include <linux/swiotlb.h>
 #include <linux/sysfs.h>
 
+
 #include "base.h"
 #include "physical_location.h"
 #include "power/power.h"
@@ -2841,10 +2842,9 @@ device_sysfs_entries_end(const struct device_sysfs_entry *entries)
 	return e;
 }
 
-static __maybe_unused int
-device_sysfs_apply(struct device *dev,
-		  const struct device_sysfs_entry *entries,
-		  enum dev_sysfs_action action, const char *name)
+static int device_sysfs_apply(struct device *dev,
+			     const struct device_sysfs_entry *entries,
+			     enum dev_sysfs_action action, const char *name)
 {
 	const struct device_sysfs_entry *e;
 
@@ -2972,11 +2972,53 @@ int device_set_sysfs_lazy(struct device *dev)
 }
 EXPORT_SYMBOL_GPL(device_set_sysfs_lazy);
 
+/*
+ * kobj_type populate adapters - thin wrappers from the kobject
+ * callback shape (kobject *, const char *) to the device-level
+ * walker. Both read dev->kobj.ktype->entries (which is currently
+ * NULL until driver_core_sysfs_entries[] is populated below) and
+ * call device_sysfs_apply() with the matching action.
+ *
+ * The walker traverses an empty table today, so these adapters are
+ * no-ops in behavioural terms. They are wired now so that the
+ * ktype plumbing lands as one reviewable unit; subsequent commits
+ * flip rows on without touching dispatch wiring.
+ */
+static int device_ktype_populate_one(struct kobject *kobj, const char *name)
+{
+	struct device *dev = kobj_to_dev(kobj);
+	const struct kobj_type *ktype = dev->kobj.ktype;
+	int ret;
+
+	/* Device is being torn down; do not populate. */
+	if (dev->p->dead)
+		return -ENOENT;
+
+	ret = device_sysfs_apply(dev, ktype ? ktype->entries : NULL,
+				DEV_SYSFS_ADD_ONE, name);
+	return ret;
+}
+
+static void device_ktype_populate_all(struct kobject *kobj)
+{
+	struct device *dev = kobj_to_dev(kobj);
+	const struct kobj_type *ktype = dev->kobj.ktype;
+
+	/* Device is being torn down; do not populate. */
+	if (dev->p->dead)
+		return;
+
+	device_sysfs_apply(dev, ktype ? ktype->entries : NULL,
+			  DEV_SYSFS_ADD_ALL, NULL);
+}
+
 static const struct kobj_type device_ktype = {
 	.release	= device_release,
 	.sysfs_ops	= &dev_sysfs_ops,
 	.namespace	= device_namespace,
 	.get_ownership	= device_get_ownership,
+	.populate	= device_ktype_populate_one,
+	.populate_all	= device_ktype_populate_all,
 };
 
 
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 07/14] driver core: migrate device sysfs to device_sysfs_entry table
  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   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 08/14] PCI/sysfs: migrate to device_sysfs_entry, defer physfn symlink Pavol Sakac
                     ` (6 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Migrate driver-core-owned per-device sysfs to the declarative table
introduced earlier in this series.  Every standalone file, synthetic
symlink, and attribute_group source visible at the top of /sys/<dev>/
becomes one row in driver_core_sysfs_entries[].  Row order matches
the eager device_add() readdir(3) order (ABI).  The walker runs
ADD_ALL forward in device_add() and REMOVE_ALL reverse in
device_del().

power/ stays eager.  dpm_sysfs_add() runs unconditionally from
device_add(), for lazy devices too.  In-kernel callers
(wakeup_sysfs_add(), dev_pm_qos_*()) sysfs_merge_group() into an
existing power/ directory at driver bind, before any userspace
access, so deferring power/ would break those merges.  device_add()
sets ->power_added on lazy devices; the power/ table row then no-ops
in create_power() (the latch is already set) and remove_power() is
its reverse-order teardown pair, calling dpm_sysfs_remove() only
when power/ was actually added.  Eager devices (->sysfs_lazy == NULL)
also get power/ at device_add() time, and create_power() returns
early for them.

device_del REMOVE_ALL.  device_del() and the SysEntryError unwind
path drop the imperative dpm_sysfs_remove() call and reverse the
table via REMOVE_ALL.  As a consequence the power/ subtree is now
torn down LAST (it is declared FIRST in the table and the walker
runs in reverse on REMOVE_ALL); the device is already dead
(dev->p->dead under device_lock) before the walker runs, so no
caller can observe a partially-torn-down state.

bus_add_device / bus_remove_device.  The bus's dev_groups and the
dev->kobj/subsystem symlink are now materialised by walker rows
(create_bus_groups, create_bus_subsystem_link).
bus_add_device() and bus_remove_device() keep only the bus-
directory back-link (sp->devices_kset->kobj/<devname>) which
targets a kobject other than dev->kobj and therefore cannot be a
per-device row.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: driver-core@lists.linux.dev
Cc: linux-api@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/bus.c     |  41 ++-
 drivers/base/core.c    | 708 +++++++++++++++++++++++++++++++----------
 fs/sysfs/sysfs.h       |   1 +
 include/linux/device.h |  50 +--
 4 files changed, 578 insertions(+), 222 deletions(-)

diff --git a/drivers/base/bus.c b/drivers/base/bus.c
index 8b6722ff8590d..4ca1bd0cf4904 100644
--- a/drivers/base/bus.c
+++ b/drivers/base/bus.c
@@ -563,34 +563,35 @@ int bus_add_device(struct device *dev)
 
 	pr_debug("bus: '%s': add device %s\n", sp->bus->name, dev_name(dev));
 
-	error = device_add_groups(dev, sp->bus->dev_groups);
+	error = sysfs_create_link(&sp->devices_kset->kobj, &dev->kobj, dev_name(dev));
 	if (error)
 		goto out_put;
 
 	if (dev->bus->driver_override) {
 		error = device_add_group(dev, &driver_override_dev_group);
 		if (error)
-			goto out_groups;
+			goto out_kset;
 	}
 
-	error = sysfs_create_link(&sp->devices_kset->kobj, &dev->kobj, dev_name(dev));
-	if (error)
-		goto out_override;
-
-	error = sysfs_create_link(&dev->kobj, &sp->subsys.kobj, "subsystem");
-	if (error)
-		goto out_subsys;
+	/*
+	 * The bus's dev_groups and the dev->kobj/subsystem symlink
+	 * are created by the driver-core walker through the
+	 * bus-groups wildcard row and the bus-bound "subsystem" row
+	 * of driver_core_sysfs_entries[] (see create_bus_groups()
+	 * and create_bus_subsystem_link() in drivers/base/core.c);
+	 * teardown runs through the REMOVE_ALL walker call in
+	 * device_del(). Only the bus-directory back-link
+	 * (sp->devices_kset->kobj -> dev->kobj, named after the
+	 * device) stays here - it targets a kobject other than
+	 * dev->kobj and therefore does not belong on the per-device
+	 * walker.
+	 */
 
 	klist_add_tail(&dev->p->knode_bus, &sp->klist_devices);
 	return 0;
 
-out_subsys:
+out_kset:
 	sysfs_remove_link(&sp->devices_kset->kobj, dev_name(dev));
-out_override:
-	if (dev->bus->driver_override)
-		device_remove_group(dev, &driver_override_dev_group);
-out_groups:
-	device_remove_groups(dev, sp->bus->dev_groups);
 out_put:
 	subsys_put(sp);
 	return error;
@@ -644,11 +645,17 @@ void bus_remove_device(struct device *dev)
 			sif->remove_dev(dev, sif);
 	mutex_unlock(&sp->mutex);
 
-	sysfs_remove_link(&dev->kobj, "subsystem");
+	/*
+	 * dev->kobj/subsystem and the bus's dev_groups are torn
+	 * down by the driver-core REMOVE_ALL walker call in
+	 * device_del() (reverse order over
+	 * driver_core_sysfs_entries[]); only the bus-directory
+	 * back-link sp->devices_kset->kobj/<devname> is removed
+	 * here because it targets a kobject other than dev->kobj.
+	 */
 	sysfs_remove_link(&sp->devices_kset->kobj, dev_name(dev));
 	if (dev->bus->driver_override)
 		device_remove_group(dev, &driver_override_dev_group);
-	device_remove_groups(dev, dev->bus->dev_groups);
 	if (klist_node_attached(&dev->p->knode_bus))
 		klist_del(&dev->p->knode_bus);
 
diff --git a/drivers/base/core.c b/drivers/base/core.c
index 975b6e0c4dabd..15a2dcf922d4b 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -2556,12 +2556,15 @@ static void device_release(struct kobject *kobj)
 	 */
 	devres_release_all(dev);
 
+	kfree(dev->dma_range_map);
+	kfree(dev->physical_location);
+	kfree(dev->driver_override.name);
+
+	/* Free per-device lazy state; kfree(NULL) handles never-opted-in. */
 	if (dev->sysfs_lazy)
 		mutex_destroy(&dev->sysfs_lazy->lock);
 	kfree(dev->sysfs_lazy);
-
-	kfree(dev->dma_range_map);
-	kfree(dev->driver_override.name);
+	dev->sysfs_lazy = NULL;
 
 	if (dev->release)
 		dev->release(dev);
@@ -2575,40 +2578,6 @@ static void device_release(struct kobject *kobj)
 	kfree(p);
 }
 
-/*
- * device_sysfs_apply() - declarative per-device sysfs dispatch
- *
- * Single walker over a sentinel-terminated struct device_sysfs_entry
- * table. Intended to be invoked on every dispatch path (device_add
- * eager, lazy populate_one, lazy populate_all, device_del teardown)
- * so adding a new per-device file is one row, not four open-coded
- * branches. Shape follows cftype + cgroup_addrm_files
- * (kernel/cgroup/cgroup.c) and pci_sysfs_entries[]
- * (drivers/pci/pci-sysfs.c).
- *
- * ADD_ONE:  first row whose applies_to passes and whose name matches
- *	     (or a wildcard row whose create() does not return
- *	     -ENOENT) terminates the walk. -ENOENT from a wildcard
- *	     row's create() signals "not my name"; the walker
- *	     continues. Realize callbacks MUST check existence (via
- *	     sysfs_*_exists()) before calling sysfs_create_*() and
- *	     return 0 if the entry already exists. Reaching
- *	     __kernfs_create_*() for a name that another lazy path
- *	     just created is forbidden under lock
- *	     serialization. If sysfs_warn_dup() ever fires from a
- *	     lazy path, it indicates a lock invariant
- *	     violation or a non-lazy path creating lazy attrs (bug).
- * ADD_ALL:  every applicable row's create() fires with name = NULL;
- *	     per-row errors are best-effort - the walker discards
- *	     return values and rows log via their own diagnostics.
- * REMOVE_ALL: reverse row order; remove() fires for every row whose
- *	     applies_to passes. Teardown never aborts.
- *
- * The full walker contract (error-absorption matrix, interaction
- * with negative-dentry caching, lifecycle caveats) is documented in
- * Documentation/driver-api/sysfs-lazy.rst, added later in this
- * series.
- */
 /*
  * Create one named attr inside @grp; honours grp->is_visible.
  * Returns 0 on success/hidden/already-present, -ENOENT on no-match.
@@ -2990,12 +2959,32 @@ static int device_ktype_populate_one(struct kobject *kobj, const char *name)
 	const struct kobj_type *ktype = dev->kobj.ktype;
 	int ret;
 
-	/* Device is being torn down; do not populate. */
-	if (dev->p->dead)
+	/* KERNFS_LAZY implies dev->sysfs_lazy was allocated by device_set_sysfs_lazy(). */
+	if (WARN_ON_ONCE(!dev->sysfs_lazy))
+		return -ENOENT;
+
+	/* Fast path: directory fully walked, kernfs has authoritative state. */
+	if (device_sysfs_populated(dev))
 		return -ENOENT;
 
+	mutex_lock(&dev->sysfs_lazy->lock);
+	/* Re-check under the lock against a concurrent populate_all. */
+	if (device_sysfs_populated(dev)) {
+		ret = -ENOENT;
+		goto out;
+	}
+	/*
+	 * FIXME: dev->p->dead is device_lock-protected and a bitfield (so not
+	 * READ_ONCE()-able); this lockless re-check is a benign KCSAN data race.
+	 */
+	if (dev->p->dead) {
+		ret = -ENOENT;
+		goto out;
+	}
 	ret = device_sysfs_apply(dev, ktype ? ktype->entries : NULL,
 				DEV_SYSFS_ADD_ONE, name);
+out:
+	mutex_unlock(&dev->sysfs_lazy->lock);
 	return ret;
 }
 
@@ -3004,14 +2993,33 @@ static void device_ktype_populate_all(struct kobject *kobj)
 	struct device *dev = kobj_to_dev(kobj);
 	const struct kobj_type *ktype = dev->kobj.ktype;
 
-	/* Device is being torn down; do not populate. */
-	if (dev->p->dead)
+	/* See device_ktype_populate_one() for the invariant. */
+	if (WARN_ON_ONCE(!dev->sysfs_lazy))
+		return;
+
+	/* Fast path: directory already fully populated. */
+	if (device_sysfs_populated(dev))
 		return;
 
+	mutex_lock(&dev->sysfs_lazy->lock);
+	/* Re-check under the lock. */
+	if (device_sysfs_populated(dev))
+		goto out;
+	/* FIXME: same racy dead read; see device_ktype_populate_one(). */
+	if (dev->p->dead)
+		goto out;
+
 	device_sysfs_apply(dev, ktype ? ktype->entries : NULL,
 			  DEV_SYSFS_ADD_ALL, NULL);
+
+
+	device_sysfs_set_populated(dev);
+out:
+	mutex_unlock(&dev->sysfs_lazy->lock);
 }
 
+static const struct device_sysfs_entry driver_core_sysfs_entries[];
+
 static const struct kobj_type device_ktype = {
 	.release	= device_release,
 	.sysfs_ops	= &dev_sysfs_ops,
@@ -3019,6 +3027,7 @@ static const struct kobj_type device_ktype = {
 	.get_ownership	= device_get_ownership,
 	.populate	= device_ktype_populate_one,
 	.populate_all	= device_ktype_populate_all,
+	.entries	= driver_core_sysfs_entries,
 };
 
 
@@ -3271,6 +3280,25 @@ void device_remove_groups(struct device *dev,
 }
 EXPORT_SYMBOL_GPL(device_remove_groups);
 
+/* Like device_remove_groups() but skips uncreated named subdirs (lazy). */
+static void device_remove_groups_if_present(struct device *dev,
+				      const struct attribute_group *const *groups)
+{
+	const struct attribute_group *const *g;
+
+	if (!groups)
+		return;
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	for (g = groups; *g; g++) {
+		if ((*g)->name && !sysfs_group_exists(&dev->kobj, *g))
+			continue;
+		sysfs_remove_group(&dev->kobj, *g);
+	}
+}
+
 union device_attr_group_devres {
 	const struct attribute_group *group;
 	const struct attribute_group **groups;
@@ -3317,101 +3345,447 @@ int devm_device_add_group(struct device *dev, const struct attribute_group *grp)
 }
 EXPORT_SYMBOL_GPL(devm_device_add_group);
 
-static int device_add_attrs(struct device *dev)
+static ssize_t dev_show(struct device *dev, struct device_attribute *attr,
+			char *buf)
 {
-	const struct class *class = dev->class;
-	const struct device_type *type = dev->type;
-	int error;
+	return print_dev_t(buf, dev->devt);
+}
+static DEVICE_ATTR_RO(dev);
 
-	if (class) {
-		error = device_add_groups(dev, class->dev_groups);
-		if (error)
-			return error;
-	}
+/* Per-device sysfs catalogue (creation order; reversed on teardown). */
+#define N_DRIVER_CORE_SYSFS_ENTRIES	14
 
-	if (type) {
-		error = device_add_groups(dev, type->groups);
-		if (error)
-			goto err_remove_class_groups;
-	}
+/* applies_to() predicates: cheap, non-sleeping, lock-free. */
+static bool dev_supports_offline_enabled(struct device *dev)
+{
+	return device_supports_offline(dev) && !dev->offline_disabled;
+}
 
-	error = device_add_groups(dev, dev->groups);
-	if (error)
-		goto err_remove_type_groups;
+static bool dev_has_fwnode_devlink(struct device *dev)
+{
+	return fw_devlink_flags && !fw_devlink_is_permissive() &&
+	       dev->fwnode;
+}
 
-	if (device_supports_offline(dev) && !dev->offline_disabled) {
-		error = device_create_file(dev, &dev_attr_online);
-		if (error)
-			goto err_remove_dev_groups;
-	}
+static bool dev_removable_valid(struct device *dev)
+{
+	return dev_removable_is_valid(dev);
+}
 
-	if (fw_devlink_flags && !fw_devlink_is_permissive() && dev->fwnode) {
-		error = device_create_file(dev, &dev_attr_waiting_for_supplier);
-		if (error)
-			goto err_remove_dev_online;
-	}
+static bool dev_has_physical_location(struct device *dev)
+{
+	return !!dev->physical_location;
+}
 
-	if (dev_removable_is_valid(dev)) {
-		error = device_create_file(dev, &dev_attr_removable);
-		if (error)
-			goto err_remove_dev_waiting_for_supplier;
-	}
+static bool dev_has_devt_major(struct device *dev)
+{
+	return MAJOR(dev->devt) != 0;
+}
 
-	if (dev_add_physical_location(dev)) {
-		error = device_add_group(dev,
-			&dev_attr_physical_location_group);
-		if (error)
-			goto err_remove_dev_removable;
-	}
+static bool dev_has_class(struct device *dev)
+{
+	return !!dev->class;
+}
 
-	return 0;
+static bool dev_has_bus_no_class(struct device *dev)
+{
+	return !dev->class && dev->bus;
+}
 
- err_remove_dev_removable:
-	device_remove_file(dev, &dev_attr_removable);
- err_remove_dev_waiting_for_supplier:
-	device_remove_file(dev, &dev_attr_waiting_for_supplier);
- err_remove_dev_online:
+static bool dev_is_class_child(struct device *dev)
+{
+	return dev->class && dev->parent && device_is_not_partition(dev);
+}
+
+static bool dev_has_class_dev_groups(struct device *dev)
+{
+	return dev->class && dev->class->dev_groups;
+}
+
+static bool dev_has_type_groups(struct device *dev)
+{
+	return dev->type && dev->type->groups;
+}
+
+static bool dev_has_own_groups(struct device *dev)
+{
+	return !!dev->groups;
+}
+
+static bool dev_has_bus_dev_groups(struct device *dev)
+{
+	return dev->bus && dev->bus->dev_groups;
+}
+
+static bool dev_needs_pm(struct device *dev)
+{
+	return !device_pm_not_required(dev);
+}
+
+/* Per-row create()/remove() callbacks. */
+static int create_power(struct device *dev, const char *name)
+{
+	int ret;
+
+	/* dpm_sysfs_add() already ran from device_add() on the eager path. */
+	if (!device_is_sysfs_lazy(dev))
+		return 0;
+
+	lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (dev->sysfs_lazy->power_added)
+		return 0;
+
+	/*
+	 * FIXME: unreachable today - device_add() adds power/ eagerly and latches
+	 * power_added for lazy devices too, so the power_added check above wins.
+	 */
+	ret = dpm_sysfs_add(dev);
+	if (!ret)
+		dev->sysfs_lazy->power_added = true;
+	return ret;
+}
+
+static void remove_power(struct device *dev)
+{
+	/* Skip dpm_sysfs_remove() on lazy devices that never materialised power/. */
+	if (device_is_sysfs_lazy(dev) && !dev->sysfs_lazy->power_added)
+		return;
+	dpm_sysfs_remove(dev);
+}
+
+static int create_class_subsystem_link(struct device *dev, const char *name)
+{
+	struct subsys_private *sp;
+	int ret;
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, "subsystem"))
+		return 0;
+
+	sp = class_to_subsys(dev->class);
+	if (!sp)
+		return 0;
+
+	ret = sysfs_create_link(&dev->kobj, &sp->subsys.kobj, "subsystem");
+	subsys_put(sp);
+	return ret;
+}
+
+static void remove_class_subsystem_link(struct device *dev)
+{
+	sysfs_remove_link(&dev->kobj, "subsystem");
+}
+
+static int create_bus_subsystem_link(struct device *dev, const char *name)
+{
+	struct subsys_private *sp;
+	int ret;
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, "subsystem"))
+		return 0;
+
+	sp = bus_to_subsys(dev->bus);
+	if (!sp)
+		return 0;
+
+	ret = sysfs_create_link(&dev->kobj, &sp->subsys.kobj, "subsystem");
+	subsys_put(sp);
+	return ret;
+}
+
+static void remove_bus_subsystem_link(struct device *dev)
+{
+	sysfs_remove_link(&dev->kobj, "subsystem");
+}
+
+static int create_device_backlink(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, "device"))
+		return 0;
+
+	return sysfs_create_link(&dev->kobj, &dev->parent->kobj, "device");
+}
+
+static void remove_device_backlink(struct device *dev)
+{
+	sysfs_remove_link(&dev->kobj, "device");
+}
+
+static int create_uevent(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, dev_attr_uevent.attr.name))
+		return 0;
+
+	return sysfs_create_file_ns(&dev->kobj, &dev_attr_uevent.attr, NULL);
+}
+
+static void remove_uevent(struct device *dev)
+{
+	device_remove_file(dev, &dev_attr_uevent);
+}
+
+static int create_online(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, dev_attr_online.attr.name))
+		return 0;
+
+	return sysfs_create_file_ns(&dev->kobj, &dev_attr_online.attr, NULL);
+}
+
+static void remove_online(struct device *dev)
+{
 	device_remove_file(dev, &dev_attr_online);
- err_remove_dev_groups:
-	device_remove_groups(dev, dev->groups);
- err_remove_type_groups:
-	if (type)
-		device_remove_groups(dev, type->groups);
- err_remove_class_groups:
-	if (class)
-		device_remove_groups(dev, class->dev_groups);
+}
 
-	return error;
+static int create_waiting_for_supplier(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, dev_attr_waiting_for_supplier.attr.name))
+		return 0;
+
+	return sysfs_create_file_ns(&dev->kobj,
+				    &dev_attr_waiting_for_supplier.attr,
+				    NULL);
 }
 
-static void device_remove_attrs(struct device *dev)
+static void remove_waiting_for_supplier(struct device *dev)
 {
-	const struct class *class = dev->class;
-	const struct device_type *type = dev->type;
+	device_remove_file(dev, &dev_attr_waiting_for_supplier);
+}
 
-	if (dev->physical_location) {
-		device_remove_group(dev, &dev_attr_physical_location_group);
-		kfree(dev->physical_location);
-	}
+static int create_removable(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
 
+	if (sysfs_kn_exists(&dev->kobj, dev_attr_removable.attr.name))
+		return 0;
+
+	return sysfs_create_file_ns(&dev->kobj, &dev_attr_removable.attr, NULL);
+}
+
+static void remove_removable(struct device *dev)
+{
 	device_remove_file(dev, &dev_attr_removable);
-	device_remove_file(dev, &dev_attr_waiting_for_supplier);
-	device_remove_file(dev, &dev_attr_online);
-	device_remove_groups(dev, dev->groups);
+}
 
-	if (type)
-		device_remove_groups(dev, type->groups);
+static int create_physical_location(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	/* NULL on non-ACPI nodes; safe under kobj reference. */
+	if (!dev->physical_location)
+		return -ENOENT;
 
-	if (class)
-		device_remove_groups(dev, class->dev_groups);
+	if (sysfs_group_exists(&dev->kobj, &dev_attr_physical_location_group))
+		return 0;
+
+	return sysfs_create_group(&dev->kobj, &dev_attr_physical_location_group);
 }
 
-static ssize_t dev_show(struct device *dev, struct device_attribute *attr,
-			char *buf)
+static void remove_physical_location(struct device *dev)
 {
-	return print_dev_t(buf, dev->devt);
+	/* dev->physical_location is freed in device_release(), not here. */
+	device_remove_group(dev, &dev_attr_physical_location_group);
+}
+
+static int create_dev_attr(struct device *dev, const char *name)
+{
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, dev_attr_dev.attr.name))
+		return 0;
+
+	return sysfs_create_file_ns(&dev->kobj, &dev_attr_dev.attr, NULL);
+}
+
+static void remove_dev_attr(struct device *dev)
+{
+	device_remove_file(dev, &dev_attr_dev);
+}
+
+/* Wildcard rows for the four attribute_group sources. */
+static int create_class_groups(struct device *dev, const char *name)
+{
+	if (!dev->class)
+		return -ENOENT;
+	return name ? create_attr_in_groups(dev, dev->class->dev_groups, name)
+		    : create_all_in_groups(dev, dev->class->dev_groups);
+}
+
+static void remove_class_groups(struct device *dev)
+{
+	if (!dev->class)
+		return;
+	if (device_is_sysfs_lazy(dev))
+		device_remove_groups_if_present(dev, dev->class->dev_groups);
+	else
+		device_remove_groups(dev, dev->class->dev_groups);
+}
+
+static int create_type_groups(struct device *dev, const char *name)
+{
+	if (!dev->type)
+		return -ENOENT;
+	return name ? create_attr_in_groups(dev, dev->type->groups, name)
+		    : create_all_in_groups(dev, dev->type->groups);
+}
+
+static void remove_type_groups(struct device *dev)
+{
+	if (!dev->type)
+		return;
+	if (device_is_sysfs_lazy(dev))
+		device_remove_groups_if_present(dev, dev->type->groups);
+	else
+		device_remove_groups(dev, dev->type->groups);
+}
+
+static int create_dev_own_groups(struct device *dev, const char *name)
+{
+	return name ? create_attr_in_groups(dev, dev->groups, name)
+		    : create_all_in_groups(dev, dev->groups);
+}
+
+static void remove_dev_own_groups(struct device *dev)
+{
+	if (device_is_sysfs_lazy(dev))
+		device_remove_groups_if_present(dev, dev->groups);
+	else
+		device_remove_groups(dev, dev->groups);
+}
+
+static int create_bus_groups(struct device *dev, const char *name)
+{
+	if (!dev->bus)
+		return -ENOENT;
+	return name ? create_attr_in_groups(dev, dev->bus->dev_groups, name)
+		    : create_all_in_groups(dev, dev->bus->dev_groups);
 }
-static DEVICE_ATTR_RO(dev);
+
+static void remove_bus_groups(struct device *dev)
+{
+	if (!dev->bus)
+		return;
+	if (device_is_sysfs_lazy(dev))
+		device_remove_groups_if_present(dev, dev->bus->dev_groups);
+	else
+		device_remove_groups(dev, dev->bus->dev_groups);
+}
+
+static const struct device_sysfs_entry driver_core_sysfs_entries[] = {
+	/*
+	 * Rows are in eager device_add() creation order: ADD_ALL creates
+	 * forward, REMOVE_ALL tears down in reverse, matching the eager
+	 * path. The lazy tree exposes the same set of names and file
+	 * contents as an eager device; readdir(3) order is kernfs name-hash
+	 * order (kernfs_sd_compare), not table order, and is not ABI.
+	 */
+	{
+		/* Created early on the eager path - keep at table top. */
+		.name		= "power",
+		.applies_to	= dev_needs_pm,
+		.create	= create_power,
+		.remove		= remove_power,
+	},
+	{
+		.name		= "uevent",
+		.create	= create_uevent,
+		.remove		= remove_uevent,
+	},
+	/* Class-side rows. */
+	{
+		.name		= "subsystem",
+		.applies_to	= dev_has_class,
+		.create	= create_class_subsystem_link,
+		.remove		= remove_class_subsystem_link,
+	},
+	{
+		.name		= "device",
+		.applies_to	= dev_is_class_child,
+		.create	= create_device_backlink,
+		.remove		= remove_device_backlink,
+	},
+	{
+		.applies_to	= dev_has_class_dev_groups,
+		.create	= create_class_groups,
+		.remove		= remove_class_groups,
+	},
+	/* Groups + standalones in eager device_attrs_create() order. */
+	{
+		.applies_to	= dev_has_type_groups,
+		.create	= create_type_groups,
+		.remove		= remove_type_groups,
+	},
+	{
+		.applies_to	= dev_has_own_groups,
+		.create	= create_dev_own_groups,
+		.remove		= remove_dev_own_groups,
+	},
+	{
+		.name		= "online",
+		.applies_to	= dev_supports_offline_enabled,
+		.create	= create_online,
+		.remove		= remove_online,
+	},
+	{
+		.name		= "waiting_for_supplier",
+		.applies_to	= dev_has_fwnode_devlink,
+		.create	= create_waiting_for_supplier,
+		.remove		= remove_waiting_for_supplier,
+	},
+	{
+		.name		= "removable",
+		.applies_to	= dev_removable_valid,
+		.create	= create_removable,
+		.remove		= remove_removable,
+	},
+	{
+		.name		= "physical_location",
+		.applies_to	= dev_has_physical_location,
+		.create	= create_physical_location,
+		.remove		= remove_physical_location,
+	},
+	/* Bus-side rows. */
+	{
+		.name		= "subsystem",
+		.applies_to	= dev_has_bus_no_class,
+		.create	= create_bus_subsystem_link,
+		.remove		= remove_bus_subsystem_link,
+	},
+	{
+		.applies_to	= dev_has_bus_dev_groups,
+		.create	= create_bus_groups,
+		.remove		= remove_bus_groups,
+	},
+	/* Late rows. */
+	{
+		.name		= "dev",
+		.applies_to	= dev_has_devt_major,
+		.create	= create_dev_attr,
+		.remove		= remove_dev_attr,
+	},
+	{ }	/* sentinel */
+};
 
 /* /sys/devices/ */
 struct kset *devices_kset;
@@ -3861,30 +4235,17 @@ static int device_add_class_symlinks(struct device *dev)
 	if (!sp)
 		return 0;
 
-	error = sysfs_create_link(&dev->kobj, &sp->subsys.kobj, "subsystem");
-	if (error)
-		goto out_devnode;
-
-	if (dev->parent && device_is_not_partition(dev)) {
-		error = sysfs_create_link(&dev->kobj, &dev->parent->kobj,
-					  "device");
-		if (error)
-			goto out_subsys;
-	}
 
 	/* link in the class directory pointing to the device */
 	error = sysfs_create_link(&sp->subsys.kobj, &dev->kobj, dev_name(dev));
 	if (error)
-		goto out_device;
-	goto exit;
+		goto out_devnode;
+
+	subsys_put(sp);
+	return 0;
 
-out_device:
-	sysfs_remove_link(&dev->kobj, "device");
-out_subsys:
-	sysfs_remove_link(&dev->kobj, "subsystem");
 out_devnode:
 	sysfs_remove_link(&dev->kobj, "of_node");
-exit:
 	subsys_put(sp);
 	return error;
 }
@@ -3899,9 +4260,6 @@ static void device_remove_class_symlinks(struct device *dev)
 	if (!sp)
 		return;
 
-	if (dev->parent && device_is_not_partition(dev))
-		sysfs_remove_link(&dev->kobj, "device");
-	sysfs_remove_link(&dev->kobj, "subsystem");
 	sysfs_delete_link(&sp->subsys.kobj, &dev->kobj, dev_name(dev));
 	subsys_put(sp);
 }
@@ -4061,38 +4419,44 @@ int device_add(struct device *dev)
 	/* notify platform of device entry */
 	device_platform_notify(dev);
 
-	error = device_create_file(dev, &dev_attr_uevent);
-	if (error)
-		goto attrError;
-
 	error = device_add_class_symlinks(dev);
 	if (error)
 		goto SymlinkError;
-	/*
-	 * device_set_sysfs_lazy() only allocates ->sysfs_lazy_state for
-	 * non-namespaced devices, and device_is_sysfs_lazy() guards opt-in
-	 * here, so kernfs_set_lazy() should always succeed.  WARN_ON catches
-	 * a regression in those preconditions.
-	 */
-	if (device_is_sysfs_lazy(dev))
-		WARN_ON(kernfs_set_lazy(dev->kobj.sd));
-
-	error = device_add_attrs(dev);
-	if (error)
-		goto AttrsError;
 	error = bus_add_device(dev);
 	if (error)
 		goto BusError;
+	/*
+	 * Create power/ eagerly even for lazy devices: in-kernel callers
+	 * (wakeup_sysfs_add(), dev_pm_qos_*()) sysfs_merge_group() into an
+	 * existing power/ dir at driver-bind, before any userspace access.
+	 * Mark it added so the lazy create/remove path treats it as done.
+	 */
 	error = dpm_sysfs_add(dev);
 	if (error)
 		goto DPMError;
+	if (device_is_sysfs_lazy(dev))
+		dev->sysfs_lazy->power_added = true;
 	device_pm_add(dev);
 
-	if (MAJOR(dev->devt)) {
-		error = device_create_file(dev, &dev_attr_dev);
-		if (error)
-			goto DevAttrError;
+	(void)dev_add_physical_location(dev);
 
+	/*
+	 * Subsystems that opt a device into lazy sysfs are responsible for
+	 * ensuring the device's kobj.sd is a non-namespaced directory: the
+	 * device_set_sysfs_lazy() contract states the call must precede
+	 * device_add(), and device_is_sysfs_lazy() gates opt-in here.
+	 * kernfs_set_lazy() should therefore always succeed; WARN_ON catches
+	 * a contract violation by an opted-in subsystem.
+	 */
+	if (device_is_sysfs_lazy(dev))
+		WARN_ON(kernfs_set_lazy(dev->kobj.sd));
+
+	/* FIXME: eager ADD_ALL return is dropped; a failed create won't fail device_add(). */
+	if (!device_is_sysfs_lazy(dev))
+		device_sysfs_apply(dev, driver_core_sysfs_entries,
+				  DEV_SYSFS_ADD_ALL, NULL);
+
+	if (MAJOR(dev->devt)) {
 		error = device_create_sys_dev_entry(dev);
 		if (error)
 			goto SysEntryError;
@@ -4169,21 +4533,23 @@ int device_add(struct device *dev)
 	put_device(dev);
 	return error;
  SysEntryError:
-	if (MAJOR(dev->devt))
-		device_remove_file(dev, &dev_attr_dev);
- DevAttrError:
+	device_lock(dev);
+	kill_device(dev);
+	device_unlock(dev);
+	/* REMOVE_ALL: the remove_power() row is the sole power-group teardown. */
+	if (device_is_sysfs_lazy(dev))
+		mutex_lock(&dev->sysfs_lazy->lock);
+	device_sysfs_apply(dev, driver_core_sysfs_entries,
+			  DEV_SYSFS_REMOVE_ALL, NULL);
+	if (device_is_sysfs_lazy(dev))
+		mutex_unlock(&dev->sysfs_lazy->lock);
 	device_pm_remove(dev);
-	dpm_sysfs_remove(dev);
  DPMError:
 	device_set_driver(dev, NULL);
 	bus_remove_device(dev);
  BusError:
-	device_remove_attrs(dev);
- AttrsError:
 	device_remove_class_symlinks(dev);
  SymlinkError:
-	device_remove_file(dev, &dev_attr_uevent);
- attrError:
 	device_platform_notify_remove(dev);
 	kobject_uevent(&dev->kobj, KOBJ_REMOVE);
 	glue_dir = get_glue_dir(dev);
@@ -4296,19 +4662,25 @@ void device_del(struct device *dev)
 	if (dev->fwnode && dev->fwnode->dev == dev)
 		dev->fwnode->dev = NULL;
 
-	/* Notify clients of device removal.  This call must come
-	 * before dpm_sysfs_remove().
-	 */
+	/* Must come before REMOVE_ALL tears down power/. */
 	noio_flag = memalloc_noio_save();
 	bus_notify(dev, BUS_NOTIFY_DEL_DEVICE);
 
-	dpm_sysfs_remove(dev);
+	/* Serialize against lazy populate callbacks. dev->p->dead is
+	 * already set so any populate taking this lock after us bails.
+	 */
+	if (device_is_sysfs_lazy(dev))
+		mutex_lock(&dev->sysfs_lazy->lock);
+	device_sysfs_apply(dev, driver_core_sysfs_entries,
+			  DEV_SYSFS_REMOVE_ALL, NULL);
+	if (device_is_sysfs_lazy(dev))
+		mutex_unlock(&dev->sysfs_lazy->lock);
+
 	if (parent)
 		klist_del(&dev->p->knode_parent);
 	if (MAJOR(dev->devt)) {
 		devtmpfs_delete_node(dev);
 		device_remove_sys_dev_entry(dev);
-		device_remove_file(dev, &dev_attr_dev);
 	}
 
 	sp = class_to_subsys(dev->class);
@@ -4325,8 +4697,6 @@ void device_del(struct device *dev)
 		mutex_unlock(&sp->mutex);
 		subsys_put(sp);
 	}
-	device_remove_file(dev, &dev_attr_uevent);
-	device_remove_attrs(dev);
 	bus_remove_device(dev);
 	device_pm_remove(dev);
 	driver_deferred_probe_del(dev);
@@ -4578,6 +4948,10 @@ EXPORT_SYMBOL_GPL(device_find_child);
 
 int __init devices_init(void)
 {
+	/* Compile-time row-count check (rows + sentinel). */
+	BUILD_BUG_ON(ARRAY_SIZE(driver_core_sysfs_entries) !=
+		     N_DRIVER_CORE_SYSFS_ENTRIES + 1);
+
 	devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
 	if (!devices_kset)
 		return -ENOMEM;
diff --git a/fs/sysfs/sysfs.h b/fs/sysfs/sysfs.h
index 94b8aca20bffc..e2ce9e6c12d84 100644
--- a/fs/sysfs/sysfs.h
+++ b/fs/sysfs/sysfs.h
@@ -28,6 +28,7 @@ void sysfs_warn_dup(struct kernfs_node *parent, const char *name);
  * file.c
  */
 
+
 /*
  * symlink.c
  */
diff --git a/include/linux/device.h b/include/linux/device.h
index e5485bfbc6c84..c0a2ee429280c 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -89,16 +89,10 @@ int subsys_virtual_register(const struct bus_type *subsys,
 			    const struct attribute_group **groups);
 
 /**
- * enum dev_sysfs_action - verb selector for device_sysfs_apply()
- * @DEV_SYSFS_ADD_ONE: realize a single named entry (lazy populate_one
- *		       miss path); walker stops at the first row whose
- *		       @applies_to passes and whose create() returns
- *		       anything other than -ENOENT.
- * @DEV_SYSFS_ADD_ALL: realize every applicable entry (eager device_add
- *		       or lazy populate_all); best-effort, per-row errors
- *		       do not abort the walk.
- * @DEV_SYSFS_REMOVE_ALL: tear down every applicable entry in reverse
- *		       row order (device_del teardown).
+ * enum dev_sysfs_action - operation requested by device_sysfs_apply()
+ * @DEV_SYSFS_ADD_ONE: create a single named entry
+ * @DEV_SYSFS_ADD_ALL: create every applicable entry
+ * @DEV_SYSFS_REMOVE_ALL: tear down every previously-created entry
  */
 enum dev_sysfs_action {
 	DEV_SYSFS_ADD_ONE,
@@ -107,35 +101,15 @@ enum dev_sysfs_action {
 };
 
 /**
- * struct device_sysfs_entry - declarative per-device sysfs content row
- * @name:	Attribute or symlink name at the top level of the device's
- *		sysfs directory, or %NULL for a wildcard row whose
- *		create() performs internal name dispatch (typically a
- *		group-source row iterating an attribute_group array).
- * @applies_to: Optional predicate gating row eligibility for this
- *		device. Must be cheap, non-sleeping, side-effect-free, and
- *		monotonic for a given device state. MUST NOT acquire any
- *		lock the walker's caller may already hold (notably
- *		device_lock()). %NULL means unconditional.
- * @create:	Required. Creates the row's sysfs content. For a named
- *		row (@name != NULL) the @name argument equals @e->name on
- *		ADD_ONE and %NULL on ADD_ALL. For a wildcard row the @name
- *		argument is the ADD_ONE target (or %NULL on ADD_ALL) and
- *		the row's create() matches internally, returning -ENOENT
- *		to signal "not my name" so the walker continues. Must
- *		convert -EEXIST from concurrent racers to 0.
- * @remove:	Optional reverse-of-realize teardown. Called on
- *		REMOVE_ALL in reverse row order. MUST NOT fail the walk.
+ * struct device_sysfs_entry - per-device sysfs entry
+ * @name:	entry name; %NULL for a wildcard row
+ * @applies_to:	optional predicate gating row eligibility
+ * @create:	creates the row's sysfs content
+ * @remove:	optional teardown
  *
- * Rows are declared in file-static, sentinel-terminated tables whose
- * base pointer is published to the walker via struct kobj_type.entries
- * (device ktype shares a single driver_core table and dispatches to
- * dev->type->entries as well). The row shape mirrors cftype +
- * cgroup_addrm_files (kernel/cgroup/cgroup.c) and the
- * pci_sysfs_entries[] at drivers/pci/pci-sysfs.c (added later in
- * the series); see Documentation/driver-api/sysfs-lazy.rst (also
- * added later in the series) for the full walker contract and
- * error-handling matrix.
+ * Rows are declared in file-static, sentinel-terminated tables and
+ * walked by device_sysfs_apply().  See
+ * Documentation/driver-api/sysfs-lazy.rst for the walker contract.
  */
 struct device_sysfs_entry {
 	const char *name;
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 08/14] PCI/sysfs: migrate to device_sysfs_entry, defer physfn symlink
  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   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 09/14] iommu: lazy-populate iommu_group reserved_regions/type attrs Pavol Sakac
                     ` (5 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Migrate PCI's per-device sysfs to the declarative walker, and as
part of that wire the bus/type dispatch chain via two new struct
device_type hooks.

  - struct device_type grows .populate / .populate_all hooks
    consulted by device_ktype_populate_one /
    device_ktype_populate_all when the driver-core walker returns
    -ENOENT.

  - PCI's bus/type-owned per-device sysfs content (resource%u /
    resource%u_wc files plus the physfn back-symlink) becomes
    pci_sysfs_entries[] and is dispatched by the same walker.

  - Lazy populate skips eager pci_create_sysfs_dev_files() so VFs
    materialise their resource files only on first userspace
    access.

The physfn symlink moves from per-device add to the wildcard row
on the VF; eager devices still see no change. Idempotent
re-creation is required by the VF lookup-then-readdir VFS order;
sysfs_kn_exists() provides the existence check.

No userspace ABI change.

Cc: Bjorn Helgaas <bhelgaas@google.com>
Cc: linux-pci@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: driver-core@lists.linux.dev
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: linux-api@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/core.c     |  10 ++-
 drivers/pci/iov.c       |   5 --
 drivers/pci/pci-sysfs.c | 149 ++++++++++++++++++++++++++++++++++++----
 drivers/pci/pci.h       |   5 ++
 drivers/pci/probe.c     |   4 +-
 include/linux/device.h  |  32 ++++++---
 6 files changed, 175 insertions(+), 30 deletions(-)

diff --git a/drivers/base/core.c b/drivers/base/core.c
index 15a2dcf922d4b..6c5d1bb66c8f4 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -2811,9 +2811,9 @@ device_sysfs_entries_end(const struct device_sysfs_entry *entries)
 	return e;
 }
 
-static int device_sysfs_apply(struct device *dev,
-			     const struct device_sysfs_entry *entries,
-			     enum dev_sysfs_action action, const char *name)
+int device_sysfs_apply(struct device *dev,
+		      const struct device_sysfs_entry *entries,
+		      enum dev_sysfs_action action, const char *name)
 {
 	const struct device_sysfs_entry *e;
 
@@ -2983,6 +2983,8 @@ static int device_ktype_populate_one(struct kobject *kobj, const char *name)
 	}
 	ret = device_sysfs_apply(dev, ktype ? ktype->entries : NULL,
 				DEV_SYSFS_ADD_ONE, name);
+	if (ret == -ENOENT && dev->type && dev->type->populate)
+		ret = dev->type->populate(dev, name);
 out:
 	mutex_unlock(&dev->sysfs_lazy->lock);
 	return ret;
@@ -3011,6 +3013,8 @@ static void device_ktype_populate_all(struct kobject *kobj)
 
 	device_sysfs_apply(dev, ktype ? ktype->entries : NULL,
 			  DEV_SYSFS_ADD_ALL, NULL);
+	if (dev->type && dev->type->populate_all)
+		dev->type->populate_all(dev);
 
 
 	device_sysfs_set_populated(dev);
diff --git a/drivers/pci/iov.c b/drivers/pci/iov.c
index 91ac4e37ecb9c..ec2fca8209593 100644
--- a/drivers/pci/iov.c
+++ b/drivers/pci/iov.c
@@ -212,16 +212,11 @@ int pci_iov_sysfs_link(struct pci_dev *dev,
 	rc = sysfs_create_link(&dev->dev.kobj, &virtfn->dev.kobj, buf);
 	if (rc)
 		goto failed;
-	rc = sysfs_create_link(&virtfn->dev.kobj, &dev->dev.kobj, "physfn");
-	if (rc)
-		goto failed1;
 
 	kobject_uevent(&virtfn->dev.kobj, KOBJ_CHANGE);
 
 	return 0;
 
-failed1:
-	sysfs_remove_link(&dev->dev.kobj, buf);
 failed:
 	return rc;
 }
diff --git a/drivers/pci/pci-sysfs.c b/drivers/pci/pci-sysfs.c
index d37860841260c..5a779ebaee326 100644
--- a/drivers/pci/pci-sysfs.c
+++ b/drivers/pci/pci-sysfs.c
@@ -1247,6 +1247,13 @@ static int pci_create_attr(struct pci_dev *pdev, int num, int write_combine)
 	res_attr->attr.mode = 0600;
 	res_attr->size = pci_resource_len(pdev, num);
 	res_attr->private = (void *)(unsigned long)num;
+
+	/* Idempotent: skip if a concurrent populate already created this slot. */
+	if (sysfs_kn_exists(&pdev->dev.kobj, res_attr->attr.name)) {
+		kfree(res_attr);
+		return 0;
+	}
+
 	retval = sysfs_create_bin_file(&pdev->dev.kobj, res_attr);
 	if (retval) {
 		kfree(res_attr);
@@ -1264,8 +1271,6 @@ static int pci_create_attr(struct pci_dev *pdev, int num, int write_combine)
 /**
  * pci_create_resource_files - create resource files in sysfs for @dev
  * @pdev: dev in question
- *
- * Walk the resources in @pdev creating files for each resource available.
  */
 static int pci_create_resource_files(struct pci_dev *pdev)
 {
@@ -1283,21 +1288,65 @@ static int pci_create_resource_files(struct pci_dev *pdev)
 		if (!pci_resource_len(pdev, i))
 			continue;
 
-		retval = pci_create_attr(pdev, i, 0);
+		/* Skip slots already materialised by populate_one(). */
+		if (!pdev->res_attr[i]) {
+			retval = pci_create_attr(pdev, i, 0);
+			if (retval) {
+				pci_remove_resource_files(pdev);
+				return retval;
+			}
+		}
 		/* for prefetchable resources, create a WC mappable file */
-		if (!retval && arch_can_pci_mmap_wc() &&
-		    pdev->resource[i].flags & IORESOURCE_PREFETCH)
+		if (arch_can_pci_mmap_wc() &&
+		    pdev->resource[i].flags & IORESOURCE_PREFETCH &&
+		    !pdev->res_attr_wc[i]) {
 			retval = pci_create_attr(pdev, i, 1);
-		if (retval) {
-			pci_remove_resource_files(pdev);
-			return retval;
+			if (retval) {
+				pci_remove_resource_files(pdev);
+				return retval;
+			}
 		}
 	}
 	return 0;
 }
+
+/*
+ * Resolve "resourceN" / "resourceN_wc" and create the single file.
+ * Returns -ENOENT for unrelated names or out-of-range/empty BARs so
+ * the walker continues to the next entry.
+ */
+static int pci_create_resource_file_by_name(struct pci_dev *pdev,
+					     const char *name)
+{
+	unsigned int num;
+	int wc = 0;
+	int n = 0;
+
+	if (sscanf(name, "resource%u%n", &num, &n) != 1)
+		return -ENOENT;
+	if (name[n]) {
+		if (strcmp(name + n, "_wc"))
+			return -ENOENT;
+		wc = 1;
+	}
+
+	if (num >= PCI_STD_NUM_BARS || !pci_resource_len(pdev, num))
+		return -ENOENT;
+
+	if (wc && (!arch_can_pci_mmap_wc() ||
+		   !(pdev->resource[num].flags & IORESOURCE_PREFETCH)))
+		return -ENOENT;
+
+	return pci_create_attr(pdev, num, wc);
+}
 #else /* !(defined(HAVE_PCI_MMAP) || defined(ARCH_GENERIC_PCI_MMAP_RESOURCE)) */
 int __weak pci_create_resource_files(struct pci_dev *dev) { return 0; }
 void __weak pci_remove_resource_files(struct pci_dev *dev) { return; }
+static int pci_create_resource_file_by_name(struct pci_dev *pdev,
+					     const char *name)
+{
+	return -ENOENT;
+}
 #endif
 
 /**
@@ -1661,26 +1710,88 @@ static const struct attribute_group pci_dev_resource_resize_group = {
 	.is_visible = resource_resize_is_visible,
 };
 
+/* Per-device PCI sysfs catalogue. */
+
+/* physfn back-symlink - SR-IOV VF only. */
+
+#ifdef CONFIG_PCI_IOV
+
+static bool pci_applies_to_virtfn(struct device *dev)
+{
+	return to_pci_dev(dev)->is_virtfn;
+}
+
+static int pci_create_physfn_link(struct device *dev, const char *name)
+{
+	struct pci_dev *pdev = to_pci_dev(dev);
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (sysfs_kn_exists(&dev->kobj, "physfn"))
+		return 0;
+
+	return sysfs_create_link(&dev->kobj, &pdev->physfn->dev.kobj, "physfn");
+}
+
+#endif /* CONFIG_PCI_IOV */
+
+/* resource%u[_wc] family - wildcard row, create() dispatches on @name. */
+
+static int pci_create_resource(struct device *dev, const char *name)
+{
+	struct pci_dev *pdev = to_pci_dev(dev);
+
+	if (device_is_sysfs_lazy(dev))
+		lockdep_assert_held(&dev->sysfs_lazy->lock);
+
+	if (!name)
+		return pci_create_resource_files(pdev);
+
+	return pci_create_resource_file_by_name(pdev, name);
+}
+
+static void pci_remove_resource(struct device *dev)
+{
+	pci_remove_resource_files(to_pci_dev(dev));
+}
+
+static const struct device_sysfs_entry pci_sysfs_entries[] = {
+#ifdef CONFIG_PCI_IOV
+	{
+		.name		= "physfn",
+		.applies_to	= pci_applies_to_virtfn,
+		.create	= pci_create_physfn_link,
+	},
+#endif
+	{
+		/* wildcard: "resource%u" / "resource%u_wc" family */
+		.create	= pci_create_resource,
+		.remove		= pci_remove_resource,
+	},
+	{ }, /* sentinel: .create == NULL */
+};
+
 int __must_check pci_create_sysfs_dev_files(struct pci_dev *pdev)
 {
 	if (!sysfs_initialized)
 		return -EACCES;
 
-	return pci_create_resource_files(pdev);
+	return device_sysfs_apply(&pdev->dev, pci_sysfs_entries,
+				 DEV_SYSFS_ADD_ALL, NULL);
 }
 
 /**
- * pci_remove_sysfs_dev_files - cleanup PCI specific sysfs files
+ * pci_remove_sysfs_dev_files - cleanup PCI sysfs files for @pdev
  * @pdev: device whose entries we should free
- *
- * Cleanup when @pdev is removed from sysfs.
  */
 void pci_remove_sysfs_dev_files(struct pci_dev *pdev)
 {
 	if (!sysfs_initialized)
 		return;
 
-	pci_remove_resource_files(pdev);
+	device_sysfs_apply(&pdev->dev, pci_sysfs_entries,
+			  DEV_SYSFS_REMOVE_ALL, NULL);
 }
 
 static int __init pci_sysfs_init(void)
@@ -1835,3 +1946,15 @@ const struct attribute_group *pci_dev_attr_groups[] = {
 #endif
 	NULL,
 };
+
+int pci_dev_type_populate_one(struct device *dev, const char *name)
+{
+	return device_sysfs_apply(dev, pci_sysfs_entries,
+				 DEV_SYSFS_ADD_ONE, name);
+}
+
+void pci_dev_type_populate_all(struct device *dev)
+{
+	device_sysfs_apply(dev, pci_sysfs_entries,
+			  DEV_SYSFS_ADD_ALL, NULL);
+}
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 4a14f88e543a2..9c884a158184c 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -395,6 +395,8 @@ static inline int pci_no_d1d2(struct pci_dev *dev)
 #ifdef CONFIG_SYSFS
 int pci_create_sysfs_dev_files(struct pci_dev *pdev);
 void pci_remove_sysfs_dev_files(struct pci_dev *pdev);
+int pci_dev_type_populate_one(struct device *dev, const char *name);
+void pci_dev_type_populate_all(struct device *dev);
 extern const struct attribute_group *pci_dev_groups[];
 extern const struct attribute_group *pci_dev_attr_groups[];
 extern const struct attribute_group *pcibus_groups[];
@@ -403,6 +405,9 @@ extern const struct attribute_group pci_doe_sysfs_group;
 #else
 static inline int pci_create_sysfs_dev_files(struct pci_dev *pdev) { return 0; }
 static inline void pci_remove_sysfs_dev_files(struct pci_dev *pdev) { }
+static inline int pci_dev_type_populate_one(struct device *dev,
+					    const char *name) { return -ENOENT; }
+static inline void pci_dev_type_populate_all(struct device *dev) { }
 #define pci_dev_groups NULL
 #define pci_dev_attr_groups NULL
 #define pcibus_groups NULL
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index b63cd0c310bc0..ff7e6016c896f 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -2499,7 +2499,9 @@ static void pci_release_dev(struct device *dev)
 }
 
 static const struct device_type pci_dev_type = {
-	.groups = pci_dev_attr_groups,
+	.groups		= pci_dev_attr_groups,
+	.populate	= pci_dev_type_populate_one,
+	.populate_all	= pci_dev_type_populate_all,
 };
 
 struct pci_dev *pci_alloc_dev(struct pci_bus *bus)
diff --git a/include/linux/device.h b/include/linux/device.h
index c0a2ee429280c..948c3ea8055e5 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -118,14 +118,27 @@ struct device_sysfs_entry {
 	void (*remove)(struct device *dev);
 };
 
-/*
- * The type of device, "struct device" is embedded in. A class
- * or bus can contain devices of different types
- * like "partitions" and "disks", "mouse" and "event".
- * This identifies the device type and carries type-specific
- * information, equivalent to the kobj_type of a kobject.
- * If "name" is specified, the uevent will contain it in
- * the DEVTYPE variable.
+int device_sysfs_apply(struct device *dev,
+		      const struct device_sysfs_entry *entries,
+		      enum dev_sysfs_action action, const char *name);
+
+/**
+ * struct device_type - The type of device, embedded in struct device.
+ * @name:     if set, appears in the DEVTYPE uevent variable.
+ * @groups:   attribute groups always present on devices of this type.
+ * @uevent:   type-specific uevent handler (may set DEVTYPE or other).
+ * @devnode:  populate /dev node metadata (mode, uid, gid).
+ * @release:  release callback run after the last reference drops.
+ * @pm:       dev_pm_ops vector for devices of this type.
+ * @populate: lazy ADD_ONE for type-owned synthetic attrs/symlinks.
+ *            Called from kernfs populate; device_lock NOT held; may
+ *            sleep. Returns -ENOENT for unknown names.
+ * @populate_all: readdir-time counterpart; best-effort, retry-safe.
+ *
+ * A class or bus can contain devices of different types like
+ * "partitions" and "disks", "mouse" and "event". This identifies
+ * the device type and carries type-specific information, equivalent
+ * to the kobj_type of a kobject.
  */
 struct device_type {
 	const char *name;
@@ -136,6 +149,9 @@ struct device_type {
 	void (*release)(struct device *dev);
 
 	const struct dev_pm_ops *pm;
+
+	int  (*populate)(struct device *dev, const char *name);
+	void (*populate_all)(struct device *dev);
 };
 
 /**
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 09/14] iommu: lazy-populate iommu_group reserved_regions/type attrs
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
                     ` (2 preceding siblings ...)
  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   ` 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
                     ` (4 subsequent siblings)
  8 siblings, 1 reply; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

iommu_group_alloc() eagerly creates two attribute files -
reserved_regions and type - that are read only on the VFIO /
iommufd ioctl path (VFIO_IOMMU_GET_INFO, iommufd type query).
On systems with thousands of SR-IOV VFs each VF typically gets
its own iommu_group, so deferring these two attrs eliminates two
kernfs nodes per group at boot.

iommu_group_ktype is the first non-device_ktype consumer of the
populate mechanism: wire iommu_group_populate_one /
iommu_group_populate_all and call kernfs_set_lazy() on the
group's kernfs node immediately after kobject_init_and_add().
See the in-diff comment at the kernfs_set_lazy() site for the
ordering rationale (must precede kobject_create_and_add("devices")
to interact correctly with kernfs_inc_rev()).

Both callbacks fast-path on iommu_group_sysfs_populated() so
re-entry after the directory is fully populated is cheap and
cannot fire sysfs_warn_dup().

The name attribute is created on demand by iommu_group_set_name()
on a separate API path and is not handled by these callbacks;
groups without a set name return -ENOENT on lookup of name.

No userspace ABI change.  A selftest for this deferral is added
later in the series under tools/testing/selftests/sysfs-lazy/.

Cc: Joerg Roedel <joro@8bytes.org>
Cc: Will Deacon <will@kernel.org>
Cc: Robin Murphy <robin.murphy@arm.com>
Cc: Jason Gunthorpe <jgg@ziepe.ca>
Cc: Kevin Tian <kevin.tian@intel.com>
Cc: Shuah Khan <shuah@kernel.org>
Cc: iommu@lists.linux.dev
Cc: linux-kselftest@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-api@vger.kernel.org
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.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>
---
 drivers/iommu/iommu.c | 132 +++++++++++++++++++++++++++++++++++++-----
 1 file changed, 119 insertions(+), 13 deletions(-)

diff --git a/drivers/iommu/iommu.c b/drivers/iommu/iommu.c
index 61c12ba782066..7a8dd43f0a09a 100644
--- a/drivers/iommu/iommu.c
+++ b/drivers/iommu/iommu.c
@@ -21,6 +21,7 @@
 #include <linux/iommufd.h>
 #include <linux/idr.h>
 #include <linux/err.h>
+#include <linux/kernfs.h>
 #include <linux/pci.h>
 #include <linux/pci-ats.h>
 #include <linux/bitops.h>
@@ -71,8 +72,22 @@ struct iommu_group {
 	struct list_head entry;
 	unsigned int owner_cnt;
 	void *owner;
+	/* Embedded lazy-sysfs state (every iommu_group is lazy). */
+	struct sysfs_lazy_state sysfs_lazy;
 };
 
+static inline bool iommu_group_sysfs_populated(const struct iommu_group *group)
+{
+	return READ_ONCE(group->sysfs_lazy.populated);
+}
+
+static inline void
+iommu_group_sysfs_set_populated(struct iommu_group *group)
+{
+	lockdep_assert_held(&group->sysfs_lazy.lock);
+	WRITE_ONCE(group->sysfs_lazy.populated, true);
+}
+
 struct group_device {
 	struct list_head list;
 	struct device *dev;
@@ -1033,9 +1048,99 @@ static void iommu_group_release(struct kobject *kobj)
 	kfree(group);
 }
 
+/* Lazy iommu_group attrs (excludes "name", managed separately). */
+static const struct {
+	const char			*name;
+	struct iommu_group_attribute	*attr;
+} iommu_group_lazy_attrs[] = {
+	{ "reserved_regions", &iommu_group_attr_reserved_regions },
+	{ "type",             &iommu_group_attr_type },
+};
+
+static int iommu_group_populate_one(struct kobject *kobj, const char *name)
+{
+	struct iommu_group *group = to_iommu_group(kobj);
+	size_t i;
+	int ret = -ENOENT;
+	bool name_present = false;
+
+	/* Fast path: directory fully populated; kernfs has authoritative state. */
+	if (iommu_group_sysfs_populated(group))
+		return -ENOENT;
+
+	mutex_lock(&group->sysfs_lazy.lock);
+	/* Re-check under the lock against a concurrent populate_all. */
+	if (iommu_group_sysfs_populated(group)) {
+		ret = -ENOENT;
+		goto out;
+	}
+
+	/* Materialise the whole table (2 entries); cheaper than per-name dispatch. */
+	for (i = 0; i < ARRAY_SIZE(iommu_group_lazy_attrs); i++) {
+		struct iommu_group_attribute *attr =
+			iommu_group_lazy_attrs[i].attr;
+		int rc;
+
+		if (sysfs_kn_exists(&group->kobj, attr->attr.name))
+			rc = 0;
+		else
+			rc = iommu_group_create_file(group, attr);
+
+		if (!strcmp(name, iommu_group_lazy_attrs[i].name)) {
+			name_present = true;
+			ret = rc;
+		} else if (rc) {
+			pr_warn("group %d: lazy-create %s failed: %d\n",
+				group->id,
+				iommu_group_lazy_attrs[i].name, rc);
+		}
+	}
+	iommu_group_sysfs_set_populated(group);
+
+out:
+	mutex_unlock(&group->sysfs_lazy.lock);
+	return name_present ? ret : -ENOENT;
+}
+
+static void iommu_group_populate_all(struct kobject *kobj)
+{
+	struct iommu_group *group = to_iommu_group(kobj);
+	size_t i;
+	int ret;
+
+	/* Fast path: directory already fully populated. */
+	if (iommu_group_sysfs_populated(group))
+		return;
+
+	mutex_lock(&group->sysfs_lazy.lock);
+	if (iommu_group_sysfs_populated(group))
+		goto out;
+
+	for (i = 0; i < ARRAY_SIZE(iommu_group_lazy_attrs); i++) {
+		struct iommu_group_attribute *attr =
+			iommu_group_lazy_attrs[i].attr;
+
+		/* Existence check absorbs the populate_one race. */
+		if (sysfs_kn_exists(&group->kobj, attr->attr.name))
+			continue;
+
+		ret = iommu_group_create_file(group, attr);
+		if (ret)
+			pr_warn("group %d: lazy-create %s failed: %d\n",
+				group->id,
+				iommu_group_lazy_attrs[i].name, ret);
+	}
+
+	iommu_group_sysfs_set_populated(group);
+out:
+	mutex_unlock(&group->sysfs_lazy.lock);
+}
+
 static const struct kobj_type iommu_group_ktype = {
 	.sysfs_ops = &iommu_group_sysfs_ops,
 	.release = iommu_group_release,
+	.populate     = iommu_group_populate_one,
+	.populate_all = iommu_group_populate_all,
 };
 
 /**
@@ -1060,6 +1165,7 @@ struct iommu_group *iommu_group_alloc(void)
 
 	group->kobj.kset = iommu_group_kset;
 	mutex_init(&group->mutex);
+	mutex_init(&group->sysfs_lazy.lock);
 	INIT_LIST_HEAD(&group->devices);
 	INIT_LIST_HEAD(&group->entry);
 	xa_init(&group->pasid_array);
@@ -1078,6 +1184,19 @@ struct iommu_group *iommu_group_alloc(void)
 		return ERR_PTR(ret);
 	}
 
+	/*
+	 * Defer reserved_regions and type - both are read only on
+	 * VFIO/iommufd ioctl paths, so let the populate callbacks
+	 * materialise them on demand.
+	 *
+	 * KERNFS_LAZY MUST be set before kobject_create_and_add("devices"),
+	 * which calls kernfs_inc_rev() on this kobject's directory and
+	 * would otherwise leave a stale negative dentry cached for a
+	 * missing reserved_regions/type child.  WARN_ON because
+	 * iommu_group_alloc() controls all group kobjects directly.
+	 */
+	WARN_ON(kernfs_set_lazy(group->kobj.sd));
+
 	group->devices_kobj = kobject_create_and_add("devices", &group->kobj);
 	if (!group->devices_kobj) {
 		kobject_put(&group->kobj); /* triggers .release & free */
@@ -1091,19 +1210,6 @@ struct iommu_group *iommu_group_alloc(void)
 	 */
 	kobject_put(&group->kobj);
 
-	ret = iommu_group_create_file(group,
-				      &iommu_group_attr_reserved_regions);
-	if (ret) {
-		kobject_put(group->devices_kobj);
-		return ERR_PTR(ret);
-	}
-
-	ret = iommu_group_create_file(group, &iommu_group_attr_type);
-	if (ret) {
-		kobject_put(group->devices_kobj);
-		return ERR_PTR(ret);
-	}
-
 	pr_debug("Allocated group %d\n", group->id);
 
 	return group;
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 10/14] PCI/IOV: opt SR-IOV VFs into sysfs_lazy
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
                     ` (3 preceding siblings ...)
  2026-07-02 17:51   ` [RFC PATCH 09/14] iommu: lazy-populate iommu_group reserved_regions/type attrs Pavol Sakac
@ 2026-07-02 17:51   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 11/14] vfio: opt vfio-dev and VFIO group devices " Pavol Sakac
                     ` (3 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

First behaviour change in the lazy-sysfs series. Call
device_set_sysfs_lazy() on every SR-IOV Virtual Function before
pci_device_add() so the VF kobj directory is marked KERNFS_LAZY and
attribute files materialise on first userspace access.

Place the opt-in in pci_iov_scan_device() (immediately after
pci_setup_device()), which is the single allocation/cleanup site
for a VF struct (Shay Drory, commit 04d50d953ab4 ("PCI: Fix NULL
dereference in SR-IOV VF creation error path")). Any -ENOMEM from
device_set_sysfs_lazy() rolls back through that one site rather
than leaking the VF struct, the physfn ref, and the bus ref.

Add an early-return guard at the top of pci_create_sysfs_dev_files()
so lazy VFs skip eager resource-file creation; the guard covers
both pci_bus_add and the late pci_sysfs_init iterator.
pci_remove_sysfs_dev_files() stays unguarded - its per-entry remove
callbacks are idempotent.

Marking VFs lazy defers the bulk of each VF's sysfs population to
first access.  The cost avoided is per VF and scales with the number
of VFs, so on hosts that create a large number of VFs it removes a
correspondingly large amount of up-front kernfs node allocation at
probe.  The nodes are deferred, not dropped: reading a VF's
attributes materialises the same tree as before.  Quantified results
are deferred to the cover letter.

No userspace ABI change: all files eventually materialize.

Cc: Bjorn Helgaas <bhelgaas@google.com>
Cc: linux-pci@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: driver-core@lists.linux.dev
Cc: linux-api@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/pci/iov.c       | 19 +++++++++++++------
 drivers/pci/pci-sysfs.c |  9 +++++++++
 2 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/drivers/pci/iov.c b/drivers/pci/iov.c
index ec2fca8209593..5537c3df6f45b 100644
--- a/drivers/pci/iov.c
+++ b/drivers/pci/iov.c
@@ -328,14 +328,21 @@ static struct pci_dev *pci_iov_scan_device(struct pci_dev *dev, int id,
 		pci_read_vf_config_common(virtfn);
 
 	rc = pci_setup_device(virtfn);
-	if (rc) {
-		pci_dev_put(dev);
-		pci_bus_put(virtfn->bus);
-		kfree(virtfn);
-		return ERR_PTR(rc);
-	}
+	if (rc)
+		goto err_free;
+
+	/* Single-site -ENOMEM rollback (kfree + pci_dev_put/pci_bus_put). */
+	rc = device_set_sysfs_lazy(&virtfn->dev);
+	if (rc)
+		goto err_free;
 
 	return virtfn;
+
+err_free:
+	pci_dev_put(dev);
+	pci_bus_put(virtfn->bus);
+	kfree(virtfn);
+	return ERR_PTR(rc);
 }
 
 int pci_iov_add_virtfn(struct pci_dev *dev, int id)
diff --git a/drivers/pci/pci-sysfs.c b/drivers/pci/pci-sysfs.c
index 5a779ebaee326..8bf3837204693 100644
--- a/drivers/pci/pci-sysfs.c
+++ b/drivers/pci/pci-sysfs.c
@@ -1777,6 +1777,9 @@ int __must_check pci_create_sysfs_dev_files(struct pci_dev *pdev)
 	if (!sysfs_initialized)
 		return -EACCES;
 
+	if (device_is_sysfs_lazy(&pdev->dev))
+		return 0;	/* lazy: entries materialize on first access */
+
 	return device_sysfs_apply(&pdev->dev, pci_sysfs_entries,
 				 DEV_SYSFS_ADD_ALL, NULL);
 }
@@ -1790,6 +1793,12 @@ void pci_remove_sysfs_dev_files(struct pci_dev *pdev)
 	if (!sysfs_initialized)
 		return;
 
+	/*
+	 * No sysfs_lazy early-return here: populate_one/populate_all may
+	 * have materialized resource files on first access, so the
+	 * per-entry remove() callbacks must run unconditionally.  They
+	 * are idempotent either way.
+	 */
 	device_sysfs_apply(&pdev->dev, pci_sysfs_entries,
 			  DEV_SYSFS_REMOVE_ALL, NULL);
 }
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 11/14] vfio: opt vfio-dev and VFIO group devices into sysfs_lazy
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
                     ` (4 preceding siblings ...)
  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   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 12/14] driver core: test: add KUnit tests for device_sysfs_apply Pavol Sakac
                     ` (2 subsequent siblings)
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Call device_set_sysfs_lazy() on vfio_device->device (vfio-dev/) and
on the VFIO group's struct device (vfio/<group>/) before device_add().
With opt-in, vfio-dev directories materialise their attribute_groups
(vfio_dev_groups + drvdata->ops->dev_groups) on first access via the
sysfs lookup path instead of eagerly at device_add(). The VFIO
group device's flat attrs follow the same deferral.

Removes the eager creation cost on platforms with thousands of VFs
assigned to VFIO containers. No userspace ABI change.

Cc: Alex Williamson <alex.williamson@redhat.com>
Cc: Jason Gunthorpe <jgg@ziepe.ca>
Cc: kvm@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: driver-core@lists.linux.dev
Cc: linux-api@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/vfio/group.c     | 5 +++++
 drivers/vfio/vfio_main.c | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/drivers/vfio/group.c b/drivers/vfio/group.c
index b2299e5bc6df3..aa7e0033b96e8 100644
--- a/drivers/vfio/group.c
+++ b/drivers/vfio/group.c
@@ -533,6 +533,11 @@ static struct vfio_group *vfio_group_alloc(struct iommu_group *iommu_group,
 	group->dev.devt = MKDEV(MAJOR(vfio.group_devt), minor);
 	group->dev.class = &vfio_class;
 	group->dev.release = vfio_group_release;
+	/* Defer per-device attrs to first kernfs access; userspace reads via ioctl. */
+	if (device_set_sysfs_lazy(&group->dev)) {
+		put_device(&group->dev);
+		return ERR_PTR(-ENOMEM);
+	}
 	cdev_init(&group->cdev, &vfio_group_fops);
 	group->cdev.owner = THIS_MODULE;
 
diff --git a/drivers/vfio/vfio_main.c b/drivers/vfio/vfio_main.c
index 6222376ab6ab5..ab90279c44bd0 100644
--- a/drivers/vfio/vfio_main.c
+++ b/drivers/vfio/vfio_main.c
@@ -306,6 +306,11 @@ static int vfio_init_device(struct vfio_device *device, struct device *dev,
 			goto out_uninit;
 	}
 
+	/* Allocated before device_initialize() so out_uninit unwind frees it. */
+	ret = device_set_sysfs_lazy(&device->device);
+	if (ret)
+		goto out_uninit;
+
 	device_initialize(&device->device);
 	device->device.release = vfio_device_release;
 	device->device.class = &vfio_device_class;
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 12/14] driver core: test: add KUnit tests for device_sysfs_apply
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
                     ` (5 preceding siblings ...)
  2026-07-02 17:51   ` [RFC PATCH 11/14] vfio: opt vfio-dev and VFIO group devices " Pavol Sakac
@ 2026-07-02 17:51   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 13/14] Documentation: add lazy sysfs initialisation and device_sysfs_entry docs Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 14/14] selftests: sysfs-lazy: add tests for lazy sysfs initialization Pavol Sakac
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

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

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

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

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

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

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

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




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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 13/14] Documentation: add lazy sysfs initialisation and device_sysfs_entry docs
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
                     ` (6 preceding siblings ...)
  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   ` Pavol Sakac
  2026-07-02 17:51   ` [RFC PATCH 14/14] selftests: sysfs-lazy: add tests for lazy sysfs initialization Pavol Sakac
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

Document the lazy sysfs mechanism and its declarative walker:

  - Documentation/driver-api/sysfs-lazy.rst: developer guide
    covering overview, opting-in, populate callbacks (per-device
    walker and the iommu_group small-table pattern), locking,
    teardown, known limitations, and a "See also" section
    pointing to the KUnit tests and the kselftest.

  - Documentation/ABI/testing/sysfs-lazy: userspace-visible
    semantics - lookup vs readdir trigger points, stat() behaviour
    on known and unknown names, inotify / fanotify interaction,
    udev cold-plug compatibility, and the list of currently
    opted-in device classes (SR-IOV VFs, VFIO, iommu_groups).

  - Documentation/driver-api/index.rst: add sysfs-lazy to the
    toctree.

  - MAINTAINERS: add F: entries for the new docs, the KUnit test
    drivers/base/test/device_sysfs_apply_test.c, and the
    tools/testing/selftests/sysfs-lazy/ directory (added in the
    next commit) under DRIVER CORE, KOBJECTS, DEBUGFS AND SYSFS.

Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Rafael J. Wysocki <rafael@kernel.org>
Cc: Jonathan Corbet <corbet@lwn.net>
Cc: linux-doc@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-api@vger.kernel.org
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: driver-core@lists.linux.dev
Cc: Shuah Khan <shuah@kernel.org>
Cc: linux-kselftest@vger.kernel.org
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 Documentation/ABI/testing/sysfs-lazy    |  77 +++++++++++++
 Documentation/driver-api/index.rst      |   1 +
 Documentation/driver-api/sysfs-lazy.rst | 146 ++++++++++++++++++++++++
 MAINTAINERS                             |   4 +
 4 files changed, 228 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-lazy
 create mode 100644 Documentation/driver-api/sysfs-lazy.rst

diff --git a/Documentation/ABI/testing/sysfs-lazy b/Documentation/ABI/testing/sysfs-lazy
new file mode 100644
index 0000000000000..3a1b2a0e97e1c
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-lazy
@@ -0,0 +1,77 @@
+What:		/sys/devices/.../
+		/sys/kernel/iommu_groups/<N>/
+Date:		May 2026
+KernelVersion:	7.1
+Contact:	Pavol Sakac <sakacpav@amazon.de>
+Description:
+		This entry documents the *realisation timing* contract
+		for sysfs entries under devices that have opted into
+		lazy population.  All attribute names, contents, modes,
+		and ownership are identical to the eager-device tree;
+		only the moment of creation differs.  The opt-in set is
+		enumerated in "Currently opted-in device classes" below.
+
+		When a device driver sets the sysfs_lazy flag on a struct
+		device before calling device_add(), the kernel defers
+		creation of attribute files, attribute groups, and
+		class/bus/driver symlinks under that device's sysfs
+		directory.
+
+		These entries are materialised on demand:
+
+		- A lookup (open, stat, readlink) of a specific filename
+		  triggers creation of that single file or symlink via
+		  the ktype->populate callback, which drives
+		  device_sysfs_apply() with DEV_SYSFS_ADD_ONE.
+		- A directory listing (readdir / getdents) triggers
+		  creation of all deferred entries at once via the
+		  ktype->populate_all callback, which drives
+		  device_sysfs_apply() with DEV_SYSFS_ADD_ALL.
+
+		A stat() call on a not-yet-realised filename:
+
+		- If the name corresponds to a known row (including one
+		  owned by dev->type->entries), the attribute or
+		  symlink is created and stat() succeeds with the same
+		  metadata an eager device would show.
+		- If the name is unknown, stat() returns -ENOENT and
+		  kernfs caches a negative dentry.  Transient errors
+		  (-ENOMEM, -EBUSY) propagate unchanged and are NOT
+		  negative-cached; the next lookup retries.
+		  access(F_OK) behaves identically to stat().
+
+		readdir() returns the full set of names the device
+		would expose if eager - applies_to gating is evaluated
+		during populate_all(), so the visible set exactly
+		matches the eager-device tree.
+
+		Once a row's entry is realised, it persists for the
+		lifetime of the device.  A second open()/stat() is a
+		plain kernfs lookup and involves no walker invocation.
+
+		What does NOT change relative to an eager device:
+
+		- The set of files and their names (applies_to is the
+		  single gate, identical between eager and lazy paths).
+		- File contents, permissions, ownership, and SELinux
+		  labels.
+		- The device directory itself (always eager) and its
+		  parent chain.
+
+		Interaction with inotify / fanotify:
+		  Watchers see IN_CREATE events at first-access time,
+		  not at device_add() time.
+
+		Interaction with udev / systemd-udevd:
+		  udev cold-plug works without modification.  udev
+		  reads the device directory on KOBJ_ADD, which
+		  triggers readdir -> full realisation.
+
+		Currently opted-in device classes:
+
+		- PCI SR-IOV Virtual Functions (drivers/pci/iov.c)
+		- VFIO group and vfio-dev children (drivers/vfio/)
+		- IOMMU groups (/sys/kernel/iommu_groups/<N>/,
+		  drivers/iommu/iommu.c)
+
+Users:		udev, systemd-udevd, libvirt, DPDK, SPDK
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index eaf7161ff9578..b1807ca6cd1ce 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -23,6 +23,7 @@ of interest to most developers working on device drivers.
    driver-model/index
    device_link
    infrastructure
+   sysfs-lazy
    ioctl
    pm/index
 
diff --git a/Documentation/driver-api/sysfs-lazy.rst b/Documentation/driver-api/sysfs-lazy.rst
new file mode 100644
index 0000000000000..5fbd7732afe0d
--- /dev/null
+++ b/Documentation/driver-api/sysfs-lazy.rst
@@ -0,0 +1,146 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=========================
+Lazy sysfs initialisation
+=========================
+
+Overview
+========
+
+``device_add()`` normally creates every sysfs attribute, attribute
+group, and class/bus/driver symlink under a new device's directory
+synchronously.  For populations created in bulk - hundreds of PCI
+SR-IOV VFs, fleets of vfio-dev children - that work dominates
+``device_add()`` latency yet the resulting sysfs nodes are rarely
+read.
+
+Lazy sysfs defers attribute creation until the directory is first
+opened by userspace.  The device directory itself, its parent chain,
+and uevent broadcast remain eager; only the *content* of the
+directory is deferred.
+
+Opting in
+=========
+
+A subsystem opts a device in by:
+
+1. Calling ``device_set_sysfs_lazy(dev)`` before ``device_add()``.
+2. Making the kernfs directory created by ``device_add()`` a lazy
+   directory via ``kernfs_set_lazy()``.
+
+``kernfs_set_lazy()`` flips ``KERNFS_LAZY`` on a directory under
+``kernfs_rwsem`` and returns ``-EINVAL`` for namespaced nodes (a
+non-NULL namespace tag or the ``KERNFS_NS`` flag) and non-directory
+nodes: lazy dispatch does not carry an active namespace tag into the
+populate callback.
+
+The flag is set once and never cleared.
+
+Populate callbacks
+==================
+
+A lazy directory's first ``readdir(3)`` or ``open(2)`` of a
+not-yet-created child invokes one of two callbacks defined on
+``struct kobj_type``::
+
+    int (*populate)(struct kobject *kobj, const char *name);
+    void (*populate_all)(struct kobject *kobj);
+
+``populate(name)`` runs on a named lookup miss and creates the single
+matching attribute.  ``populate_all()`` runs on the first
+``readdir(3)`` and creates every attribute the device exposes.
+
+For a ``struct device``, the driver core wires both callbacks to
+``device_sysfs_apply()``, which walks an immutable
+``device_sysfs_entry`` table.  Each row declares an
+``applies_to(dev)`` predicate, a ``create(dev, name)`` action, and a
+``remove(dev)`` action.  The walker dispatches the rows whose
+predicate passes for ``@dev``; the rows themselves call
+``sysfs_create_file()``, ``sysfs_create_link()``, or
+``sysfs_create_group()`` directly.
+
+The same pattern is used by ``iommu_group``: a lazy ``iommu_group``
+kobject's ``populate``/``populate_all`` walks a small fixed table of
+group attributes (``reserved_regions``, ``type``).
+
+Locking
+=======
+
+Each lazy device holds ``dev->sysfs_lazy->lock`` (a mutex).  All
+three populate paths take it::
+
+    populate(name)      -> mutex_lock(lock)
+                          walk(ADD_ONE, name)
+                          mutex_unlock
+    populate_all()      -> if (test_bit(POPULATED)) return;
+                          mutex_lock(lock)
+                          if (test_bit(POPULATED)) goto out;
+                          if (dev->p->dead)        goto out;
+                          walk(ADD_ALL, NULL)
+                          set_bit(POPULATED)
+                          mutex_unlock
+    REMOVE_ALL teardown -> mutex_lock(lock)
+                          walk(REMOVE_ALL, NULL)
+                          mutex_unlock
+
+The ``dev->sysfs_lazy->populated`` bool is the
+``populate_all()`` once-latch.  The unlocked read fast path is a
+performance optimisation; the field is only authoritative under
+``lock``.  A stale false on the fast path harmlessly retakes
+the mutex and re-checks.
+
+Lock ordering: ``lock`` nests inside any caller-held VFS or
+kernfs lock that brought us into ``populate``/``populate_all``.  No
+sysfs / kernfs operation that requires ``kernfs_rwsem`` write may be
+issued while holding ``lock``.
+
+The ``iommu_group`` path uses the same pattern:
+``group->sysfs_lazy.lock`` and
+``group->sysfs_lazy.populated``.
+
+Teardown
+========
+
+``device_del()`` sets ``dev->p->dead`` *before* the
+``REMOVE_ALL`` walker call and takes ``lock`` around it.
+Any populate callback racing with ``device_del()``:
+
+1. Either takes the lock first and runs against a not-yet-dead
+   device (its created files are torn down a moment later by
+   ``REMOVE_ALL``, which is idempotent against the row's
+   ``remove()``); or
+2. Sees ``dev->p->dead == true`` after acquiring the lock and bails
+   without creating anything.
+
+Either way, the device exits with a quiesced sysfs subtree and no
+populate callback sees a half-deleted device.
+
+``iommu_group_release()`` follows the same dead-flag + lock-then-walk
+contract.
+
+Known limitations
+=================
+
+* Lazy sysfs is only available for devices whose populate path is
+  driven through ``device_sysfs_entry`` rows.  Subsystems with bespoke
+  ``device_add()``-time sysfs creation must migrate before opting in.
+* Namespaced kobjects cannot be marked lazy: ``kernfs_set_lazy()``
+  rejects them because the populate callback does not carry the
+  active namespace tag.
+* The directory's eager skeleton (the directory inode itself and the
+  ``uevent`` file emitted by ``device_add()``) is not deferred.
+* Per-attribute creation cost is unchanged.  Lazy sysfs amortises
+  the cost across first-access events instead of paying it during
+  ``device_add()``.
+
+See also
+========
+
+* ``include/linux/kernfs.h`` - ``kernfs_set_lazy()``, ``KERNFS_LAZY``.
+* ``drivers/base/core.c`` - ``device_sysfs_apply()``,
+  ``driver_core_sysfs_entries[]``.
+* ``drivers/iommu/iommu.c`` - ``iommu_group_lazy_attrs[]``.
+* ``drivers/base/test/device_sysfs_apply_test.c`` - KUnit walker
+  contract tests.
+* ``tools/testing/selftests/sysfs-lazy/`` - userspace VFS-level
+  parity tests.
diff --git a/MAINTAINERS b/MAINTAINERS
index 5d8a887c868ed..1424e641d535f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7800,9 +7800,12 @@ M:	Danilo Krummrich <dakr@kernel.org>
 L:	driver-core@lists.linux.dev
 S:	Supported
 T:	git git://git.kernel.org/pub/scm/linux/kernel/git/driver-core/driver-core.git
+F:	Documentation/ABI/testing/sysfs-lazy
 F:	Documentation/core-api/kobject.rst
 F:	Documentation/driver-api/driver-model/
+F:	Documentation/driver-api/sysfs-lazy.rst
 F:	drivers/base/
+F:	drivers/base/test/device_sysfs_apply_test.c
 F:	fs/debugfs/
 F:	fs/sysfs/
 F:	include/linux/device/
@@ -7830,6 +7833,7 @@ F:	samples/rust/rust_debugfs_scoped.rs
 F:	samples/rust/rust_driver_platform.rs
 F:	samples/rust/rust_driver_faux.rs
 F:	samples/rust/rust_soc.rs
+F:	tools/testing/selftests/sysfs-lazy/
 
 DRIVERS FOR OMAP ADAPTIVE VOLTAGE SCALING (AVS)
 M:	Nishanth Menon <nm@ti.com>
-- 
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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [RFC PATCH 14/14] selftests: sysfs-lazy: add tests for lazy sysfs initialization
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
                     ` (7 preceding siblings ...)
  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
  8 siblings, 0 replies; 17+ messages in thread
From: Pavol Sakac @ 2026-07-02 17:51 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich,
	Tejun Heo
  Cc: linux-kernel, driver-core, David Woodhouse, Pasha Tatashin,
	Mike Rapoport, Pratyush Yadav, David Matlack, Samiullah Khawaja,
	Alexander Graf, linux-mm, kexec, nh-open-source

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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* Re: [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up
  2026-07-02 17:40 [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Pavol Sakac
                   ` (4 preceding siblings ...)
  2026-07-02 17:51 ` [RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker Pavol Sakac
@ 2026-07-10 14:16 ` Greg Kroah-Hartman
  5 siblings, 0 replies; 17+ messages in thread
From: Greg Kroah-Hartman @ 2026-07-10 14:16 UTC (permalink / raw)
  To: Pavol Sakac
  Cc: Rafael J . Wysocki, Danilo Krummrich, Tejun Heo, linux-kernel,
	driver-core, David Woodhouse, Pasha Tatashin, Mike Rapoport,
	Pratyush Yadav, David Matlack, Samiullah Khawaja, Alexander Graf,
	linux-mm, kexec, nh-open-source

On Thu, Jul 02, 2026 at 07:40:19PM +0200, Pavol Sakac wrote:
> Virtualization hardware keeps increasing in CPU-core and VF density.
> Kernel is trending towards preserving VFs using LUO across a kexec
> which will put SR-IOV in live-update hotpath. VFs must be re-added to
> the sysfs tree, along with its supporting devices (e.g. vfio,
> iommu_groups). On production hardware a single VF can consume ~80 kernfs
> nodes. With thousands of VFs on modern hardware, hundreds of thousands
> of kernfs nodes need to be created, which can add 100ms+ to the boot
> time and to guest downtime.

Hundreds of thousands?  You have a hundred sysfs files per device?

> Proposed here is a PoC of deferred materialization of sysfs files until
> first access to avoid cost of kernfs node creation in hot path. In the
> reproducer below using a synthetic VF to isolate the sysfs overhead,
> the per-device sysfs-creation time is reduced by ~74%. It exploits the
> expected access pattern of hypervisors, where only a subset of device
> files need to be accessed to attach a VF to a VM on the hot path.

Ick, that access pattern could change with other users, this feels very
fragile.

> To allow lazy init scheme, a minimal set of files and directories is
> built per device: a device directory resolves directly to the device's
> kobject and is used to materialize attributes at access time. The
> access is trapped in kernfs at the dir-read or path-walk stage and
> depending on the trigger, either the full directory or a single file is
> materialized.
> 
> The changes are two logically distinct parts:
> 
>   1. Refactor of hardcoded device_add / iommu_group sysfs attribute
>      creation to a table-driven form to allow walkability of all
>      attributes and single point of definition for attributes
>      regardless of lazy opt-in to avoid duplicate definitions and
>      associated maintainability issues with it.

This is probably a good idea anyway.  Want to just send this as a series
to start with, so we can take that?

>   2. Lazy-init infra to front-run access to device files at lookup/
>      readdir time to trigger materialization. Opt-in per device: PCI VFs
>      and VFIO devices opt in via device_set_sysfs_lazy(); iommu_group
>      opts in at the kobject level.

This is going to get complex.  Where exactly is the bottleneck?  I'm
guessing that systems with that many devices also have many hundreds of
CPUs, right?  Are we hitting a single lock somewhere?  There have been
changes in kernfs in the past to split up the locks to be more
fine-grained, perhaps that needs to continue to solve this?

> As a PoC, the code does not yet follow this logical structure in the
> commit sequence and likely does not yet cover all corner cases.
> 
> Synthetic reproducer measurements
> ---------------------------------
>   devices   per-dev time   total time       kernfs nodes (base -> lazy)
>   -------   ------------   --------------   ----------------------------
>      500    31.8->8.3 us    15.9-> 4.1 ms     36,417 ->  3,922  (72.8->7.8/dev)
>     1000    33.2->8.7 us    33.2-> 8.7 ms     73,427 ->  8,432  (73.4->8.4/dev)
>     2500    32.8->9.1 us    82.0->22.7 ms    184,421 -> 21,925  (73.8->8.8/dev)
>     5000    33.0->8.7 us   165.1->43.5 ms    369,401 -> 44,406  (73.9->8.9/dev)
> 
> The reproducer is synthetic: an in-kernel module registers N unbound
> platform devices (60 attributes / 4 groups, ~74 kernfs nodes each; no
> config space, BARs, or probe) in one timed burst that models SR-IOV
> enablement. It runs as a two-kernel A/B from one vanilla 7.1-rc2 tree
> (eager baseline vs patched, plus a patched/opt-in-OFF arm), 20 runs per
> point. Measures only sysfs-creation slice.

I sure hope sr-iov devices are NOT platform devices.  If so, please go
fix that up first :)

> Deferral removes ~74% of the sysfs-creation time and ~88% of the kernfs
> nodes. At 5000 devices that is 121.6 ms removed (165.1 -> 43.5 ms) and
> ~325,000 fewer kernfs nodes (369,401 -> 44,406). The table-driven rework
> adds a +7..+11% eager-path time overhead to non-opted devices.

You're just deferring this work to happen later, so when is that
"later"?  And what about multi-threaded probing of the bus, doesn't that
speed stuff up too?

> Available as a docker image that orchestrates builds in qemu guest and
> prints results as a table (x86_64 Linux host with /dev/kvm assumed):
> 
>    docker run --device /dev/kvm ghcr.io/pavsa/linux-lazy-sysfs-vf-bench:7.1-rc2
> 
> Assumptions / Trade-offs
> ------------------------
> The saving depends on the access pattern of hypervisors, and on no
> component walking the whole device tree on the hot path to avoid full
> materialization. Both are expected to hold on optimized hypervisors.

Hah, as if.  Many userspace tools, once a device is notified, walk the
whole tree of it to read the attributes.  You are going to be fighting
that problem constantly...

> An actual production hypervisor was observed in local testing to
> materialize an additional 5-10% of total nodes on average on the hot
> path.
> 
> Overhead: +7..+11% eager-path time overhead applies to every non-opted
> device going through device_add() as all struct devices traverse the
> walker and can't be gated under a config option.
> The lazy opt-in infra can be gated under config option to have zero
> impact if disabled.

Again, where exactly is the time being spent?  What lock is "hot"?

> Final words
> -----------
> This is an RFC to first get feedback on the decisions taken at driver
> core + kernfs level before engaging maintainers of other subsystems
> if we want to pursue this further or pivot to something else.
> 
> The code is at a proof-of-concept quality written with AI assistance
> with some known limitations with main target to gather potential
> savings and initiate conversation.
> 
> Feedback appreciated for:
> - table-driven attribute refactor targeting common device add path /
>   suggestions for more efficient/less invasive approach

What's wrong with the normal way attributes are defined and allocated?
They should all be tables today, and the driver core creating them when
needed, no individual driver should ever be doing that on its own.

thanks,

greg k-h


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [RFC PATCH 09/14] iommu: lazy-populate iommu_group reserved_regions/type attrs
  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
  0 siblings, 0 replies; 17+ messages in thread
From: Greg Kroah-Hartman @ 2026-07-10 14:19 UTC (permalink / raw)
  To: Pavol Sakac
  Cc: Rafael J . Wysocki, Danilo Krummrich, Tejun Heo, linux-kernel,
	driver-core, David Woodhouse, Pasha Tatashin, Mike Rapoport,
	Pratyush Yadav, David Matlack, Samiullah Khawaja, Alexander Graf,
	linux-mm, kexec, nh-open-source

On Thu, Jul 02, 2026 at 07:51:09PM +0200, Pavol Sakac wrote:
> +	/*
> +	 * Defer reserved_regions and type - both are read only on
> +	 * VFIO/iommufd ioctl paths, so let the populate callbacks
> +	 * materialise them on demand.
> +	 *
> +	 * KERNFS_LAZY MUST be set before kobject_create_and_add("devices"),
> +	 * which calls kernfs_inc_rev() on this kobject's directory and
> +	 * would otherwise leave a stale negative dentry cached for a
> +	 * missing reserved_regions/type child.  WARN_ON because
> +	 * iommu_group_alloc() controls all group kobjects directly.
> +	 */
> +	WARN_ON(kernfs_set_lazy(group->kobj.sd));

You just crashed the kernel, that's not nice :(

>  	group->devices_kobj = kobject_create_and_add("devices", &group->kobj);
>  	if (!group->devices_kobj) {
>  		kobject_put(&group->kobj); /* triggers .release & free */
> @@ -1091,19 +1210,6 @@ struct iommu_group *iommu_group_alloc(void)
>  	 */
>  	kobject_put(&group->kobj);
>  
> -	ret = iommu_group_create_file(group,
> -				      &iommu_group_attr_reserved_regions);
> -	if (ret) {
> -		kobject_put(group->devices_kobj);
> -		return ERR_PTR(ret);
> -	}
> -
> -	ret = iommu_group_create_file(group, &iommu_group_attr_type);
> -	if (ret) {
> -		kobject_put(group->devices_kobj);
> -		return ERR_PTR(ret);
> -	}

Why isn't the driver core creating these properly on its own today?
These calls should not have to be done.  Also, what's up with the "raw"
kobject?  That's not ok, this is circumventing the driver core in a way
that is not going to work out at all (as you are finding here...)

thanks,

greg k-h


^ permalink raw reply	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2026-07-10 14:19 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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   ` [RFC PATCH 14/14] selftests: sysfs-lazy: add tests for lazy sysfs initialization Pavol Sakac
2026-07-10 14:16 ` [RFC PATCH 00/14] driver core: defer per-VF sysfs creation for fast SR-IOV bring-up Greg Kroah-Hartman

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox