[RFC PATCH 3/8] sysfs: add opt-in staged directory creation and publication
From: Pavol Sakac
Date: Fri Sep 11 2026 - 13:54:23 EST
Wire the kernfs staged mechanism into sysfs without touching a single
population entry point. struct kobject gains one opt-in bit, sd_staged,
in the existing state bitfield hole, so no struct grows. When it is set,
sysfs_create_dir_ns() builds the directory with
kernfs_create_dir_ns_staged(); because kobj->sd is valid throughout the
window, every existing population path runs verbatim and staging lives
entirely below them in kernfs. The kobj->sd store becomes
smp_store_release() for every kobject, eager paths included, ordering
the node's initialisation before the pointer's publication.
sysfs_publish_dir() performs the staged-to-visible transition via
kernfs_publish(). It is __must_check like its sibling directory
creators, because an ignored publication failure leaves a permanently
invisible directory. The only modular caller anticipated is the KUnit
suite a later patch in this series adds, so the export is scoped with
EXPORT_SYMBOL_IF_KUNIT().
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@xxxxxxxxx>
---
fs/sysfs/dir.c | 52 ++++++++++++++++++++++++++++++++++++++---
include/linux/kobject.h | 25 ++++++++++++++++++++
include/linux/sysfs.h | 6 +++++
3 files changed, 80 insertions(+), 3 deletions(-)
diff --git a/fs/sysfs/dir.c b/fs/sysfs/dir.c
index ffdcd4153c58..1694c01e8b84 100644
--- a/fs/sysfs/dir.c
+++ b/fs/sysfs/dir.c
@@ -11,6 +11,7 @@
#define pr_fmt(fmt) "sysfs: " fmt
+#include <kunit/visibility.h>
#include <linux/fs.h>
#include <linux/kobject.h>
#include <linux/slab.h>
@@ -56,18 +57,63 @@ int sysfs_create_dir_ns(struct kobject *kobj, const struct ns_common *ns)
kobject_get_ownership(kobj, &uid, &gid);
- kn = kernfs_create_dir_ns(parent, kobject_name(kobj), 0755, uid, gid,
- kobj, ns);
+ if (kobject_sd_staged(kobj))
+ kn = kernfs_create_dir_ns_staged(parent, kobject_name(kobj),
+ 0755, uid, gid, kobj, ns);
+ else
+ kn = kernfs_create_dir_ns(parent, kobject_name(kobj), 0755,
+ uid, gid, kobj, ns);
if (IS_ERR(kn)) {
if (PTR_ERR(kn) == -EEXIST)
sysfs_warn_dup(parent, kobject_name(kobj));
return PTR_ERR(kn);
}
- kobj->sd = kn;
+ /*
+ * Publish the node with release semantics: every initialisation of
+ * @kn above is ordered before the pointer store, so a task that
+ * learns of @kobj through a synchronising handoff (registration
+ * locks, a uevent, a notifier) observes a fully initialised node.
+ * The kernfs staged funnel re-verifies the flags it routes on under
+ * its mutex, so its pre-lock advisory reads decide nothing.
+ */
+ smp_store_release(&kobj->sd, kn);
return 0;
}
+/**
+ * sysfs_publish_dir - make a staged kobject directory visible
+ * @kobj: object whose staged directory is to be published
+ *
+ * Completes the staged creation begun by sysfs_create_dir_ns() when
+ * kobject_set_sd_staged() armed it: links the directory and everything
+ * populated beneath it into the parent and activates it in a single step.
+ * On a name collision the standard duplicate-name warning is emitted,
+ * matching sysfs_create_dir_ns().
+ *
+ * Return: 0 on success, -EEXIST on a name collision, -ENOENT if the parent
+ * went away, or -EINVAL on misuse (WARN). On failure the subtree stays staged
+ * and is removed by the caller's normal error unwind
+ * (kobject_del()/sysfs_remove_dir()).
+ */
+int sysfs_publish_dir(struct kobject *kobj)
+{
+ int ret;
+
+ if (WARN_ON(!kobj || !kobj->sd))
+ return -EINVAL;
+
+ ret = kernfs_publish(kobj->sd);
+ if (ret == -EEXIST) {
+ struct kernfs_node *parent = kernfs_get_parent(kobj->sd);
+
+ sysfs_warn_dup(parent, kobject_name(kobj));
+ kernfs_put(parent);
+ }
+ return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(sysfs_publish_dir);
+
/**
* sysfs_remove_dir - remove an object's directory.
* @kobj: object.
diff --git a/include/linux/kobject.h b/include/linux/kobject.h
index 55e37a5d405e..938ff5ff747e 100644
--- a/include/linux/kobject.h
+++ b/include/linux/kobject.h
@@ -75,12 +75,37 @@ struct kobject {
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
+ /* see kobject_set_sd_staged() */
+ unsigned int sd_staged:1;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
};
+/**
+ * kobject_set_sd_staged - arm staged sysfs directory creation
+ * @kobj: object whose directory creation mode is being set
+ * @staged: true to create the directory staged (invisible)
+ *
+ * Set before kobject_add(); read by sysfs_create_dir_ns() at add time and
+ * by the caller's publish path afterwards. The bit is the snapshot of
+ * what sysfs honored at add time; publish paths must read it, not
+ * whatever live state armed it, so the query cannot race the
+ * registration. The bit is never cleared: a kobject re-added after
+ * kobject_del() is staged again and requires another sysfs_publish_dir().
+ */
+static inline void kobject_set_sd_staged(struct kobject *kobj, bool staged)
+{
+ kobj->sd_staged = staged;
+}
+
+/* see kobject_set_sd_staged() */
+static inline bool kobject_sd_staged(const struct kobject *kobj)
+{
+ return kobj->sd_staged;
+}
+
__printf(2, 3) int kobject_set_name(struct kobject *kobj, const char *name, ...);
__printf(2, 0) int kobject_set_name_vargs(struct kobject *kobj, const char *fmt, va_list vargs);
diff --git a/include/linux/sysfs.h b/include/linux/sysfs.h
index b1a3a1e6ad09..b537a5b52e1a 100644
--- a/include/linux/sysfs.h
+++ b/include/linux/sysfs.h
@@ -397,6 +397,7 @@ struct sysfs_ops {
#ifdef CONFIG_SYSFS
int __must_check sysfs_create_dir_ns(struct kobject *kobj, const struct ns_common *ns);
+int __must_check sysfs_publish_dir(struct kobject *kobj);
void sysfs_remove_dir(struct kobject *kobj);
int __must_check sysfs_rename_dir_ns(struct kobject *kobj, const char *new_name,
const struct ns_common *new_ns);
@@ -507,6 +508,11 @@ static inline int sysfs_create_dir_ns(struct kobject *kobj, const struct ns_comm
return 0;
}
+static inline int sysfs_publish_dir(struct kobject *kobj)
+{
+ return 0;
+}
+
static inline void sysfs_remove_dir(struct kobject *kobj)
{
}
--
2.47.3