[RFC PATCH 05/14] driver core: add struct device_sysfs_entry and walker

From: Pavol Sakac

Date: Thu Jul 02 2026 - 14:33:04 EST


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@xxxxxxxxxxxxxxxxxxx>
Cc: Rafael J. Wysocki <rafael@xxxxxxxxxx>
Cc: Tejun Heo <tj@xxxxxxxxxx>
Cc: Jonathan Corbet <corbet@xxxxxxx>
Cc: Bjorn Helgaas <bhelgaas@xxxxxxxxxx>
Cc: Danilo Krummrich <dakr@xxxxxxxxxx>
Cc: linux-kernel@xxxxxxxxxxxxxxx
Cc: linux-api@xxxxxxxxxxxxxxx
Cc: driver-core@xxxxxxxxxxxxxxx
Assisted-by: Claude:claude-opus-4.7
Signed-off-by: Pavol Sakac <sakacpav@xxxxxxxxx>
---
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