* [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration
@ 2026-09-11 17:43 Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 1/8] kernfs: factor out reusable directory helpers Pavol Sakac
` (7 more replies)
0 siblings, 8 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
Once SR-IOV enablement becomes part of a kexec-based live update
(LUO), 100-1000s of VFs would perform initialization in parallel [1],
each VF adding ~60-80 kernfs nodes to sysfs, each node acquiring the
kernfs_rwsem write lock once (after [2]). Acquiring one global lock to
add a single node, potentially hundreds of thousands of times per
enable - a reality on datacenter-level HW - does not scale. The
eventual goal, not built in this set, is enabling SR-IOV on all PFs in
parallel; with PFs on different NUMA nodes, cores on different nodes
then compete for the same global lock. We need some way of reducing
its use.
When performing device_add() on a VF, the device's sysfs nodes do not
need to be visible while being built: publish them once fully built,
taking the global lock once.
This series implements that as an opt-in staged registration mode. The
device's subtree is created fully initialized, but not linked into its
parent's children rbtree and not activated, so no name lookup can reach
it; its creator reaches it through kobj->sd. While staged, the kernfs
add, lookup and remove entry points - kernfs_add_one(),
kernfs_find_and_get_ns() and the two kernfs_remove() paths - serialize
on a mutex hashed by the staged-top node, a second NR_KERNFS_LOCKS-entry
array alongside the existing node_mutex, instead of the root's
kernfs_rwsem; unrelated staged subtrees serialize only when they hash to
the same slot. The staged paths bump the parent's timestamps under the
subtree mutex, not kernfs_iattr_rwsem, so a kernfs_setattr() there can
skew ctime/mtime on a node that is not yet visible; no opt-in path does
it. kernfs_publish() then links and activates the whole subtree in one
kernfs_rwsem write hold - one write hold per staged subtree instead of
one write acquisition per node. Attributes added after publication pay
per node as before. The device announces itself only after publication,
and struct kernfs_node does not grow: KERNFS_STAGED takes its last free
flag bit and the staged-top marker aliases the FILE-only KERNFS_HAS_MMAP
under a DIR-only rule, rather than widening the field. In this series,
PCI SR-IOV VFs and VFIO's group chardev and vfio-dev class devices opt
in.
Lock statistics and SR-IOV init time for 4x PF (NVMe, 255 VFs each), on
the reproducer from the parallel VF initialization cover letter [1]:
lock_stat:
Lock wait: Before After contentions: Before After
iommu_probe_device_lock 12367 ms 9612 ms 990 838
&root->kernfs_rwsem 2189 ms 843 ms 62799 16466
gdp_mutex 314 ms 410 ms 191 153
Stage SR-IOV init time:
S0 (baseline) 3027 ms
S1 999 ms
S2 995 ms
S3 991 ms
S4 943 ms
S5 (this series) 944 ms
Reproducer disclaimer:
I lean primarily on lock_stat numbers to defend the improvements. In
the reproducer, the residual iommu_probe_device_lock dominates the
window and masks the later series' wall-time gains; reducing that lock
further is out of scope for this set, but the dominant residual is
named in the iommu series [4].
On real hardware - a large dual-socket arm64 Neoverse V2 server with
thousands of VFs - iommu_probe_device_lock is nowhere near the top
due to other optimizations on that path, and kernfs_rwsem is the top
waited lock, which is what this series improves.
The five series together cut SR-IOV initialization by 65%; parallel
initialization [1] alone accounts for less than 5% of that, so the
reduction comes from the lock work in this and the preceding series.
Two transients are new: a symlink from a published directory into the
staged subtree resolves -ENOENT until publication, and a child device
registered inside the window is fully registered and bindable before its
sysfs tree is visible - its uevents are suppressed, and its addition is
replayed once the window closes (KOBJ_ADD, then KOBJ_BIND if it bound
meanwhile). Not fully addressed: an in-window KOBJ_CHANGE is dropped
rather than reconstructed, and if the opted-in registration fails the
queued children stay suppressed, so userspace sees neither their
addition nor their removal. A device should opt in only when its
children, if any, tolerate that. Non-opted devices are behaviorally
unchanged.
Patch 6, which defers those uevents, has no trigger from either opt-in
here - it exists for wakeup-source registration that an opted-in caller
does not control - and can be dropped in favour of stating the
restriction as an opt-in condition.
The existing KERNFS_ROOT_CREATE_DEACTIVATED does not help here: it
batches only activation - each node is still linked under the root write
lock.
RFC question: Is an opt-in staged mode an acceptable direction for
kernfs and the driver core, or should we pursue another direction? For
the creation window only, this splits the global lock from one
acquisition per node to one per device_add(), with the staged subtree
serialized on a hashed mutex instead for as long as it is staged.
This replaces the previously attempted optimization with lazy-sysfs [3].
Testing:
This series adds a KUnit suite. The lock_stat and timing figures come
from the public reproducer. The full series has also been tested on
current datacenter server hardware with thousands of VFs.
[1] https://lore.kernel.org/r/20260911-vfopt-s1-v1-0-693271dc0226@amazon.de
[2] https://lore.kernel.org/r/20260911-vfopt-s3-v1-0-66e3602f76f7@amazon.de
[3] https://lore.kernel.org/lkml/20260702174033.32116-1-sakacpav@amazon.de/
[4] https://lore.kernel.org/r/20260911-vfopt-s2-v1-0-fff3db7e01c2@amazon.de
Pavol Sakac (8):
kernfs: factor out reusable directory helpers
kernfs: add staged directory creation and publication
sysfs: add opt-in staged directory creation and publication
driver core: Register opted-in devices through the staged sysfs path
drivers: base: test: Add KUnit suite for staged sysfs registration
driver core: Defer uevents for devices registered in a staged window
PCI/IOV: Register virtual functions through the staged sysfs path
vfio: Opt the group and vfio-dev class devices into staged sysfs
drivers/base/base.h | 1 +
drivers/base/bus.c | 29 +-
drivers/base/core.c | 193 ++++
drivers/base/test/.kunitconfig | 1 +
drivers/base/test/Kconfig | 12 +
drivers/base/test/Makefile | 1 +
drivers/base/test/staged-device-test.c | 1406 ++++++++++++++++++++++++
drivers/pci/iov.c | 2 +
drivers/vfio/group.c | 7 +
drivers/vfio/vfio_main.c | 3 +
fs/kernfs/dir.c | 573 ++++++++--
fs/kernfs/mount.c | 4 +-
fs/sysfs/dir.c | 78 +-
include/linux/device.h | 26 +
include/linux/kernfs.h | 46 +
include/linux/kobject.h | 25 +
include/linux/sysfs.h | 12 +
17 files changed, 2352 insertions(+), 67 deletions(-)
create mode 100644 drivers/base/test/staged-device-test.c
base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
prerequisite-patch-id: 999e5d0fa76c26a057422f88454b1fefbb9b3822
prerequisite-patch-id: 40e7a6b4269c49235c297e1e8907cc85f39ec7bb
prerequisite-patch-id: 2de5f9198a49c3b10381455c3eb05a1445faaf9a
prerequisite-patch-id: 0bbae0313618b47cfedf5109565d41268cd6a48c
prerequisite-patch-id: 25f377891ba4b2c3915ccf6ac908074d0b427391
prerequisite-patch-id: 05867d594d1c5402821ee7f2172dd450b6e17659
prerequisite-patch-id: 99fc0938e1512adfaaa311f08a0ed1d7d0a4dc92
prerequisite-patch-id: 8898e957c193dcad8b70a534043a5e73ca3b8afe
prerequisite-patch-id: b5e8437666e8dd2a8cb5bcd4605665b3fab9556d
prerequisite-patch-id: d4e1d35cf3614bfbe325e03ab00d4882f222d599
prerequisite-patch-id: 747f6bb8326988a36f206a3a87ac8403dd616e3d
prerequisite-patch-id: e8d4a978122173cf6c708e4b730e1fa33bc1be57
--
2.47.3
^ permalink raw reply [flat|nested] 11+ messages in thread
* [RFC PATCH 1/8] kernfs: factor out reusable directory helpers
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 2/8] kernfs: add staged directory creation and publication Pavol Sakac
` (6 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
An upcoming change adds staged directories whose children collections are
protected by a per-subtree mutex instead of kernfs_rwsem, so it needs the
pure rbtree and directory-creation operations without the rwsem assertions
and accounting wrapped around them.
Split those out: __kernfs_link_sibling() and __kernfs_find_ns() for the
children-rbtree work, __kernfs_create_dir() for the sequence both directory
creators repeat, and kernfs_update_parent_times() for the parent timestamp
bump open-coded at each link and unlink site. The existing names stay as
locked wrappers carrying the lockdep assertions, the link-side wrapper
gaining the write-side assertion it lacked. No functional change intended.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
fs/kernfs/dir.c | 128 ++++++++++++++++++++++++++++++++----------------
1 file changed, 87 insertions(+), 41 deletions(-)
diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
index 1938edd39eff..a6290f94139c 100644
--- a/fs/kernfs/dir.c
+++ b/fs/kernfs/dir.c
@@ -459,12 +459,24 @@ static int kernfs_sd_compare(const struct kernfs_node *left,
return kernfs_name_compare(left->hash, kernfs_rcu_name(left), left->ns, right);
}
+/* Bump @parent's ctime/mtime; caller holds whichever lock covers @parent. */
+static void kernfs_update_parent_times(struct kernfs_node *parent)
+{
+ struct kernfs_iattrs *ps_iattr = parent ? parent->iattr : NULL;
+
+ if (ps_iattr) {
+ ktime_get_real_ts64(&ps_iattr->ia_ctime);
+ ps_iattr->ia_mtime = ps_iattr->ia_ctime;
+ }
+}
+
/**
- * kernfs_link_sibling - link kernfs_node into sibling rbtree
+ * __kernfs_link_sibling - link kernfs_node into sibling rbtree
* @kn: kernfs_node of interest
*
- * Link @kn into its sibling rbtree which starts from
- * @kn->parent->dir.children.
+ * Link @kn into its parent's children rbtree. This is the pure rbtree
+ * insertion, without the subdir/revision accounting; the caller performs
+ * that under whichever lock protects the parent's children collection.
*
* Locking:
* kernfs_rwsem held exclusive
@@ -472,7 +484,7 @@ static int kernfs_sd_compare(const struct kernfs_node *left,
* Return:
* %0 on success, -EEXIST on failure.
*/
-static int kernfs_link_sibling(struct kernfs_node *kn)
+static int __kernfs_link_sibling(struct kernfs_node *kn)
{
struct rb_node *parent = NULL;
struct kernfs_node *kn_parent;
@@ -500,7 +512,26 @@ static int kernfs_link_sibling(struct kernfs_node *kn)
rb_link_node(&kn->rb, parent, node);
rb_insert_color(&kn->rb, &kn_parent->dir.children);
+ return 0;
+}
+
+/*
+ * Locked variant of __kernfs_link_sibling(): kernfs_rwsem held exclusive; also
+ * performs the subdir count and directory-revision accounting.
+ */
+static int kernfs_link_sibling(struct kernfs_node *kn)
+{
+ struct kernfs_node *kn_parent;
+ int ret;
+
+ lockdep_assert_held_write(&kernfs_root(kn)->kernfs_rwsem);
+
+ ret = __kernfs_link_sibling(kn);
+ if (ret)
+ return ret;
+
/* successfully added, account subdir number */
+ kn_parent = kernfs_parent(kn);
down_write(&kernfs_root(kn)->kernfs_iattr_rwsem);
if (kernfs_type(kn) == KERNFS_DIR)
kn_parent->dir.subdirs++;
@@ -934,7 +965,6 @@ struct kernfs_node *kernfs_find_and_get_node_by_id(struct kernfs_root *root,
int kernfs_add_one(struct kernfs_node *kn)
{
struct kernfs_root *root = kernfs_root(kn);
- struct kernfs_iattrs *ps_iattr;
struct kernfs_node *parent;
bool has_ns;
int ret;
@@ -964,13 +994,7 @@ int kernfs_add_one(struct kernfs_node *kn)
/* Update timestamps on the parent */
down_write(&root->kernfs_iattr_rwsem);
-
- ps_iattr = parent->iattr;
- if (ps_iattr) {
- ktime_get_real_ts64(&ps_iattr->ia_ctime);
- ps_iattr->ia_mtime = ps_iattr->ia_ctime;
- }
-
+ kernfs_update_parent_times(parent);
up_write(&root->kernfs_iattr_rwsem);
/*
@@ -990,25 +1014,24 @@ int kernfs_add_one(struct kernfs_node *kn)
}
/**
- * kernfs_find_ns - find kernfs_node with the given name
+ * __kernfs_find_ns - find kernfs_node with the given name
* @parent: kernfs_node to search under
* @name: name to look for
* @ns: the namespace tag to use
*
- * Look for kernfs_node with name @name under @parent.
+ * Caller must hold a lock covering @parent's children collection:
+ * kernfs_rwsem.
*
* Return: pointer to the found kernfs_node on success, %NULL on failure.
*/
-static struct kernfs_node *kernfs_find_ns(struct kernfs_node *parent,
- const unsigned char *name,
- const struct ns_common *ns)
+static struct kernfs_node *__kernfs_find_ns(struct kernfs_node *parent,
+ const unsigned char *name,
+ const struct ns_common *ns)
{
struct rb_node *node = parent->dir.children.rb_node;
bool has_ns = kernfs_ns_enabled(parent);
unsigned int hash;
- lockdep_assert_held(&kernfs_root(parent)->kernfs_rwsem);
-
if (has_ns != (bool)ns) {
WARN(1, KERN_WARNING "kernfs: ns %s in '%s' for '%s'\n",
has_ns ? "required" : "invalid", kernfs_rcu_name(parent), name);
@@ -1032,6 +1055,18 @@ static struct kernfs_node *kernfs_find_ns(struct kernfs_node *parent,
return NULL;
}
+/*
+ * The asserted kernfs_rwsem hold (write or read) also covers the RCU-managed
+ * name dereferences in __kernfs_find_ns()'s walk.
+ */
+static struct kernfs_node *kernfs_find_ns(struct kernfs_node *parent,
+ const unsigned char *name,
+ const struct ns_common *ns)
+{
+ lockdep_assert_held(&kernfs_root(parent)->kernfs_rwsem);
+ return __kernfs_find_ns(parent, name, ns);
+}
+
static struct kernfs_node *kernfs_walk_ns(struct kernfs_node *parent,
const unsigned char *path,
const struct ns_common *ns)
@@ -1218,6 +1253,31 @@ struct kernfs_node *kernfs_root_to_node(struct kernfs_root *root)
return root->kn;
}
+/*
+ * Allocate and initialize a directory node with @extra_flags OR'd into its
+ * type flags, without linking it anywhere.
+ */
+static struct kernfs_node *__kernfs_create_dir(struct kernfs_node *parent,
+ const char *name, umode_t mode,
+ kuid_t uid, kgid_t gid,
+ void *priv,
+ const struct ns_common *ns,
+ unsigned int extra_flags)
+{
+ struct kernfs_node *kn;
+
+ kn = kernfs_new_node(parent, name, mode | S_IFDIR, uid, gid,
+ KERNFS_DIR | extra_flags);
+ if (!kn)
+ return ERR_PTR(-ENOMEM);
+
+ kn->dir.root = parent->dir.root;
+ kn->ns = ns;
+ kn->priv = priv;
+
+ return kn;
+}
+
/**
* kernfs_create_dir_ns - create a directory
* @parent: parent in which to create a new directory
@@ -1240,14 +1300,9 @@ struct kernfs_node *kernfs_create_dir_ns(struct kernfs_node *parent,
int rc;
/* allocate */
- kn = kernfs_new_node(parent, name, mode | S_IFDIR,
- uid, gid, KERNFS_DIR);
- if (!kn)
- return ERR_PTR(-ENOMEM);
-
- kn->dir.root = parent->dir.root;
- kn->ns = ns;
- kn->priv = priv;
+ kn = __kernfs_create_dir(parent, name, mode, uid, gid, priv, ns, 0);
+ if (IS_ERR(kn))
+ return kn;
/* link in */
rc = kernfs_add_one(kn);
@@ -1272,15 +1327,12 @@ struct kernfs_node *kernfs_create_empty_dir(struct kernfs_node *parent,
int rc;
/* allocate */
- kn = kernfs_new_node(parent, name, S_IRUGO|S_IXUGO|S_IFDIR,
- GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, KERNFS_DIR);
- if (!kn)
- return ERR_PTR(-ENOMEM);
+ kn = __kernfs_create_dir(parent, name, 0555,
+ GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, NULL, NULL, 0);
+ if (IS_ERR(kn))
+ return kn;
kn->flags |= KERNFS_EMPTY_DIR;
- kn->dir.root = parent->dir.root;
- kn->ns = NULL;
- kn->priv = NULL;
/* link in */
rc = kernfs_add_one(kn);
@@ -1706,18 +1758,12 @@ static void __kernfs_remove(struct kernfs_node *kn)
* to decide who's responsible for cleanups.
*/
if (!parent || kernfs_unlink_sibling(pos)) {
- struct kernfs_iattrs *ps_iattr =
- parent ? parent->iattr : NULL;
-
down_write(&kernfs_root(kn)->kernfs_iattr_rwsem);
kernfs_clear_inode_nlink(pos);
/* update timestamps on the parent */
- if (ps_iattr) {
- ktime_get_real_ts64(&ps_iattr->ia_ctime);
- ps_iattr->ia_mtime = ps_iattr->ia_ctime;
- }
+ kernfs_update_parent_times(parent);
up_write(&kernfs_root(kn)->kernfs_iattr_rwsem);
kernfs_put(pos);
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 2/8] kernfs: add staged directory creation and publication
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 1/8] kernfs: factor out reusable directory helpers Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 3/8] sysfs: add opt-in " Pavol Sakac
` (5 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
Each sysfs node creation takes the root's kernfs_rwsem for write to link
the node into its parent's children rbtree and activate it, so mass
registration against the single sysfs root serializes all of them on one
lock. KERNFS_ROOT_CREATE_DEACTIVATED already flips a finished subtree
visible atomically, but its nodes are still linked under the root
write-lock as they are created, so the per-node traffic that is the
actual cost remains.
Add a staged mode that removes it. A staged directory is fully
initialized but neither linked into its parent's children rbtree nor
activated, so only the creator's saved pointer (for sysfs, kobj->sd)
reaches it, and the kernfs children-collection operations that can reach
one take a per-staged-subtree mutex hashed by the staged-top node address
in place of the per-root rwsems. kernfs_publish() then performs the
single staged-to-visible transition under kernfs_rwsem, dropping
per-device root-lock cost from one write acquisition per node to one
write hold for the whole subtree. struct kernfs_node does not grow:
the mutex array reuses the existing kernfs_global_locks node_mutex
idiom.
The funnel covers the operations that reach a staged subtree through the
kernfs creation and removal APIs. kernfs_setattr() takes
kernfs_iattr_rwsem and is not serialized against them, so a staged
node's attributes must be mutated only through the funnel.
kernfs_node::flags is an unsigned short whose plain bits are exhausted,
and widening it would grow every node by 8 bytes, so the
KERNFS_STAGED_TOP marker aliases the FILE-only KERNFS_HAS_MMAP under a
DIR-only discipline: staged code touches the bit only on directories,
and the parent-chain climb that locates a top checks the node type
before reading it, so a staged mmap file's HAS_MMAP is never disturbed.
The pre-existing flags updates a linked node can receive (activation,
visibility toggling) become marked writes for the same reason.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
fs/kernfs/dir.c | 447 +++++++++++++++++++++++++++++++++++++++--
fs/kernfs/mount.c | 4 +-
include/linux/kernfs.h | 46 +++++
3 files changed, 474 insertions(+), 23 deletions(-)
diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
index a6290f94139c..a0f0db82ef3f 100644
--- a/fs/kernfs/dir.c
+++ b/fs/kernfs/dir.c
@@ -479,7 +479,9 @@ static void kernfs_update_parent_times(struct kernfs_node *parent)
* that under whichever lock protects the parent's children collection.
*
* Locking:
- * kernfs_rwsem held exclusive
+ * kernfs_rwsem held exclusive, or -- for a staged @kn -- the staged
+ * subtree mutex inside an RCU read section, which is what the ->__parent
+ * and ->name dereferences below require (see kernfs_staged_lock()).
*
* Return:
* %0 on success, -EEXIST on failure.
@@ -573,6 +575,171 @@ static bool kernfs_unlink_sibling(struct kernfs_node *kn)
return true;
}
+/* see staged_mutex in struct kernfs_global_locks */
+static inline struct mutex *kernfs_staged_mutex_ptr(struct kernfs_node *kn)
+{
+ return &kernfs_locks->staged_mutex[hash_ptr(kn, NR_KERNFS_LOCK_BITS)];
+}
+
+/* STAGED_TOP aliases a FILE-only bit; clear it only on directories */
+static unsigned short kernfs_staged_clear_mask(struct kernfs_node *kn)
+{
+ return KERNFS_STAGED |
+ (kernfs_type(kn) == KERNFS_DIR ? KERNFS_STAGED_TOP : 0);
+}
+
+/*
+ * kernfs_parent()'s lockdep conditions cannot express the runtime-selected
+ * staged subtree mutex, so the RCU read section is what makes the
+ * ->__parent dereference legal.
+ *
+ * The value is stable, not merely valid: __kernfs_new_node() holds a
+ * counted parent reference that lives as long as @kn, and a staged node is
+ * never activated, so kernfs_rename_ns() rejects it with -ENOENT before it
+ * can reach the ->__parent reassignment.
+ */
+static struct kernfs_node *kernfs_staged_parent(const struct kernfs_node *kn)
+{
+ struct kernfs_node *parent;
+
+ rcu_read_lock();
+ parent = rcu_dereference(kn->__parent);
+ rcu_read_unlock();
+
+ return parent;
+}
+
+/**
+ * kernfs_staged_lock - find and lock the subtree mutex for a staged node
+ * @parent: a node believed to be in a staged subtree
+ *
+ * Return the locked subtree mutex L(top) of @parent's staged top, or NULL if
+ * the subtree is (or became) published, in which case the caller uses the
+ * kernfs_rwsem path.
+ *
+ * Top-ness is one flag, KERNFS_STAGED_TOP: set once at staged creation,
+ * mutated only under L(top) at publish/teardown. The climb is lock-free
+ * and advisory; the re-verify under L(top) is a single read of STAGED_TOP,
+ * fresh because the bit is mutated only under the lock now held, so a
+ * stale walk cannot confirm a published or wrong-subtree node. Publication
+ * and teardown are one-way, so retries terminate. The climb needs no lock:
+ * the caller's reference on @parent pins its ancestors and staged nodes
+ * never move (see kernfs_staged_parent()).
+ *
+ * Context: May sleep. Returns with the returned mutex HELD; the caller
+ * releases it with mutex_unlock(). Not sparse-annotated: the acquisition is
+ * conditional and the lock runtime-selected, which __acquires() cannot say.
+ */
+static struct mutex *kernfs_staged_lock(struct kernfs_node *parent)
+{
+ for (;;) {
+ struct kernfs_node *top = parent;
+ struct mutex *lock;
+
+ for (;;) {
+ /* advisory; authoritative re-check under L(top) */
+ unsigned short flags = READ_ONCE(top->flags);
+
+ if (!(flags & KERNFS_STAGED))
+ return NULL;
+ /* STAGED_TOP is DIR-only (see its definition) */
+ if ((flags & KERNFS_TYPE_MASK) == KERNFS_DIR &&
+ (flags & KERNFS_STAGED_TOP))
+ break;
+ /* staged interior: climb */
+ top = kernfs_staged_parent(top);
+ }
+
+ lock = kernfs_staged_mutex_ptr(top);
+ mutex_lock(lock);
+ /*
+ * READ_ONCE: after publication the eager path writes other
+ * bits of this word.
+ */
+ if (READ_ONCE(top->flags) & KERNFS_STAGED_TOP)
+ return lock; /* still staged; L(top) is correct */
+ mutex_unlock(lock);
+ /* published or torn down under us; retry from @parent */
+ }
+}
+
+/*
+ * Caller holds the lock covering @parent's children collection:
+ * kernfs_rwsem or the staged subtree mutex.
+ */
+static int kernfs_add_precheck(struct kernfs_node *parent,
+ struct kernfs_node *kn)
+{
+ bool has_ns = kernfs_ns_enabled(parent);
+
+ if (has_ns != (bool)kn->ns) {
+ rcu_read_lock();
+ WARN(1, "kernfs: ns %s in '%s' for '%s'\n",
+ has_ns ? "required" : "invalid",
+ kernfs_rcu_name(parent), kernfs_rcu_name(kn));
+ rcu_read_unlock();
+ return -EINVAL;
+ }
+
+ if (kernfs_type(parent) != KERNFS_DIR)
+ return -EINVAL;
+
+ if (parent->flags & (KERNFS_REMOVING | KERNFS_EMPTY_DIR))
+ return -ENOENT;
+
+ return 0;
+}
+
+/**
+ * kernfs_add_one_staged - add @kn under a staged parent
+ * @kn: kernfs_node to add (parent already set and staged)
+ * @lock: the staged subtree mutex returned by kernfs_staged_lock(), held
+ *
+ * Links @kn into the staged parent without the per-root rwsems. @kn is
+ * marked staged so its own children funnel here too, and is left inactive;
+ * publication activates the whole subtree at once.
+ *
+ * Return: %0 on success, -errno on failure.
+ */
+static int kernfs_add_one_staged(struct kernfs_node *kn, struct mutex *lock)
+{
+ struct kernfs_node *parent = kernfs_staged_parent(kn);
+ int ret;
+
+ lockdep_assert_held(lock);
+
+ ret = kernfs_add_precheck(parent, kn);
+ if (ret)
+ return ret;
+
+ /*
+ * The RCU read section covers the RCU-managed name and parent
+ * dereferences here and inside the rbtree walk; the caller's subtree
+ * mutex is what serializes them. Neither can change under it: a
+ * staged node is unreachable, so it cannot be renamed or reparented.
+ */
+ rcu_read_lock();
+ kn->hash = kernfs_name_hash(kernfs_rcu_name(kn), kn->ns);
+ ret = __kernfs_link_sibling(kn);
+ rcu_read_unlock();
+ if (ret)
+ return ret;
+
+ /*
+ * The caller's subtree mutex serializes this parent's children, so
+ * the subdir/revision/timestamp accounting eager kernfs_add_one()
+ * does under kernfs_iattr_rwsem is done here without it.
+ */
+ if (kernfs_type(kn) == KERNFS_DIR)
+ parent->dir.subdirs++;
+ kernfs_inc_rev(parent);
+ kernfs_update_parent_times(parent);
+
+ /* marked store: racing advisory reads in kernfs_staged_lock() */
+ WRITE_ONCE(kn->flags, kn->flags | KERNFS_STAGED);
+ return 0;
+}
+
/**
* kernfs_get_active - get an active reference to kernfs_node
* @kn: kernfs_node to get an active reference to
@@ -966,24 +1133,25 @@ int kernfs_add_one(struct kernfs_node *kn)
{
struct kernfs_root *root = kernfs_root(kn);
struct kernfs_node *parent;
- bool has_ns;
+ struct mutex *lock;
int ret;
+ /*
+ * Staged parent: link under its subtree mutex, off the per-root
+ * rwsems. NULL once published, then the locked path below runs.
+ */
+ lock = kernfs_staged_lock(kernfs_staged_parent(kn));
+ if (lock) {
+ ret = kernfs_add_one_staged(kn, lock);
+ mutex_unlock(lock);
+ return ret;
+ }
+
down_write(&root->kernfs_rwsem);
parent = kernfs_parent(kn);
- ret = -EINVAL;
- has_ns = kernfs_ns_enabled(parent);
- if (WARN(has_ns != (bool)kn->ns, KERN_WARNING "kernfs: ns %s in '%s' for '%s'\n",
- has_ns ? "required" : "invalid",
- kernfs_rcu_name(parent), kernfs_rcu_name(kn)))
- goto out_unlock;
-
- if (kernfs_type(parent) != KERNFS_DIR)
- goto out_unlock;
-
- ret = -ENOENT;
- if (parent->flags & (KERNFS_REMOVING | KERNFS_EMPTY_DIR))
+ ret = kernfs_add_precheck(parent, kn);
+ if (ret)
goto out_unlock;
kn->hash = kernfs_name_hash(kernfs_rcu_name(kn), kn->ns);
@@ -1020,7 +1188,8 @@ int kernfs_add_one(struct kernfs_node *kn)
* @ns: the namespace tag to use
*
* Caller must hold a lock covering @parent's children collection:
- * kernfs_rwsem.
+ * kernfs_rwsem, or -- for a staged @parent -- the staged subtree mutex inside
+ * an RCU read section (the rbtree walk dereferences RCU-managed names).
*
* Return: pointer to the found kernfs_node on success, %NULL on failure.
*/
@@ -1114,8 +1283,21 @@ struct kernfs_node *kernfs_find_and_get_ns(struct kernfs_node *parent,
const struct ns_common *ns)
{
struct kernfs_node *kn;
- struct kernfs_root *root = kernfs_root(parent);
+ struct kernfs_root *root;
+ struct mutex *lock;
+
+ /* staged parent: serialize on its subtree mutex; NULL once published */
+ lock = kernfs_staged_lock(parent);
+ if (lock) {
+ rcu_read_lock();
+ kn = __kernfs_find_ns(parent, name, ns);
+ rcu_read_unlock();
+ kernfs_get(kn);
+ mutex_unlock(lock);
+ return kn;
+ }
+ root = kernfs_root(parent);
down_read(&root->kernfs_rwsem);
kn = kernfs_find_ns(parent, name, ns);
kernfs_get(kn);
@@ -1313,6 +1495,41 @@ struct kernfs_node *kernfs_create_dir_ns(struct kernfs_node *parent,
return ERR_PTR(rc);
}
+/**
+ * kernfs_create_dir_ns_staged - create a staged directory
+ * @parent: live parent in which the directory will eventually appear
+ * @name: name of the new directory
+ * @mode: mode of the new directory
+ * @uid: uid of the new directory
+ * @gid: gid of the new directory
+ * @priv: opaque data associated with the new directory
+ * @ns: optional namespace tag of the directory
+ *
+ * Create a directory node that is fully initialized, with @parent set and a
+ * parent reference taken, but NOT linked into @parent's children collection
+ * and NOT activated: it is invisible to lookup, readdir and the dcache. The
+ * KERNFS_STAGED bit is set here, before the node is reachable. A caller
+ * that exposes the returned pointer to lock-free readers must publish it
+ * with release semantics so these init stores are visible first. While
+ * staged, the subtree is mutated only through the kernfs creation and
+ * removal APIs, which serialize on the subtree mutex; kernfs_setattr()
+ * and other per-root-lock paths are not serialized against them. The
+ * subtree built beneath it through the unchanged creation APIs stays staged
+ * until kernfs_publish() links it into @parent in one transaction; see
+ * there for the activation policy.
+ *
+ * Return: the created node on success, ERR_PTR() value on failure.
+ */
+struct kernfs_node *kernfs_create_dir_ns_staged(struct kernfs_node *parent,
+ const char *name, umode_t mode,
+ kuid_t uid, kgid_t gid,
+ void *priv,
+ const struct ns_common *ns)
+{
+ return __kernfs_create_dir(parent, name, mode, uid, gid, priv, ns,
+ KERNFS_STAGED | KERNFS_STAGED_TOP);
+}
+
/**
* kernfs_create_empty_dir - create an always empty directory
* @parent: parent in which to create a new directory
@@ -1605,7 +1822,8 @@ static void kernfs_activate_one(struct kernfs_node *kn)
{
lockdep_assert_held_write(&kernfs_root(kn)->kernfs_rwsem);
- kn->flags |= KERNFS_ACTIVATED;
+ /* flags is read locklessly by the staged funnel; mark its racers */
+ WRITE_ONCE(kn->flags, kn->flags | KERNFS_ACTIVATED);
if (kernfs_active(kn) || (kn->flags & (KERNFS_HIDDEN | KERNFS_REMOVING)))
return;
@@ -1643,6 +1861,99 @@ void kernfs_activate(struct kernfs_node *kn)
up_write(&root->kernfs_rwsem);
}
+/**
+ * kernfs_publish - make a staged subtree visible
+ * @kn: staged top (as returned by kernfs_create_dir_ns_staged())
+ *
+ * Under the per-root kernfs_rwsem (held write) and the subtree mutex, in
+ * this order: verify the parent is still live and the name does not collide
+ * with a live sibling, link @kn into the parent's children collection
+ * (bumping the directory revision that invalidates negative dentries), then
+ * walk the subtree clearing KERNFS_STAGED and activating every node.
+ *
+ * Readers see the transition via lock pairing -- kernfs_rwsem for VFS readers,
+ * L(top) re-verification for funnel entrants; see kernfs_staged_lock(). On
+ * success the directory is indistinguishable from one built eagerly.
+ *
+ * On a %KERNFS_ROOT_CREATE_DEACTIVATED root the subtree is linked and no longer
+ * staged, but left deactivated, matching kernfs_add_one(); the caller makes it
+ * visible with kernfs_activate().
+ *
+ * @kn must still be staged, and its parent must not itself be staged (a staged
+ * parent's children are serialized by a different subtree mutex); no in-tree
+ * caller does either.
+ *
+ * Return: %0 on success, -EEXIST on a name collision, -ENOENT if the parent is
+ * gone, or -EINVAL on misuse (WARN), including publishing a node that is not
+ * staged (already published or torn down). On failure the subtree stays staged
+ * and tear-downable.
+ */
+int kernfs_publish(struct kernfs_node *kn)
+{
+ struct kernfs_node *parent = kernfs_staged_parent(kn);
+ struct kernfs_root *root = kernfs_root(kn);
+ struct kernfs_node *pos;
+ struct mutex *lock;
+ bool activate;
+ int ret;
+
+ /* Unlocked pre-check; the locked re-check below is authoritative. */
+ if (WARN_ON_ONCE(!parent || (data_race(parent->flags) & KERNFS_STAGED)))
+ return -EINVAL;
+
+ /* Unlocked pre-check, as above. */
+ if (WARN_ON_ONCE(!(data_race(kn->flags) & KERNFS_STAGED)))
+ return -EINVAL;
+
+ activate = !(root->flags & KERNFS_ROOT_CREATE_DEACTIVATED);
+ lock = kernfs_staged_mutex_ptr(kn);
+
+ down_write(&root->kernfs_rwsem);
+ mutex_lock(lock);
+
+ /* authoritative re-check: the advisory reads above can race */
+ ret = -EINVAL;
+ if (WARN_ON_ONCE(!(READ_ONCE(kn->flags) & KERNFS_STAGED)))
+ goto out;
+
+ ret = -ENOENT;
+ if (parent->flags & (KERNFS_REMOVING | KERNFS_EMPTY_DIR))
+ goto out;
+
+ /* the write hold covers the RCU-managed name from here on */
+ kn->hash = kernfs_name_hash(kernfs_rcu_name(kn), kn->ns);
+
+ ret = kernfs_link_sibling(kn);
+ if (ret) /* -EEXIST on a live-sibling name collision */
+ goto out;
+
+ /* mirror kernfs_add_one(): bump the parent's timestamps */
+ down_write(&root->kernfs_iattr_rwsem);
+ kernfs_update_parent_times(parent);
+ up_write(&root->kernfs_iattr_rwsem);
+
+ /*
+ * @kn is now linked; clear staged and activate every node. A walker
+ * that observes a cleared bit therefore finds the tree already linked,
+ * so its rwsem fallback is correct. WRITE_ONCE: races advisory reads.
+ * Clearing is unconditional -- the funnel in kernfs_staged_lock() must
+ * terminate here whatever the root's activation policy is -- while
+ * activation follows that policy, as in kernfs_add_one().
+ */
+ pos = NULL;
+ while ((pos = kernfs_next_descendant_post(pos, kn))) {
+ WRITE_ONCE(pos->flags,
+ pos->flags & ~kernfs_staged_clear_mask(pos));
+ if (activate)
+ kernfs_activate_one(pos);
+ }
+ ret = 0;
+out:
+ mutex_unlock(lock);
+ up_write(&root->kernfs_rwsem);
+ return ret;
+}
+
/**
* kernfs_show - show or hide a node
* @kn: kernfs_node to show or hide
@@ -1665,11 +1976,11 @@ void kernfs_show(struct kernfs_node *kn, bool show)
down_write(&root->kernfs_rwsem);
if (show) {
- kn->flags &= ~KERNFS_HIDDEN;
+ WRITE_ONCE(kn->flags, kn->flags & ~KERNFS_HIDDEN);
if (kn->flags & KERNFS_ACTIVATED)
kernfs_activate_one(kn);
} else {
- kn->flags |= KERNFS_HIDDEN;
+ WRITE_ONCE(kn->flags, kn->flags | KERNFS_HIDDEN);
if (kernfs_active(kn))
atomic_add(KN_DEACTIVATED_BIAS, &kn->active);
kernfs_drain(kn, false);
@@ -1709,6 +2020,70 @@ static void kernfs_clear_inode_nlink(struct kernfs_node *kn)
}
}
+/**
+ * kernfs_remove_staged - tear down a staged subtree rooted at @kn
+ * @kn: staged node (a staged top, or an interior node being removed
+ * individually during the window)
+ * @lock: the staged-subtree mutex the caller holds
+ *
+ * Every node in a staged subtree is inactive (KN_DEACTIVATED_BIAS) and
+ * unreachable by userspace, so there is nothing to drain and kernfs_rwsem is
+ * not required. For the same reason no inode can exist for any of these
+ * nodes -- an inode is instantiated only through a lookup, which cannot reach
+ * an unlinked, inactive node -- so unlike __kernfs_remove() this needs
+ * neither kernfs_supers_rwsem nor kernfs_clear_inode_nlink().
+ *
+ * This mirrors __kernfs_remove()'s per-node reference drop for eager parity,
+ * and additionally drops the base reference of a never-linked staged top,
+ * which the eager path would leak: __kernfs_remove() short-circuits on an
+ * unlinked node with a parent, and kernfs_unlink_sibling() returns false
+ * for it.
+ */
+static void kernfs_remove_staged(struct kernfs_node *kn, struct mutex *lock)
+{
+ struct kernfs_node *pos;
+
+ lockdep_assert_held(lock);
+
+ do {
+ struct kernfs_node *parent;
+
+ pos = kernfs_leftmost_descendant(kn);
+ kernfs_get(pos);
+ parent = kernfs_staged_parent(pos);
+
+ /*
+ * Clear STAGED and set REMOVING; a funnel entrant that lost
+ * the L(top) race then hits kernfs_add_one()'s REMOVING
+ * check on the rwsem path instead of leaking into this dead
+ * subtree. The mask clears STAGED_TOP only on directories
+ * (see its definition). WRITE_ONCE: the store races the
+ * advisory reads in kernfs_staged_lock().
+ */
+ WRITE_ONCE(pos->flags,
+ (pos->flags & ~kernfs_staged_clear_mask(pos)) |
+ KERNFS_REMOVING);
+
+ /* linked node: unlink from its staged parent's rbtree */
+ if (parent && !RB_EMPTY_NODE(&pos->rb)) {
+ if (kernfs_type(pos) == KERNFS_DIR)
+ parent->dir.subdirs--;
+ kernfs_inc_rev(parent);
+ /*
+ * Parent time parity with eager __kernfs_remove();
+ * owner-serialized by the subtree mutex, so no
+ * kernfs_iattr_rwsem (as on the staged add side).
+ */
+ kernfs_update_parent_times(parent);
+ rb_erase(&pos->rb, &parent->dir.children);
+ RB_CLEAR_NODE(&pos->rb);
+ }
+
+ kernfs_put(pos); /* base ref (__kernfs_remove parity) */
+ kernfs_put(pos); /* protective ref; free drops parent */
+ } while (pos != kn);
+}
+
static void __kernfs_remove(struct kernfs_node *kn)
{
struct kernfs_node *pos, *parent;
@@ -1733,7 +2108,7 @@ static void __kernfs_remove(struct kernfs_node *kn)
down_write(&kernfs_root(kn)->kernfs_iattr_rwsem);
pos = NULL;
while ((pos = kernfs_next_descendant_post(pos, kn))) {
- pos->flags |= KERNFS_REMOVING;
+ WRITE_ONCE(pos->flags, pos->flags | KERNFS_REMOVING);
if (kernfs_active(pos))
atomic_add(KN_DEACTIVATED_BIAS, &pos->active);
}
@@ -1782,10 +2157,19 @@ static void __kernfs_remove(struct kernfs_node *kn)
void kernfs_remove(struct kernfs_node *kn)
{
struct kernfs_root *root;
+ struct mutex *lock;
if (!kn)
return;
+ /* staged subtree: tear down under its mutex; NULL once published */
+ lock = kernfs_staged_lock(kn);
+ if (lock) {
+ kernfs_remove_staged(kn, lock);
+ mutex_unlock(lock);
+ return;
+ }
+
root = kernfs_root(kn);
down_read(&root->kernfs_supers_rwsem);
@@ -1897,9 +2281,9 @@ bool kernfs_remove_self(struct kernfs_node *kn)
* instance of kernfs_remove_self() finished.
*/
if (!(kn->flags & KERNFS_SUICIDAL)) {
- kn->flags |= KERNFS_SUICIDAL;
+ WRITE_ONCE(kn->flags, kn->flags | KERNFS_SUICIDAL);
__kernfs_remove(kn);
- kn->flags |= KERNFS_SUICIDED;
+ WRITE_ONCE(kn->flags, kn->flags | KERNFS_SUICIDED);
ret = true;
} else {
wait_queue_head_t *waitq = &kernfs_root(kn)->deactivate_waitq;
@@ -1949,6 +2333,7 @@ int kernfs_remove_by_name_ns(struct kernfs_node *parent, const char *name,
{
struct kernfs_node *kn;
struct kernfs_root *root;
+ struct mutex *lock;
if (!parent) {
WARN(1, KERN_WARNING "kernfs: can not remove '%s', no directory\n",
@@ -1956,6 +2341,24 @@ int kernfs_remove_by_name_ns(struct kernfs_node *parent, const char *name,
return -ENOENT;
}
+ /* staged parent: serialize on its subtree mutex; NULL once published */
+ lock = kernfs_staged_lock(parent);
+ if (lock) {
+ bool found;
+
+ rcu_read_lock();
+ kn = __kernfs_find_ns(parent, name, ns);
+ rcu_read_unlock();
+ found = kn;
+ if (kn) {
+ kernfs_get(kn);
+ kernfs_remove_staged(kn, lock);
+ kernfs_put(kn);
+ }
+ mutex_unlock(lock);
+ return found ? 0 : -ENOENT;
+ }
+
root = kernfs_root(parent);
down_read(&root->kernfs_supers_rwsem);
down_write(&root->kernfs_rwsem);
diff --git a/fs/kernfs/mount.c b/fs/kernfs/mount.c
index f183a96778b9..19b1d52ab966 100644
--- a/fs/kernfs/mount.c
+++ b/fs/kernfs/mount.c
@@ -445,8 +445,10 @@ static void __init kernfs_mutex_init(void)
{
int count;
- for (count = 0; count < NR_KERNFS_LOCKS; count++)
+ for (count = 0; count < NR_KERNFS_LOCKS; count++) {
mutex_init(&kernfs_locks->node_mutex[count]);
+ mutex_init(&kernfs_locks->staged_mutex[count]);
+ }
}
static void __init kernfs_lock_init(void)
diff --git a/include/linux/kernfs.h b/include/linux/kernfs.h
index 6440882b7d58..b3e574047a90 100644
--- a/include/linux/kernfs.h
+++ b/include/linux/kernfs.h
@@ -95,6 +95,15 @@ struct kernfs_iattrs;
*/
struct kernfs_global_locks {
struct mutex node_mutex[NR_KERNFS_LOCKS];
+
+ /*
+ * Hashed by the staged-top kernfs_node address. kernfs operations on
+ * a staged directory's children collection take the matching mutex in
+ * place of the per-root kernfs_rwsem / kernfs_iattr_rwsem, so mass
+ * creation of staged subtrees does not serialize on one root's locks.
+ * This is a second instance of the node_mutex idiom above.
+ */
+ struct mutex staged_mutex[NR_KERNFS_LOCKS];
};
enum kernfs_node_type {
@@ -118,6 +127,28 @@ enum kernfs_node_flag {
KERNFS_EMPTY_DIR = 0x1000,
KERNFS_HAS_RELEASE = 0x2000,
KERNFS_REMOVING = 0x4000,
+ /*
+ * Under-construction directory subtree: initialized and reachable
+ * only via the creator's saved pointer, not linked into its parent
+ * and not activated, so it is invisible to lookup/readdir/dcache.
+ * Set at staged creation, cleared at kernfs_publish()/teardown.
+ * Set on every node of the subtree.
+ */
+ KERNFS_STAGED = 0x8000,
+ /*
+ * Marks the single top of a staged subtree (never an interior), so
+ * kernfs_staged_lock() identifies the owning subtree mutex from one
+ * location, and its re-verify under L(top) is fresh (the bit is
+ * mutated only there).
+ *
+ * kernfs_node::flags is an unsigned short with its plain bits
+ * exhausted at 0x8000, so this aliases KERNFS_HAS_MMAP under a
+ * DIR-only discipline: on a KERNFS_FILE node the bit always means
+ * HAS_MMAP; staged code sets, clears and tests STAGED_TOP only on
+ * KERNFS_DIR nodes (a staged top is always a directory), so a staged
+ * mmap file's HAS_MMAP is never disturbed.
+ */
+ KERNFS_STAGED_TOP = KERNFS_HAS_MMAP,
};
/* @flags for kernfs_create_root() */
@@ -447,6 +478,12 @@ struct kernfs_node *kernfs_create_dir_ns(struct kernfs_node *parent,
kuid_t uid, kgid_t gid,
void *priv,
const struct ns_common *ns);
+struct kernfs_node *kernfs_create_dir_ns_staged(struct kernfs_node *parent,
+ const char *name, umode_t mode,
+ kuid_t uid, kgid_t gid,
+ void *priv,
+ const struct ns_common *ns);
+int kernfs_publish(struct kernfs_node *kn);
struct kernfs_node *kernfs_create_empty_dir(struct kernfs_node *parent,
const char *name);
struct kernfs_node *__kernfs_create_file(struct kernfs_node *parent,
@@ -550,6 +587,15 @@ kernfs_create_dir_ns(struct kernfs_node *parent, const char *name,
void *priv, const struct ns_common *ns)
{ return ERR_PTR(-ENOSYS); }
+static inline struct kernfs_node *
+kernfs_create_dir_ns_staged(struct kernfs_node *parent, const char *name,
+ umode_t mode, kuid_t uid, kgid_t gid,
+ void *priv, const struct ns_common *ns)
+{ return ERR_PTR(-ENOSYS); }
+
+static inline int kernfs_publish(struct kernfs_node *kn)
+{ return -ENOSYS; }
+
static inline struct kernfs_node *
__kernfs_create_file(struct kernfs_node *parent, const char *name,
umode_t mode, kuid_t uid, kgid_t gid,
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 3/8] sysfs: add opt-in staged directory creation and publication
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 1/8] kernfs: factor out reusable directory helpers Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 2/8] kernfs: add staged directory creation and publication Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 4/8] driver core: Register opted-in devices through the staged sysfs path Pavol Sakac
` (4 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
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@amazon.de>
---
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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 4/8] driver core: Register opted-in devices through the staged sysfs path
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
` (2 preceding siblings ...)
2026-09-11 17:43 ` [RFC PATCH 3/8] sysfs: add opt-in " Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration Pavol Sakac
` (3 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
dev_set_sysfs_staged() opts a device in to staged registration before
device_add(). struct device gains one flag, DEV_FLAG_SYSFS_STAGED, in
the existing flags bitmap, so the struct does not grow.
device_add() stays a single code path with a staged bracket: it arms
staged creation once the parent kobject is resolved, everything it builds
in between lands in the staged subtree through unchanged code, and after
device_pm_add() sysfs_publish_dir() makes the whole subtree visible in
one step, before anything announces the device. bus_add_device() defers
its bus-klist insertion until just after publication, because
driver_attach() walks that klist and must not see a not-yet-published
device.
A class device with no parent lands under a class glue directory and
needs no special casing: cleanup_glue_dir() cannot see a staged child
through kobject_has_children(), but since commit ac43432cb1f5 ("driver
core: Fix use-after-free and double free on glue directory") that gate
also requires the caller's reference to be the last one, and a staged
device holds two.
Two transients are new and bounded. A symlink pointing at this device
from an already-published directory resolves -ENOENT until publication,
and an eager device_add() nested under the still-staged device fires its
KOBJ_ADD before its own path resolves. Neither opt-in reaches that
second case; code the opt-in caller does not control can, through the
two wakeup-source routes named in the uevent-deferral patch. Both close
at publication, before the ancestor's own KOBJ_ADD. The symlink
transient announces nothing; the nested child's KOBJ_ADD does announce
a path that does not yet resolve, and a later commit defers and replays
exactly these events. Opt-in is therefore appropriate for devices
whose nested children, if any, tolerate being fully registered,
enumerable and bindable, while their sysfs tree is not yet visible.
Publish-side code reads the kobject's sd_staged snapshot rather than this
flag, which is free to change once device_add() has sampled it.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
drivers/base/base.h | 1 +
drivers/base/bus.c | 29 ++++++++++++++++++++++++++++-
drivers/base/core.c | 27 +++++++++++++++++++++++++++
include/linux/device.h | 11 +++++++++++
4 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/drivers/base/base.h b/drivers/base/base.h
index f5d608f4aaa5..5c8266dad7f8 100644
--- a/drivers/base/base.h
+++ b/drivers/base/base.h
@@ -157,6 +157,7 @@ static inline void auxiliary_bus_init(void) { }
struct kobject *virtual_device_parent(void);
int bus_add_device(struct device *dev);
+void bus_add_device_publish(struct device *dev);
void bus_probe_device(struct device *dev);
void bus_remove_device(struct device *dev);
void bus_notify(struct device *dev, enum bus_notifier_event value);
diff --git a/drivers/base/bus.c b/drivers/base/bus.c
index d17bd91490ee..f5af3b1e2ee3 100644
--- a/drivers/base/bus.c
+++ b/drivers/base/bus.c
@@ -588,7 +588,16 @@ int bus_add_device(struct device *dev)
if (error)
goto out_subsys;
- klist_add_tail(&dev->p->knode_bus, &sp->klist_devices);
+ /*
+ * A staged device must not appear on the bus klist (walked by
+ * driver_attach()) until it has been published, or a driver could bind
+ * to a not-yet-visible device. Defer the insertion to
+ * bus_add_device_publish(). The sp reference taken above is held until
+ * device removal in both cases, and bus_remove_device() already
+ * tolerates a never-inserted knode_bus via klist_node_attached().
+ */
+ if (!kobject_sd_staged(&dev->kobj))
+ klist_add_tail(&dev->p->knode_bus, &sp->klist_devices);
return 0;
out_subsys:
@@ -603,6 +612,24 @@ int bus_add_device(struct device *dev)
return error;
}
+/**
+ * bus_add_device_publish - finish bus registration deferred past publication
+ * @dev: the now-published staged device
+ *
+ * Performs the bus klist insertion bus_add_device() deferred for a staged
+ * device; see the comment there.
+ */
+void bus_add_device_publish(struct device *dev)
+{
+ struct subsys_private *sp = bus_to_subsys(dev->bus);
+
+ if (!sp)
+ return;
+
+ klist_add_tail(&dev->p->knode_bus, &sp->klist_devices);
+ subsys_put(sp);
+}
+
/**
* bus_probe_device - probe drivers for a new device
* @dev: device to probe
diff --git a/drivers/base/core.c b/drivers/base/core.c
index 5dea641cbdb6..caba5610a04d 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -3740,6 +3740,14 @@ int device_add(struct device *dev)
if (kobj)
dev->kobj.parent = kobj;
+ /*
+ * Glue-dir parents are safe: cleanup_glue_dir() only reaps a glue
+ * dir whose kref is 1, and this device holds a glue-dir reference
+ * from get_device_parent() for the whole staged window. Assigned
+ * unconditionally so a reused kobject cannot carry a stale bit.
+ */
+ kobject_set_sd_staged(&dev->kobj, dev_sysfs_staged(dev));
+
/* use parent numa_node */
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));
@@ -3773,6 +3781,25 @@ int device_add(struct device *dev)
goto DPMError;
device_pm_add(dev);
+ /*
+ * Publish the staged directory before anything makes the device
+ * observable: the /sys/dev entry, the devtmpfs node,
+ * BUS_NOTIFY_ADD_DEVICE, the KOBJ_ADD uevent and driver probing all
+ * follow. Once published, the deferred bus klist insertion puts the
+ * device where driver_attach() can see it. (The outside-in symlinks
+ * whose target is this device -- class/bus/ACPI -- are created earlier
+ * and resolve only at publication.) Children added by notify hooks
+ * inside the staged window were created staged-interior and are
+ * published together with this device; their uevents may precede
+ * their sysfs visibility.
+ */
+ if (kobject_sd_staged(&dev->kobj)) {
+ error = sysfs_publish_dir(&dev->kobj);
+ if (error)
+ goto DevAttrError;
+ bus_add_device_publish(dev);
+ }
+
if (MAJOR(dev->devt)) {
error = device_create_file(dev, &dev_attr_dev);
if (error)
diff --git a/include/linux/device.h b/include/linux/device.h
index aee79fd6b32b..74701a8aa9d2 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -599,6 +599,15 @@ struct device_physical_location {
* ancestor device.
* @DEV_FLAG_OFFLINE_DISABLED: If set, the device is permanently online.
* @DEV_FLAG_OFFLINE: Set after successful invocation of bus type's .offline().
+ * @DEV_FLAG_SYSFS_STAGED: Opt in to staged sysfs registration. device_add()
+ * then builds the device's sysfs directory and all content added
+ * before the publication point invisibly and off the sysfs root
+ * lock, and publishes it in one step before the device becomes
+ * observable to userspace or to drivers. The per-device
+ * sysfs-root lock cost stops scaling with the number of nodes in
+ * the directory, which matters when many devices (for example
+ * SR-IOV virtual functions) are registered in parallel.
+ * Must be set before device_add().
* @DEV_FLAG_COUNT: Number of defined struct_device_flags.
*/
enum struct_device_flags {
@@ -612,6 +621,7 @@ enum struct_device_flags {
DEV_FLAG_OF_NODE_REUSED = 7,
DEV_FLAG_OFFLINE_DISABLED = 8,
DEV_FLAG_OFFLINE = 9,
+ DEV_FLAG_SYSFS_STAGED = 10,
DEV_FLAG_COUNT
};
@@ -829,6 +839,7 @@ __create_dev_flag_accessors(dma_coherent, DEV_FLAG_DMA_COHERENT);
__create_dev_flag_accessors(of_node_reused, DEV_FLAG_OF_NODE_REUSED);
__create_dev_flag_accessors(offline_disabled, DEV_FLAG_OFFLINE_DISABLED);
__create_dev_flag_accessors(offline, DEV_FLAG_OFFLINE);
+__create_dev_flag_accessors(sysfs_staged, DEV_FLAG_SYSFS_STAGED);
#undef __create_dev_flag_accessors
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
` (3 preceding siblings ...)
2026-09-11 17:43 ` [RFC PATCH 4/8] driver core: Register opted-in devices through the staged sysfs path Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-12 13:14 ` Andy Shevchenko
2026-09-11 17:43 ` [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window Pavol Sakac
` (2 subsequent siblings)
7 siblings, 1 reply; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
Add a staged_device KUnit suite under its own
CONFIG_STAGED_DEVICE_KUNIT_TEST symbol exercising the staged mechanism.
kobject_add() on a kobject with ->sd_staged builds the directory staged
but does not publish it, since only device_add() does, so the tests
drive the window directly: stage, then populate and observe, then
sysfs_publish_dir(). The cases cover publication of plain, named-group
and merged-group content, the failure modes of publication itself, abort
without publication, foreign-thread access, concurrent windows including
two that share one hashed staged mutex, glue-directory sharing, and
bus-klist membership by the time device_add() returns. The suite also
covers the KERNFS_STAGED_TOP/KERNFS_HAS_MMAP aliasing on a staged
mmap-capable bin file.
The publication-misuse cases intentionally trigger the publication
guards' WARN backtraces, and the duplicate-name cases trigger
sysfs_warn_dup() splats: those backtraces are expected output of a
passing run, not failures. The suite is therefore unsuitable for
runners that set panic_on_warn, which turns the first intentional splat
into a panic, and does not follow KUNIT_ALL_TESTS: it runs only when its
symbol is enabled explicitly.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
drivers/base/test/.kunitconfig | 1 +
drivers/base/test/Kconfig | 12 +
drivers/base/test/Makefile | 1 +
drivers/base/test/staged-device-test.c | 1042 ++++++++++++++++++++++++
4 files changed, 1056 insertions(+)
create mode 100644 drivers/base/test/staged-device-test.c
diff --git a/drivers/base/test/.kunitconfig b/drivers/base/test/.kunitconfig
index 28322bad39a7..3766972a2e84 100644
--- a/drivers/base/test/.kunitconfig
+++ b/drivers/base/test/.kunitconfig
@@ -1,3 +1,4 @@
CONFIG_KUNIT=y
CONFIG_DM_KUNIT_TEST=y
CONFIG_GLUE_DIR_KUNIT_TEST=y
+CONFIG_STAGED_DEVICE_KUNIT_TEST=y
diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 253b5bd96aff..0b32e6ca2e12 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -36,3 +36,15 @@ config GLUE_DIR_KUNIT_TEST
not a glue directory is never mistaken for one.
If unsure say N.
+
+config STAGED_DEVICE_KUNIT_TEST
+ tristate "KUnit Tests for staged device registration" if !KUNIT_ALL_TESTS
+ depends on KUNIT && SYSFS
+ help
+ Enable this option to test staged sysfs device registration and
+ publication.
+
+ Some cases intentionally exercise warning and duplicate-name paths;
+ do not run this suite with panic_on_warn.
+
+ If unsure, say N.
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index f13f0c399bea..2e6dbb56fb7b 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -3,6 +3,7 @@ obj-$(CONFIG_TEST_ASYNC_DRIVER_PROBE) += test_async_driver_probe.o
obj-$(CONFIG_DM_KUNIT_TEST) += root-device-test.o
obj-$(CONFIG_DM_KUNIT_TEST) += platform-device-test.o
+obj-$(CONFIG_STAGED_DEVICE_KUNIT_TEST) += staged-device-test.o
obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) += property-entry-test.o
CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
diff --git a/drivers/base/test/staged-device-test.c b/drivers/base/test/staged-device-test.c
new file mode 100644
index 000000000000..d32f00214a8b
--- /dev/null
+++ b/drivers/base/test/staged-device-test.c
@@ -0,0 +1,1042 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for staged sysfs directory registration.
+ *
+ * The mechanism lives below the sysfs population layer: a kobject opted in
+ * with ->sd_staged has its directory built staged (invisible, off the sysfs
+ * root lock) and published in one step. kobject_add() creates the staged
+ * directory but does NOT publish it (only device_add() does), which gives
+ * these tests direct control over the window: stage -> populate/observe ->
+ * sysfs_publish_dir().
+ */
+
+#include <kunit/resource.h>
+#include <kunit/test.h>
+
+#include <linux/device.h>
+#include <linux/hash.h>
+#include <linux/kobject.h>
+#include <linux/kernfs.h>
+#include <linux/kthread.h>
+#include <linux/completion.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+/* A minimal kobject type usable as a staged directory. */
+
+struct staged_kobj {
+ struct kobject kobj;
+};
+
+static ssize_t staged_attr_show(struct kobject *kobj, struct attribute *attr,
+ char *buf)
+{
+ return 0;
+}
+
+static const struct sysfs_ops staged_sysfs_ops = {
+ .show = staged_attr_show,
+};
+
+static void staged_kobj_release(struct kobject *kobj)
+{
+ /* frees the container; no caller frees a kobject-embedding struct */
+ kfree(container_of(kobj, struct staged_kobj, kobj));
+}
+
+static const struct kobj_type staged_ktype = {
+ .sysfs_ops = &staged_sysfs_ops,
+ .release = staged_kobj_release,
+};
+
+struct staged_test_priv {
+ struct kobject *parent; /* published parent for staged kids */
+};
+
+/* Is @parent's staged bit set on its kernfs node? */
+static bool sd_is_staged(struct kobject *kobj)
+{
+ return kobj->sd && (kobj->sd->flags & KERNFS_STAGED);
+}
+
+/* subdir count of a kobject's kernfs directory (parent accounting) */
+static unsigned long sd_subdirs(struct kobject *kobj)
+{
+ return kobj->sd ? kobj->sd->dir.subdirs : 0;
+}
+
+/* Does a visible child @name exist under @parent (goes through the funnel)? */
+static bool child_visible(struct kobject *parent, const char *name)
+{
+ struct kernfs_node *kn = kernfs_find_and_get(parent->sd, name);
+ bool found = !!kn;
+
+ kernfs_put(kn);
+ return found;
+}
+
+/*
+ * Allocate a staged child kobject. It is freed by staged_kobj_release() when
+ * its last reference is dropped (kobject_put()); nothing frees it directly, so
+ * the deferred release under CONFIG_DEBUG_KOBJECT_RELEASE is safe.
+ */
+static struct kobject *staged_child_alloc(struct kunit *test)
+{
+ struct staged_kobj *sk;
+
+ sk = kzalloc_obj(*sk);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sk);
+
+ kobject_init(&sk->kobj, &staged_ktype);
+ kobject_set_sd_staged(&sk->kobj, true);
+ return &sk->kobj;
+}
+
+/* Add @n plain attribute files to a (staged) kobject; names attr0..attrN-1. */
+static int staged_add_files(struct kunit *test, struct kobject *kobj, int n)
+{
+ int i, ret;
+
+ for (i = 0; i < n; i++) {
+ struct attribute *a = kunit_kzalloc(test, sizeof(*a), GFP_KERNEL);
+ char *nm;
+
+ if (!a)
+ return -ENOMEM;
+ sysfs_attr_init(a);
+ nm = kunit_kmalloc(test, 16, GFP_KERNEL);
+ if (!nm)
+ return -ENOMEM;
+ snprintf(nm, 16, "attr%d", i);
+ a->name = nm;
+ a->mode = 0644;
+ ret = sysfs_create_file(kobj, a);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int staged_test_init(struct kunit *test)
+{
+ struct staged_test_priv *priv;
+
+ priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, priv);
+
+ priv->parent = kobject_create_and_add("staged_kunit", kernel_kobj);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, priv->parent);
+
+ test->priv = priv;
+ return 0;
+}
+
+static void staged_test_exit(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+
+ if (priv && priv->parent)
+ kobject_put(priv->parent);
+}
+
+/*
+ * End-to-end publish; the merged group is the sysfs_merge_group() case,
+ * which dereferences kobj->sd while the directory is still staged.
+ */
+static struct attribute grp_attr = { .name = "grp_attr", .mode = 0644 };
+static struct attribute *named_grp_attrs[] = { &grp_attr, NULL };
+static const struct attribute_group named_grp = {
+ .name = "ngroup",
+ .attrs = named_grp_attrs,
+};
+
+static struct attribute merge_attr = { .name = "merged", .mode = 0644 };
+static struct attribute *merge_attrs[] = { &merge_attr, NULL };
+static const struct attribute_group merge_grp = {
+ .name = "ngroup",
+ .attrs = merge_attrs,
+};
+
+static void staged_test_publish_end_to_end(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *kobj = staged_child_alloc(test);
+ unsigned long parent_subdirs = sd_subdirs(priv->parent);
+ struct kernfs_node *ngroup_kn, *merged_kn;
+ int ret;
+
+ ret = kobject_add(kobj, priv->parent, "end_to_end");
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_TRUE(test, sd_is_staged(kobj));
+ KUNIT_EXPECT_FALSE(test, child_visible(priv->parent, "end_to_end"));
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), parent_subdirs);
+
+ ret = sysfs_create_file(kobj, &grp_attr);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = sysfs_create_group(kobj, &named_grp);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ret = sysfs_merge_group(kobj, &merge_grp);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ ret = sysfs_publish_dir(kobj);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(kobj));
+ KUNIT_EXPECT_TRUE(test, child_visible(priv->parent, "end_to_end"));
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), parent_subdirs + 1);
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "grp_attr"));
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "ngroup"));
+
+ /* the merged attr lives under ngroup/, not at the top level */
+ ngroup_kn = kernfs_find_and_get(kobj->sd, "ngroup");
+ KUNIT_ASSERT_NOT_NULL(test, ngroup_kn);
+ merged_kn = kernfs_find_and_get(ngroup_kn, "merged");
+ KUNIT_EXPECT_NOT_NULL(test, merged_kn);
+ kernfs_put(merged_kn);
+ kernfs_put(ngroup_kn);
+
+ sysfs_unmerge_group(kobj, &merge_grp);
+ sysfs_remove_group(kobj, &named_grp);
+ sysfs_remove_file(kobj, &grp_attr);
+ kobject_del(kobj);
+ kobject_put(kobj);
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), parent_subdirs);
+}
+
+/*
+ * Duplicate name: two staged children with the same name. The first
+ * publishes; the second fails -EEXIST at publication and tears down cleanly,
+ * leaving the winner and the parent undisturbed.
+ */
+static void staged_test_duplicate_name(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *win = staged_child_alloc(test);
+ struct kobject *lose = staged_child_alloc(test);
+ unsigned long base = sd_subdirs(priv->parent);
+ int ret;
+
+ KUNIT_ASSERT_EQ(test, kobject_add(win, priv->parent, "dup"), 0);
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(win), 0);
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base + 1);
+
+ /* second staged dir, same name, only fails at publish */
+ KUNIT_ASSERT_EQ(test, kobject_add(lose, priv->parent, "dup"), 0);
+ ret = sysfs_publish_dir(lose);
+ KUNIT_EXPECT_EQ(test, ret, -EEXIST);
+
+ /* winner undisturbed; parent gained exactly one child */
+ KUNIT_EXPECT_TRUE(test, child_visible(priv->parent, "dup"));
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base + 1);
+
+ kobject_del(lose); /* tear down the staged loser */
+ kobject_put(lose);
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base + 1);
+
+ kobject_del(win);
+ kobject_put(win);
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base);
+}
+
+/*
+ * kernfs_publish() rejects (WARN, -EINVAL) a staged top whose parent is
+ * itself still staged: a staged parent's children are serialized by a
+ * different subtree mutex, so publication into it is misuse. Outside-in
+ * order -- the parent first, then the child -- publishes both.
+ */
+static void staged_test_publish_staged_parent(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *outer = staged_child_alloc(test);
+ struct kobject *inner = staged_child_alloc(test);
+
+ KUNIT_ASSERT_EQ(test, kobject_add(outer, priv->parent, "outer"), 0);
+
+ /* a staged top of its own, under the still-staged outer */
+ KUNIT_ASSERT_EQ(test, kobject_add(inner, outer, "inner"), 0);
+
+ KUNIT_EXPECT_EQ(test, sysfs_publish_dir(inner), -EINVAL);
+
+ /* outside-in publication order works */
+ KUNIT_EXPECT_EQ(test, sysfs_publish_dir(outer), 0);
+ KUNIT_EXPECT_EQ(test, sysfs_publish_dir(inner), 0);
+ KUNIT_EXPECT_TRUE(test, child_visible(outer, "inner"));
+
+ kobject_del(inner);
+ kobject_put(inner);
+ kobject_del(outer);
+ kobject_put(outer);
+}
+
+/*
+ * kernfs_publish() rejects (WARN, -EINVAL) a node that is no longer staged:
+ * a second publication of an already-published directory fails and leaves
+ * the published directory undisturbed.
+ */
+static void staged_test_publish_twice(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *kobj = staged_child_alloc(test);
+
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, priv->parent, "twice"), 0);
+ KUNIT_ASSERT_EQ(test, staged_add_files(test, kobj, 2), 0);
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(kobj), 0);
+
+ /* no longer staged: a second publication is misuse */
+ KUNIT_EXPECT_EQ(test, sysfs_publish_dir(kobj), -EINVAL);
+
+ /* the published directory is undisturbed */
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(kobj));
+ KUNIT_EXPECT_TRUE(test, child_visible(priv->parent, "twice"));
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "attr0"));
+
+ kobject_del(kobj);
+ kobject_put(kobj);
+}
+
+/*
+ * Never called: a staged file cannot be opened, and the case never mmaps
+ * the file after publication either.
+ */
+static int staged_mmap_stub(struct file *file, struct kobject *kobj,
+ const struct bin_attribute *attr,
+ struct vm_area_struct *vma)
+{
+ return -ENODEV;
+}
+
+static const struct bin_attribute mmap_file_attr = {
+ .attr = { .name = "mmap_file", .mode = 0444 },
+ .size = PAGE_SIZE,
+ .mmap = staged_mmap_stub,
+};
+
+/*
+ * KERNFS_STAGED_TOP aliases KERNFS_HAS_MMAP in one flag bit, and publication
+ * and teardown clear KERNFS_STAGED_TOP only on directories -- so a FILE's
+ * aliased HAS_MMAP must never be disturbed. A staged mmap-capable bin file
+ * carries KERNFS_HAS_MMAP from creation, and publication's DIR-gated clear
+ * mask strips KERNFS_STAGED from it while leaving KERNFS_HAS_MMAP intact.
+ */
+static void staged_test_mmap_file_alias(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *kobj = staged_child_alloc(test);
+ struct kernfs_node *kn, *fresh;
+
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, priv->parent, "mmap_alias"), 0);
+ KUNIT_ASSERT_EQ(test, sysfs_create_bin_file(kobj, &mmap_file_attr), 0);
+
+ /* in the window: HAS_MMAP from creation, STAGED from the subtree */
+ kn = kernfs_find_and_get(kobj->sd, "mmap_file");
+ KUNIT_ASSERT_NOT_NULL(test, kn);
+ KUNIT_EXPECT_EQ(test, kernfs_type(kn), KERNFS_FILE);
+ KUNIT_EXPECT_TRUE(test, kn->flags & KERNFS_HAS_MMAP);
+ KUNIT_EXPECT_TRUE(test, kn->flags & KERNFS_STAGED);
+
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(kobj), 0);
+
+ /* same node: STAGED stripped, aliased HAS_MMAP survived the clear */
+ KUNIT_EXPECT_TRUE(test, kn->flags & KERNFS_HAS_MMAP);
+ KUNIT_EXPECT_FALSE(test, kn->flags & KERNFS_STAGED);
+
+ /* and the file is reachable post-publish */
+ fresh = kernfs_find_and_get(kobj->sd, "mmap_file");
+ KUNIT_EXPECT_PTR_EQ(test, fresh, kn);
+ kernfs_put(fresh);
+ kernfs_put(kn);
+
+ sysfs_remove_bin_file(kobj, &mmap_file_attr);
+ kobject_del(kobj);
+ kobject_put(kobj);
+}
+
+/* A foreign task acts on the staged directory. */
+
+struct foreign_ctx {
+ struct kobject *kobj;
+ struct attribute *new_attr; /* file the foreign task creates */
+ struct attribute *rm_attr; /* file (we created) it removes */
+ const char *notify_name; /* a live staged attr to notify */
+ int create_ret; /* sysfs_create_file() return */
+ bool notify_target_present; /* the notified attr was findable */
+ struct completion done;
+};
+
+static int foreign_fn(void *data)
+{
+ struct foreign_ctx *c = data;
+ struct kernfs_node *kn;
+
+ c->create_ret = sysfs_create_file(c->kobj, c->new_attr);
+ sysfs_remove_file(c->kobj, c->rm_attr);
+
+ /*
+ * Notify a LIVE staged attribute (the one just created) so
+ * kernfs_notify actually runs against a staged node; record that it
+ * was findable.
+ */
+ kn = kernfs_find_and_get(c->kobj->sd, c->notify_name);
+ c->notify_target_present = !!kn;
+ kernfs_put(kn);
+ sysfs_notify(c->kobj, NULL, c->notify_name);
+
+ complete(&c->done);
+ return 0;
+}
+
+static void staged_test_foreign_task(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *kobj = staged_child_alloc(test);
+ struct task_struct *t;
+ struct foreign_ctx c = {};
+ long rc;
+
+ c.kobj = kobj;
+ c.new_attr = kunit_kzalloc(test, sizeof(*c.new_attr), GFP_KERNEL);
+ c.rm_attr = kunit_kzalloc(test, sizeof(*c.rm_attr), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c.new_attr);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c.rm_attr);
+ sysfs_attr_init(c.new_attr);
+ sysfs_attr_init(c.rm_attr);
+ c.new_attr->name = "foreign_new";
+ c.new_attr->mode = 0644;
+ c.rm_attr->name = "to_remove";
+ c.rm_attr->mode = 0644;
+ c.notify_name = "foreign_new";
+ init_completion(&c.done);
+
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, priv->parent, "foreign"), 0);
+ /* we create the file the foreign task will remove */
+ KUNIT_ASSERT_EQ(test, sysfs_create_file(kobj, c.rm_attr), 0);
+
+ t = kthread_run(foreign_fn, &c, "staged_foreign");
+ KUNIT_ASSERT_FALSE(test, IS_ERR(t));
+
+ rc = wait_for_completion_timeout(&c.done, msecs_to_jiffies(10000));
+ if (!rc)
+ wait_for_completion(&c.done); /* join before reading ctx */
+ KUNIT_EXPECT_GT(test, rc, 0);
+
+ /* foreign create succeeded; the notified attribute was a live node */
+ KUNIT_EXPECT_EQ(test, c.create_ret, 0);
+ KUNIT_EXPECT_TRUE(test, c.notify_target_present);
+
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(kobj), 0);
+
+ /* after publish: created file present, removed file absent */
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "foreign_new"));
+ KUNIT_EXPECT_FALSE(test, child_visible(kobj, "to_remove"));
+
+ sysfs_remove_file(kobj, c.new_attr);
+ kobject_del(kobj);
+ kobject_put(kobj);
+}
+
+/*
+ * Abort without publication, twice in a row. Leak-observable: the parent
+ * kernfs node's subdir count and its base refcount return to their
+ * pre-registration values after each aborted, torn-down window.
+ */
+static void staged_test_abort_twice(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ unsigned long base_subdirs = sd_subdirs(priv->parent);
+ int base_count = atomic_read(&priv->parent->sd->count);
+ int i;
+
+ for (i = 0; i < 2; i++) {
+ struct kobject *kobj = staged_child_alloc(test);
+
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, priv->parent, "abort"), 0);
+ KUNIT_ASSERT_EQ(test, staged_add_files(test, kobj, 10), 0);
+ KUNIT_EXPECT_TRUE(test, sd_is_staged(kobj));
+
+ /* abort: tear down without publishing */
+ kobject_del(kobj);
+ kobject_put(kobj);
+
+ /* leak-observable: parent fully restored */
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base_subdirs);
+ KUNIT_EXPECT_EQ(test, atomic_read(&priv->parent->sd->count),
+ base_count);
+ }
+}
+
+/*
+ * Partial abort: create then remove a named group inside the window; publish;
+ * the removed group is absent, other content present.
+ */
+static void staged_test_partial_group_abort(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *kobj = staged_child_alloc(test);
+
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, priv->parent, "partial"), 0);
+ KUNIT_ASSERT_EQ(test, sysfs_create_group(kobj, &named_grp), 0);
+ KUNIT_ASSERT_EQ(test, sysfs_create_file(kobj, &grp_attr), 0);
+
+ /* remove the group again, still in the window */
+ sysfs_remove_group(kobj, &named_grp);
+
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(kobj), 0);
+ KUNIT_EXPECT_FALSE(test, child_visible(kobj, "ngroup"));
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "grp_attr"));
+
+ sysfs_remove_file(kobj, &grp_attr);
+ kobject_del(kobj);
+ kobject_put(kobj);
+}
+
+/*
+ * Parent removed during the window: publication fails -ENOENT and the orphan
+ * tears down cleanly.
+ */
+static void staged_test_parent_removed(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *mid, *kobj;
+ int ret;
+
+ /* an intermediate published parent we can remove mid-window */
+ mid = kobject_create_and_add("mid", priv->parent);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, mid);
+
+ kobj = staged_child_alloc(test);
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, mid, "orphan"), 0);
+ KUNIT_ASSERT_EQ(test, staged_add_files(test, kobj, 4), 0);
+
+ /* remove the parent while the child is still staged */
+ kobject_del(mid);
+
+ ret = sysfs_publish_dir(kobj);
+ KUNIT_EXPECT_EQ(test, ret, -ENOENT);
+
+ /* orphan tears down cleanly */
+ kobject_del(kobj);
+ kobject_put(kobj);
+ kobject_put(mid);
+}
+
+/*
+ * O(1) shared-parent cost: a staged directory with >= 20 nodes is built
+ * without a single link into the parent's children collection: the parent's
+ * subdir count is unchanged across the whole population and rises by exactly
+ * one at publish, independent of node count.
+ */
+static void staged_test_o1_root_cost(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct kobject *kobj = staged_child_alloc(test);
+ unsigned long base = sd_subdirs(priv->parent);
+
+ KUNIT_ASSERT_EQ(test, kobject_add(kobj, priv->parent, "o1cost"), 0);
+ KUNIT_ASSERT_EQ(test, staged_add_files(test, kobj, 25), 0);
+
+ /* 25 nodes added, zero links into the shared parent */
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base);
+ KUNIT_EXPECT_FALSE(test, child_visible(priv->parent, "o1cost"));
+
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(kobj), 0);
+ /* exactly one link at publish, regardless of the 25 nodes */
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base + 1);
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "attr0"));
+ KUNIT_EXPECT_TRUE(test, child_visible(kobj, "attr24"));
+
+ kobject_del(kobj);
+ kobject_put(kobj);
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base);
+}
+
+/* Two concurrent windows. */
+
+#define WINDOW_NODES 50
+
+struct window_ctx {
+ struct kobject *parent;
+ const char *name;
+ int add_ret;
+ int publish_ret;
+ struct completion done;
+};
+
+static int window_fn(void *data)
+{
+ struct window_ctx *w = data;
+ struct attribute *attrs[WINDOW_NODES] = {};
+ struct staged_kobj *sk;
+ struct kobject *kobj;
+ int i;
+
+ sk = kzalloc_obj(*sk);
+ if (!sk) {
+ w->add_ret = -ENOMEM;
+ complete(&w->done);
+ return 0;
+ }
+ kobject_init(&sk->kobj, &staged_ktype);
+ kobject_set_sd_staged(&sk->kobj, true);
+ kobj = &sk->kobj;
+
+ w->add_ret = kobject_add(kobj, w->parent, "%s", w->name);
+ if (w->add_ret) {
+ kobject_put(kobj); /* release frees the container */
+ complete(&w->done);
+ return 0;
+ }
+
+ for (i = 0; i < WINDOW_NODES; i++) {
+ struct attribute *a = kzalloc_obj(*a);
+
+ if (!a)
+ break;
+ sysfs_attr_init(a);
+ a->name = kasprintf(GFP_KERNEL, "attr%d", i);
+ a->mode = 0644;
+ if (sysfs_create_file(kobj, a)) {
+ kfree((void *)a->name);
+ kfree(a);
+ break;
+ }
+ attrs[i] = a;
+ }
+
+ w->publish_ret = sysfs_publish_dir(kobj);
+
+ kobject_del(kobj);
+ kobject_put(kobj); /* release frees the container */
+
+ /* attributes outlive their files; free them after teardown */
+ for (i = 0; i < WINDOW_NODES; i++) {
+ if (!attrs[i])
+ continue;
+ kfree((void *)attrs[i]->name);
+ kfree(attrs[i]);
+ }
+
+ complete(&w->done);
+ return 0;
+}
+
+static void staged_test_two_windows(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ struct window_ctx w1 = { .parent = priv->parent, .name = "winA" };
+ struct window_ctx w2 = { .parent = priv->parent, .name = "winB" };
+ struct task_struct *t1, *t2;
+ long r1, r2;
+
+ init_completion(&w1.done);
+ init_completion(&w2.done);
+
+ t1 = kthread_run(window_fn, &w1, "staged_winA");
+ /* nothing spawned yet if this aborts */
+ KUNIT_ASSERT_FALSE(test, IS_ERR(t1));
+ t2 = kthread_run(window_fn, &w2, "staged_winB");
+ KUNIT_EXPECT_FALSE(test, IS_ERR(t2));
+ if (IS_ERR(t2)) {
+ /* t1 writes on-stack w1: join it before returning */
+ wait_for_completion(&w1.done);
+ return;
+ }
+
+ /* no deadlock within the timeout */
+ r1 = wait_for_completion_timeout(&w1.done, msecs_to_jiffies(10000));
+ r2 = wait_for_completion_timeout(&w2.done, msecs_to_jiffies(10000));
+ /* join both before touching the on-stack contexts they write to */
+ if (!r1)
+ wait_for_completion(&w1.done);
+ if (!r2)
+ wait_for_completion(&w2.done);
+ KUNIT_EXPECT_GT(test, r1, 0);
+ KUNIT_EXPECT_GT(test, r2, 0);
+
+ KUNIT_EXPECT_EQ(test, w1.add_ret, 0);
+ KUNIT_EXPECT_EQ(test, w2.add_ret, 0);
+ KUNIT_EXPECT_EQ(test, w1.publish_ret, 0);
+ KUNIT_EXPECT_EQ(test, w2.publish_ret, 0);
+}
+
+/* Staged registration through device_add(). */
+
+static void staged_dev_release(struct device *dev)
+{
+ kfree(dev);
+}
+
+static void staged_dev_unregister(void *data)
+{
+ device_unregister(data);
+}
+
+static void staged_root_unregister(void *data)
+{
+ root_device_unregister(data);
+}
+
+/*
+ * Allocate and initialize a device for device_add(); @staged opts it in to the
+ * staged path. Freed by staged_dev_release() when the last reference drops;
+ * nothing frees it directly.
+ */
+static struct device *staged_dev_alloc(struct kunit *test,
+ struct device *parent,
+ const struct bus_type *bus,
+ const char *name, bool staged)
+{
+ struct device *dev;
+ int ret;
+
+ dev = kzalloc_obj(*dev);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+
+ device_initialize(dev);
+ dev->parent = parent;
+ dev->bus = bus;
+ dev->release = staged_dev_release;
+ if (staged)
+ dev_set_sysfs_staged(dev);
+
+ ret = dev_set_name(dev, "%s", name);
+ if (ret)
+ put_device(dev);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ return dev;
+}
+
+/* Staged registration under a class glue directory. */
+
+static void staged_class_destroy(void *data)
+{
+ class_destroy(data);
+}
+
+/*
+ * Register a staged class device with a NULL parent; it lands under the
+ * class glue directory in /sys/devices/virtual/. Unregistered by a deferred
+ * kunit action (released early where a test removes it mid-flight).
+ */
+static struct device *staged_class_dev_add(struct kunit *test,
+ const struct class *class,
+ const char *name)
+{
+ struct device *dev;
+ int ret;
+
+ dev = staged_dev_alloc(test, NULL, NULL, name, true);
+ dev->class = class;
+
+ ret = device_add(dev);
+ if (ret)
+ put_device(dev);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_unregister,
+ dev), 0);
+ return dev;
+}
+
+/*
+ * Staged registration under a class glue directory. Two staged class
+ * devices share one glue dir; removing one sibling must not reap the
+ * shared glue dir under the other (cleanup_glue_dir() sees the survivor's
+ * glue-dir reference, kref >= 2), and once the last child is gone the
+ * reaped glue dir must be recreatable by a further staged registration.
+ */
+static struct attribute glue_attr = { .name = "glue_attr", .mode = 0644 };
+
+static void staged_test_glue_dir(struct kunit *test)
+{
+ struct device *dev_a, *dev_b, *dev_a2;
+ struct class *class;
+
+ class = class_create("staged_kunit_class");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, class);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_class_destroy,
+ class), 0);
+
+ /* two staged children of the same (new) glue directory */
+ dev_a = staged_class_dev_add(test, class, "glueA");
+ dev_b = staged_class_dev_add(test, class, "glueB");
+ KUNIT_ASSERT_NOT_NULL(test, dev_a->kobj.sd);
+ KUNIT_ASSERT_NOT_NULL(test, dev_b->kobj.sd);
+ KUNIT_EXPECT_PTR_EQ(test, dev_a->kobj.parent, dev_b->kobj.parent);
+
+ /*
+ * The glue dir itself is not staged: glue dirs are created eagerly.
+ * This case cannot observe the inside of the window -- device_add()
+ * opens and closes it internally; the kobject-level cases above
+ * cover the window itself.
+ */
+ KUNIT_EXPECT_TRUE(test, kobject_sd_staged(&dev_a->kobj));
+ KUNIT_EXPECT_TRUE(test, kobject_sd_staged(&dev_b->kobj));
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&dev_a->kobj));
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&dev_b->kobj));
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(dev_a->kobj.parent));
+ KUNIT_EXPECT_TRUE(test, child_visible(dev_a->kobj.parent, "glueA"));
+ KUNIT_EXPECT_TRUE(test, child_visible(dev_b->kobj.parent, "glueB"));
+
+ /* sibling removal must not reap the glue dir under dev_b */
+ kunit_release_action(test, staged_dev_unregister, dev_a);
+ KUNIT_ASSERT_NOT_NULL(test, dev_b->kobj.sd);
+ KUNIT_EXPECT_TRUE(test, child_visible(dev_b->kobj.parent, "glueB"));
+ KUNIT_EXPECT_EQ(test, sysfs_create_file(&dev_b->kobj, &glue_attr), 0);
+ KUNIT_EXPECT_TRUE(test, child_visible(&dev_b->kobj, "glue_attr"));
+ sysfs_remove_file(&dev_b->kobj, &glue_attr);
+
+ /* last child gone: the glue dir is reaped ... */
+ kunit_release_action(test, staged_dev_unregister, dev_b);
+
+ /* ... and a staged re-registration under A's name recreates it */
+ dev_a2 = staged_class_dev_add(test, class, "glueA");
+ KUNIT_EXPECT_NOT_NULL(test, dev_a2->kobj.sd);
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&dev_a2->kobj));
+}
+
+/*
+ * device_add() staged bracket, error unwind. A staged device whose name
+ * collides with a live sibling only fails at publication, deep inside
+ * device_add(). The failure must unwind the whole bracket: the caller sees the
+ * error, the failed device keeps no kernfs node, the collision winner and the
+ * parent are undisturbed, the last reference frees the device, and a fresh
+ * staged registration under a free name still succeeds afterwards.
+ */
+static void staged_test_device_add_unwind(struct kunit *test)
+{
+ struct device *root, *eager, *staged, *retry;
+ struct kernfs_node *kn;
+ unsigned long base;
+ int ret;
+
+ root = root_device_register("staged_kunit_unwind");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_root_unregister,
+ root), 0);
+
+ /* the collision winner, registered eagerly */
+ eager = staged_dev_alloc(test, root, NULL, "collide", false);
+ KUNIT_ASSERT_EQ(test, device_add(eager), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_unregister,
+ eager), 0);
+ base = sd_subdirs(&root->kobj);
+
+ /* same name, staged: the directory is built, publication collides */
+ staged = staged_dev_alloc(test, root, NULL, "collide", true);
+ ret = device_add(staged);
+ KUNIT_ASSERT_EQ(test, ret, -EEXIST);
+
+ /* unwound: no kernfs node left on the failed device */
+ KUNIT_EXPECT_NULL(test, staged->kobj.sd);
+
+ /* no residue under the parent; the winner is what remains */
+ KUNIT_EXPECT_EQ(test, sd_subdirs(&root->kobj), base);
+ kn = kernfs_find_and_get(root->kobj.sd, "collide");
+ KUNIT_EXPECT_PTR_EQ(test, kn, eager->kobj.sd);
+ kernfs_put(kn);
+
+ /* the failed device's last reference frees it */
+ put_device(staged);
+
+ /* a fresh staged registration under a free name still succeeds */
+ retry = staged_dev_alloc(test, root, NULL, "retry", true);
+ KUNIT_ASSERT_EQ(test, device_add(retry), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_unregister,
+ retry), 0);
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&retry->kobj));
+ KUNIT_EXPECT_TRUE(test, child_visible(&root->kobj, "retry"));
+ KUNIT_EXPECT_EQ(test, sd_subdirs(&root->kobj), base + 1);
+}
+
+/* Deferred bus-klist insertion, on a real bus with a real driver. */
+
+static const struct bus_type staged_test_bus = {
+ .name = "staged_kunit_bus",
+};
+
+static int staged_test_driver_probe(struct device *dev)
+{
+ return 0;
+}
+
+static struct device_driver staged_test_driver = {
+ .name = "staged_kunit_drv",
+ .bus = &staged_test_bus,
+ .owner = THIS_MODULE,
+ .probe = staged_test_driver_probe,
+};
+
+static void staged_bus_unregister(void *data)
+{
+ bus_unregister(data);
+}
+
+static void staged_driver_unregister(void *data)
+{
+ driver_unregister(data);
+}
+
+struct staged_bus_scan {
+ struct device *want;
+ unsigned int seen;
+ bool found;
+};
+
+static int staged_bus_scan_fn(struct device *dev, void *data)
+{
+ struct staged_bus_scan *scan = data;
+
+ scan->seen++;
+ if (dev == scan->want)
+ scan->found = true;
+ return 0;
+}
+
+/*
+ * bus_add_device() keeps a staged device off the bus klist -- the one
+ * driver_attach() walks -- until publication, and device_add() inserts it via
+ * bus_add_device_publish() before anything can observe the device. Assert the
+ * outcome that a regression in that gating would destroy: once device_add()
+ * returns, the device is on the klist that bus_for_each_dev() walks, and the
+ * driver has actually bound to it.
+ */
+static void staged_test_bus_klist(struct kunit *test)
+{
+ struct staged_bus_scan scan = {};
+ struct device *dev;
+
+ KUNIT_ASSERT_EQ(test, bus_register(&staged_test_bus), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_bus_unregister,
+ (void *)&staged_test_bus), 0);
+
+ KUNIT_ASSERT_EQ(test, driver_register(&staged_test_driver), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_driver_unregister,
+ &staged_test_driver), 0);
+
+ dev = staged_dev_alloc(test, NULL, &staged_test_bus, "busdev", true);
+ KUNIT_ASSERT_EQ(test, device_add(dev), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_unregister,
+ dev), 0);
+
+ /* opted in, and published by the time device_add() returned */
+ KUNIT_EXPECT_TRUE(test, kobject_sd_staged(&dev->kobj));
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&dev->kobj));
+
+ /* the deferred klist insertion happened: the bus walkers see it */
+ scan.want = dev;
+ KUNIT_ASSERT_EQ(test, bus_for_each_dev(&staged_test_bus, NULL, &scan,
+ staged_bus_scan_fn), 0);
+ KUNIT_EXPECT_TRUE(test, scan.found);
+ KUNIT_EXPECT_EQ(test, scan.seen, 1u);
+
+ /* ... and driver binding actually ran against it */
+ KUNIT_EXPECT_PTR_EQ(test, dev->driver, &staged_test_driver);
+}
+
+/*
+ * Hashed staged-mutex slot aliasing. Open one more coexisting staged window
+ * than the hashed mutex array has slots, so by pigeonhole at least two staged
+ * tops share one mutex. Aliasing must not confuse kernfs_staged_lock()'s
+ * owning-subtree identification: every window must populate, publish and come
+ * out complete. The collision is asserted rather than assumed -- the slot
+ * index is the same pure function of the node address that
+ * kernfs_staged_mutex_ptr() uses.
+ */
+#define STAGED_SLOT_WINDOWS (NR_KERNFS_LOCKS + 1)
+
+static void staged_test_hashed_slot_collision(struct kunit *test)
+{
+ struct staged_test_priv *priv = test->priv;
+ unsigned long base = sd_subdirs(priv->parent);
+ struct kobject **kids;
+ unsigned int *slots;
+ bool collided = false;
+ int i, j;
+
+ kids = kunit_kcalloc(test, STAGED_SLOT_WINDOWS, sizeof(*kids), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, kids);
+ slots = kunit_kcalloc(test, STAGED_SLOT_WINDOWS, sizeof(*slots), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, slots);
+
+ for (i = 0; i < STAGED_SLOT_WINDOWS; i++) {
+ kids[i] = staged_child_alloc(test);
+ KUNIT_ASSERT_EQ(test, kobject_add(kids[i], priv->parent,
+ "slot%d", i), 0);
+ KUNIT_ASSERT_TRUE(test, sd_is_staged(kids[i]));
+ KUNIT_ASSERT_EQ(test, staged_add_files(test, kids[i], 2), 0);
+ slots[i] = hash_ptr(kids[i]->sd, NR_KERNFS_LOCK_BITS);
+ }
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base);
+
+ /* pigeonhole: NR_KERNFS_LOCKS + 1 tops over NR_KERNFS_LOCKS slots */
+ for (i = 0; i < STAGED_SLOT_WINDOWS && !collided; i++)
+ for (j = i + 1; j < STAGED_SLOT_WINDOWS; j++)
+ if (slots[i] == slots[j]) {
+ collided = true;
+ break;
+ }
+ KUNIT_EXPECT_TRUE(test, collided);
+
+ for (i = 0; i < STAGED_SLOT_WINDOWS; i++)
+ KUNIT_ASSERT_EQ(test, sysfs_publish_dir(kids[i]), 0);
+
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent),
+ base + STAGED_SLOT_WINDOWS);
+ for (i = 0; i < STAGED_SLOT_WINDOWS; i++) {
+ char name[16];
+
+ snprintf(name, sizeof(name), "slot%d", i);
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(kids[i]));
+ KUNIT_EXPECT_TRUE(test, child_visible(priv->parent, name));
+ KUNIT_EXPECT_TRUE(test, child_visible(kids[i], "attr0"));
+ KUNIT_EXPECT_TRUE(test, child_visible(kids[i], "attr1"));
+ }
+
+ for (i = 0; i < STAGED_SLOT_WINDOWS; i++) {
+ kobject_del(kids[i]);
+ kobject_put(kids[i]);
+ }
+ KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base);
+}
+
+static struct kunit_case staged_device_tests[] = {
+ KUNIT_CASE(staged_test_publish_end_to_end),
+ KUNIT_CASE(staged_test_duplicate_name),
+ KUNIT_CASE(staged_test_publish_staged_parent),
+ KUNIT_CASE(staged_test_publish_twice),
+ KUNIT_CASE(staged_test_mmap_file_alias),
+ KUNIT_CASE(staged_test_foreign_task),
+ KUNIT_CASE(staged_test_abort_twice),
+ KUNIT_CASE(staged_test_partial_group_abort),
+ KUNIT_CASE(staged_test_parent_removed),
+ KUNIT_CASE(staged_test_o1_root_cost),
+ KUNIT_CASE(staged_test_two_windows),
+ KUNIT_CASE(staged_test_glue_dir),
+ KUNIT_CASE(staged_test_device_add_unwind),
+ KUNIT_CASE(staged_test_bus_klist),
+ KUNIT_CASE(staged_test_hashed_slot_collision),
+ {}
+};
+
+static struct kunit_suite staged_device_test_suite = {
+ .name = "staged_device",
+ .init = staged_test_init,
+ .exit = staged_test_exit,
+ .test_cases = staged_device_tests,
+};
+
+kunit_test_suite(staged_device_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for staged sysfs directory registration");
+MODULE_LICENSE("GPL");
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
` (4 preceding siblings ...)
2026-09-11 17:43 ` [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-12 13:20 ` Andy Shevchenko
2026-09-11 17:43 ` [RFC PATCH 7/8] PCI/IOV: Register virtual functions through the staged sysfs path Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 8/8] vfio: Opt the group and vfio-dev class devices into staged sysfs Pavol Sakac
7 siblings, 1 reply; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
Staged registration publishes a device's subtree in one step, so nothing
observes the device before it is complete. That does not hold for a
device registered from inside the window: device_add() calls hooks that
can register a child of the device being added, and such a child takes
the eager path and announces itself with KOBJ_ADD while its own path
resolves to nothing. Nothing replays that event, so a consumer that
cannot open the DEVPATH never learns of the child. Neither opt-in in
this series reaches that case: nothing attaches a wakeup source to a
PCI VF or to the VFIO class devices, and pci_acpi_setup() enables
wakeup only for a bridge that can do D3. It is reachable by code the
opt-in caller does not control: dpm_sysfs_add() registers a
wakeup-source device whenever power.wakeup is already attached, and
wakeup_source_register() registers one directly for a device that is
already registered, which a staged device is. The driver core
therefore handles the case rather than forbidding it in the opt-in
contract.
Defer such a registration, composing the idiom block/genhd.c already uses
for a disk's partition tree: suppress the uevents, complete the tree,
then unsuppress and replay KOBJ_ADD. A device whose directory kernfs
marked staged is enqueued suppressed on the window of the nearest
ancestor that opted in, and the window is drained as the ancestor's
device_add() returns, just after its own KOBJ_ADD, each member's ADD
delivered in registration order followed by BIND where a driver bound
in the window. An ancestor whose owner suppresses its uevent and
replays it after registration will see members announced before its
replayed ADD. The window lives in a global hashtable keyed by the
opted-in device, so no struct grows.
Members are expected to be registered synchronously by the opted-in
device_add() that owns the window, the only shape the driver core can
reason about here. Raw uevent_suppress is not reused as the detection
marker, precisely because a subsystem may already own it; a device found
already suppressed is left out of the window entirely. An in-window
KOBJ_CHANGE is dropped rather than replayed, as in the genhd case, since
only the addition can be reconstructed afterwards. A failed opted-in
registration closes its window without replaying anything and leaves
suppression set, so a member's KOBJ_REMOVE is dropped too and userspace
never sees a removal for an addition it never saw.
The staged_device suite gains five cases for the window: the replay of a
member's addition once the owner's device_add() returns, the aborted
window that replays nothing and leaves suppression set, a member behind
a class glue directory, the reconstructed KOBJ_BIND of a driver bound in
the window, and a member found already suppressed being left out of the
window.
If you would rather not carry this until a caller needs it, it can be
dropped and the restriction stated as an opt-in condition instead.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
drivers/base/core.c | 170 +++++++++++-
drivers/base/test/staged-device-test.c | 364 +++++++++++++++++++++++++
fs/sysfs/dir.c | 26 ++
include/linux/device.h | 15 +
include/linux/sysfs.h | 6 +
5 files changed, 579 insertions(+), 2 deletions(-)
diff --git a/drivers/base/core.c b/drivers/base/core.c
index caba5610a04d..7d3784bd2136 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -3663,6 +3663,148 @@ static int device_private_init(struct device *dev)
return 0;
}
+/*
+ * Deferred uevents for devices registered inside a staged window.
+ *
+ * A hook called from an opted-in device_add() may register a child inside
+ * the still-invisible subtree; announcing it right away would advertise a
+ * path userspace cannot open, so its uevents are suppressed at registration
+ * and replayed once the window's top publishes.
+ *
+ * A window is a record on the top's device_add() stack, hashed by the top
+ * device, so a member is found without a field of its own in struct device,
+ * kobject or kernfs_node. Membership is decided under @staged_windows_lock,
+ * which also serializes against the drain: a device either joins a window that
+ * has not drained yet, and is replayed, or finds no window -- which means the
+ * subtree is published and announcing immediately is correct.
+ */
+struct staged_window {
+ struct hlist_node node;
+ struct device *top;
+ struct list_head members;
+};
+
+struct staged_member {
+ struct list_head node;
+ struct device *dev; /* holds a reference */
+};
+
+static DEFINE_HASHTABLE(staged_windows, 6);
+static DEFINE_SPINLOCK(staged_windows_lock);
+
+static void staged_window_open(struct staged_window *win, struct device *top)
+{
+ win->top = top;
+ INIT_LIST_HEAD(&win->members);
+
+ spin_lock(&staged_windows_lock);
+ hash_add(staged_windows, &win->node, (unsigned long)top);
+ spin_unlock(&staged_windows_lock);
+}
+
+/*
+ * Suppress and enqueue @dev if it is being registered inside an ancestor's
+ * staged window. The fast path is one query of @dev's own directory, which
+ * kernfs marked staged when it linked it below a staged parent, so the
+ * non-opted world pays no ancestor walk. Only a device that is genuinely
+ * inside a window walks up to the nearest opted-in ancestor, the device whose
+ * window it is.
+ */
+static void device_defer_uevents(struct device *dev)
+{
+ struct staged_member *member;
+ struct staged_window *win;
+ struct device *top;
+
+ if (!sysfs_dir_staged(&dev->kobj))
+ return;
+
+ /*
+ * Suppression already set belongs to whoever set it (block/genhd.c
+ * suppresses a disk across device_add() and keeps a hidden disk
+ * suppressed for good); taking no membership at all is what keeps
+ * the drain from clearing it or announcing on the owner's behalf.
+ */
+ if (dev_get_uevent_suppress(dev))
+ return;
+
+ for (top = dev->parent; top; top = top->parent)
+ if (dev_sysfs_staged(top))
+ break;
+ /*
+ * Nothing above @dev opted in, so there is no window to defer into.
+ * Announce immediately -- the eager behaviour, unresolvable path
+ * included; a registration is not worth failing over a uevent.
+ */
+ if (!top)
+ return;
+
+ member = kzalloc_obj(*member);
+ /*
+ * Same fallback, same reason, for a membership record that cannot be
+ * allocated: announce immediately rather than fail the registration.
+ */
+ if (!member)
+ return;
+ member->dev = get_device(dev);
+
+ spin_lock(&staged_windows_lock);
+ hash_for_each_possible(staged_windows, win, node, (unsigned long)top) {
+ if (win->top != top)
+ continue;
+ dev_set_uevent_suppress(dev, 1);
+ list_add_tail(&member->node, &win->members);
+ member = NULL;
+ break;
+ }
+ spin_unlock(&staged_windows_lock);
+
+ if (member) {
+ put_device(dev);
+ kfree(member);
+ }
+}
+
+/*
+ * Close @win. With @replay the members' suppressed uevents are delivered in
+ * registration order, ADD and then BIND for a member a driver has already bound
+ * to. Without it -- the top's registration failed -- nothing is delivered and
+ * suppression stays set, so a member's KOBJ_REMOVE is dropped as well and
+ * userspace never sees a removal for an addition it never saw (a member
+ * deleted before publication is likewise never announced). Emission
+ * sleeps, so the list is spliced out under the lock and walked without it.
+ *
+ * A member is expected to have been registered synchronously, by the top's own
+ * device_add(), so what the drain reads of a member is settled state written by
+ * this thread. The registration and driver tests below are a guard against
+ * misuse, not an ordering contract with a thread registering members of its
+ * own: nothing here makes that concurrent.
+ */
+static void staged_window_close(struct staged_window *win, bool replay)
+{
+ struct staged_member *member, *tmp;
+ LIST_HEAD(members);
+
+ spin_lock(&staged_windows_lock);
+ hash_del(&win->node);
+ list_splice_init(&win->members, &members);
+ spin_unlock(&staged_windows_lock);
+
+ list_for_each_entry_safe(member, tmp, &members, node) {
+ struct device *dev = member->dev;
+
+ if (replay && device_is_registered(dev)) {
+ dev_set_uevent_suppress(dev, 0);
+ kobject_uevent(&dev->kobj, KOBJ_ADD);
+ /* best effort: members bind in this thread */
+ if (READ_ONCE(dev->driver))
+ kobject_uevent(&dev->kobj, KOBJ_BIND);
+ }
+ put_device(dev);
+ kfree(member);
+ }
+}
+
/**
* device_add - add device to device hierarchy.
* @dev: device.
@@ -3696,6 +3838,7 @@ int device_add(struct device *dev)
struct device *parent;
struct kobject *kobj;
struct class_interface *class_intf;
+ struct staged_window win;
int error = -EINVAL;
struct kobject *glue_dir = NULL;
@@ -3760,6 +3903,17 @@ int device_add(struct device *dev)
goto Error;
}
+ /*
+ * The directory is invisible from here to publication: open the
+ * window that devices registered from inside it defer their uevents
+ * into, or -- for a device that is itself such a registration --
+ * join the window of the ancestor whose subtree it landed in.
+ */
+ if (kobject_sd_staged(&dev->kobj))
+ staged_window_open(&win, dev);
+ else
+ device_defer_uevents(dev);
+
/* notify platform of device entry */
device_platform_notify(dev);
@@ -3790,8 +3944,9 @@ int device_add(struct device *dev)
* whose target is this device -- class/bus/ACPI -- are created earlier
* and resolve only at publication.) Children added by notify hooks
* inside the staged window were created staged-interior and are
- * published together with this device; their uevents may precede
- * their sysfs visibility.
+ * published together with this device; their uevents were suppressed at
+ * registration and are replayed below, once publication has made them
+ * resolvable.
*/
if (kobject_sd_staged(&dev->kobj)) {
error = sysfs_publish_dir(&dev->kobj);
@@ -3818,6 +3973,10 @@ int device_add(struct device *dev)
bus_notify(dev, BUS_NOTIFY_ADD_DEVICE);
kobject_uevent(&dev->kobj, KOBJ_ADD);
+ /* the subtree is visible now: announce what was registered inside it */
+ if (kobject_sd_staged(&dev->kobj))
+ staged_window_close(&win, true);
+
/*
* Check if any of the other devices (consumers) have been waiting for
* this device (supplier) to be added so that they can create a device
@@ -3897,6 +4056,13 @@ int device_add(struct device *dev)
device_remove_file(dev, &dev_attr_uevent);
attrError:
device_platform_notify_remove(dev);
+ /*
+ * Matching remove notifications must undo registrations made by
+ * the add notifications. Anything still queued is dropped without
+ * announcement.
+ */
+ if (kobject_sd_staged(&dev->kobj))
+ staged_window_close(&win, false);
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
glue_dir = get_glue_dir(dev);
kobject_del(&dev->kobj);
diff --git a/drivers/base/test/staged-device-test.c b/drivers/base/test/staged-device-test.c
index d32f00214a8b..55891d8426ba 100644
--- a/drivers/base/test/staged-device-test.c
+++ b/drivers/base/test/staged-device-test.c
@@ -858,8 +858,13 @@ static void staged_test_device_add_unwind(struct kunit *test)
/* Deferred bus-klist insertion, on a real bus with a real driver. */
+/* Records replayed child uevents; defined with the in-window cases below. */
+static int staged_test_bus_uevent(const struct device *dev,
+ struct kobj_uevent_env *env);
+
static const struct bus_type staged_test_bus = {
.name = "staged_kunit_bus",
+ .uevent = staged_test_bus_uevent,
};
static int staged_test_driver_probe(struct device *dev)
@@ -1010,6 +1015,360 @@ static void staged_test_hashed_slot_collision(struct kunit *test)
KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base);
}
+/* Uevents deferred for devices registered inside the window. */
+
+/*
+ * A device registered from inside another device's staged window must not
+ * announce itself while its path is unresolvable, and must be announced once
+ * the window's top publishes.
+ *
+ * The seam: the hook has to run between kobject_add() and publication, and the
+ * one such point a test can drive is an attribute group's is_visible()
+ * callback, which device_add_attrs() invokes on the device's own groups inside
+ * the window. BUS_NOTIFY_ADD_DEVICE cannot serve -- device_add() emits it
+ * after publication. (The in-tree instance is the wakeup source an ACPI
+ * platform notifier registers under the device being added.)
+ */
+struct staged_window_ctx {
+ struct kunit *test;
+ struct device *top; /* the staged device being added */
+ struct device *child; /* pre-allocated, added by the hook */
+ const struct class *child_class; /* set: a glue dir interposes */
+ const struct bus_type *child_bus; /* if set, a driver can bind */
+ bool suppress_child; /* the child's owner suppresses it */
+ bool hook_ran;
+ bool delete_in_window; /* delete the child while inside */
+ int add_ret;
+ unsigned int suppressed_in_window;
+ bool staged_in_window;
+ bool bound_in_window;
+ unsigned int suppressed_at_delete;
+ unsigned int add_uevents; /* child ACTION=add bus callbacks */
+ unsigned int bind_uevents; /* child ACTION=bind bus callbacks */
+ unsigned int uevent_seq;
+ unsigned int add_seq; /* sequence of the last ADD */
+ unsigned int bind_seq; /* sequence of the last BIND */
+};
+
+static struct staged_window_ctx *staged_window_ctx;
+
+/*
+ * Records the child's replayed uevents as they are dispatched through the
+ * bus uevent callback -- which runs upstream of netlink broadcast, so this
+ * observes action-specific kernel dispatch and ordering, not reception by
+ * any consumer. Scoped to the active context's child: the same static bus
+ * serves another case, and teardown emits further events.
+ */
+static int staged_test_bus_uevent(const struct device *dev,
+ struct kobj_uevent_env *env)
+{
+ struct staged_window_ctx *c = staged_window_ctx;
+ int i;
+
+ if (!c || dev != c->child)
+ return 0;
+
+ for (i = 0; i < env->envp_idx; i++) {
+ if (!strcmp(env->envp[i], "ACTION=add")) {
+ c->add_uevents++;
+ c->add_seq = ++c->uevent_seq;
+ } else if (!strcmp(env->envp[i], "ACTION=bind")) {
+ c->bind_uevents++;
+ c->bind_seq = ++c->uevent_seq;
+ }
+ }
+ return 0;
+}
+
+/*
+ * Outcome-aware deferred cleanup for the window tests' devices: registered
+ * as soon as an initialized reference exists, so a fatal assertion cannot
+ * leak a device whichever side of registration it aborts on. Explicit
+ * teardown goes through kunit_release_action() so no stale action remains.
+ */
+static void staged_dev_cleanup(void *data)
+{
+ struct device *dev = data;
+
+ if (device_is_registered(dev))
+ device_unregister(dev);
+ else
+ put_device(dev);
+}
+
+static umode_t staged_window_is_visible(struct kobject *kobj,
+ struct attribute *attr, int n)
+{
+ struct staged_window_ctx *c = staged_window_ctx;
+
+ if (!c || c->hook_ran || kobj != &c->top->kobj)
+ return attr->mode;
+ c->hook_ran = true;
+
+ c->add_ret = device_add(c->child);
+ if (c->add_ret)
+ return attr->mode;
+
+ c->suppressed_in_window = dev_get_uevent_suppress(c->child);
+ c->staged_in_window = sd_is_staged(&c->child->kobj);
+ c->bound_in_window = c->child->driver;
+
+ if (c->delete_in_window) {
+ struct device *child = c->child;
+
+ device_del(child);
+ c->suppressed_at_delete = dev_get_uevent_suppress(child);
+ c->child = NULL;
+ /* runs staged_dev_cleanup: unregistered now, so put_device */
+ kunit_release_action(c->test, staged_dev_cleanup, child);
+ }
+ return attr->mode;
+}
+
+static struct attribute in_window_attr = {
+ .name = "in_window_attr", .mode = 0644,
+};
+
+static struct attribute *in_window_attrs[] = { &in_window_attr, NULL };
+static const struct attribute_group in_window_grp = {
+ .attrs = in_window_attrs,
+ .is_visible = staged_window_is_visible,
+};
+
+static const struct attribute_group *in_window_grps[] = {
+ &in_window_grp, NULL,
+};
+
+/* Register @name staged under @parent, with a child added inside its window. */
+static struct device *staged_window_dev_add(struct kunit *test,
+ struct staged_window_ctx *ctx,
+ struct device *parent,
+ const char *name, int want)
+{
+ struct device *top;
+ int ret;
+
+ ctx->test = test;
+ top = staged_dev_alloc(test, parent, NULL, name, true);
+ top->groups = in_window_grps;
+ ctx->top = top;
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_cleanup,
+ top), 0);
+ ctx->child = staged_dev_alloc(test, top, ctx->child_bus, "in_window",
+ false);
+ ctx->child->class = ctx->child_class;
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_cleanup,
+ ctx->child), 0);
+ /* the genhd shape: an owner suppresses the device before adding it */
+ if (ctx->suppress_child)
+ dev_set_uevent_suppress(ctx->child, 1);
+
+ staged_window_ctx = ctx;
+ ret = device_add(top);
+ staged_window_ctx = NULL;
+
+ /* the success paths below depend on a registered top */
+ if (!want)
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ else
+ KUNIT_EXPECT_EQ(test, ret, want);
+
+ /* the hook ran inside the window, and the child registered there */
+ KUNIT_ASSERT_TRUE(test, ctx->hook_ran);
+ KUNIT_ASSERT_EQ(test, ctx->add_ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->suppressed_in_window, 1u);
+ KUNIT_EXPECT_TRUE(test, ctx->staged_in_window);
+ return top;
+}
+
+/*
+ * Replay: the child is suppressed and invisible inside the window, and once
+ * the top's device_add() returns it is visible and unsuppressed again.
+ */
+static void staged_test_in_window_uevent_replay(struct kunit *test)
+{
+ struct staged_window_ctx ctx = {};
+ struct device *root, *top;
+
+ root = root_device_register("staged_kunit_replay");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_root_unregister,
+ root), 0);
+
+ top = staged_window_dev_add(test, &ctx, root, "replay_top", 0);
+
+ /* published: the deferred addition was announced and is resolvable */
+ KUNIT_EXPECT_EQ(test, dev_get_uevent_suppress(ctx.child), 0u);
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&ctx.child->kobj));
+ KUNIT_EXPECT_TRUE(test, child_visible(&top->kobj, "in_window"));
+
+ /* a child goes before the parent directory it lives in */
+ kunit_release_action(test, staged_dev_cleanup, ctx.child);
+}
+
+/*
+ * Unwind: the top's registration fails at publication, after a child was
+ * registered inside the window and deleted again there. The deletion must
+ * leave suppression set -- userspace saw no addition, so it must see no
+ * removal -- the aborted window must drain without replaying anything, and the
+ * mechanism must still work for the next window.
+ */
+static void staged_test_in_window_uevent_unwind(struct kunit *test)
+{
+ struct staged_window_ctx ctx = { .delete_in_window = true };
+ struct staged_window_ctx retry_ctx = {};
+ struct device *root, *eager, *top, *retry;
+
+ root = root_device_register("staged_kunit_replay_unwind");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_root_unregister,
+ root), 0);
+
+ /* the collision winner: the staged top below fails at publication */
+ eager = staged_dev_alloc(test, root, NULL, "collide", false);
+ KUNIT_ASSERT_EQ(test, device_add(eager), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_dev_unregister,
+ eager), 0);
+
+ top = staged_window_dev_add(test, &ctx, root, "collide", -EEXIST);
+
+ /* deleted before publication: still suppressed, so its REMOVE is too */
+ KUNIT_EXPECT_EQ(test, ctx.suppressed_at_delete, 1u);
+
+ /* the failed top is fully unwound, and its last reference frees it */
+ KUNIT_EXPECT_NULL(test, top->kobj.sd);
+ kunit_release_action(test, staged_dev_cleanup, top);
+
+ /* the next window still defers and replays */
+ retry = staged_window_dev_add(test, &retry_ctx, root, "retry", 0);
+ KUNIT_EXPECT_EQ(test, dev_get_uevent_suppress(retry_ctx.child), 0u);
+ KUNIT_EXPECT_TRUE(test, child_visible(&retry->kobj, "in_window"));
+ kunit_release_action(test, staged_dev_cleanup, retry_ctx.child);
+}
+
+/*
+ * A member behind a glue directory. An eager class device parented to the
+ * staged top does not land directly underneath it: get_device_parent()
+ * interposes a class glue directory, so the member's own directory sits a level
+ * further down. It is detected all the same, because detection queries the
+ * member's own directory -- which kernfs marks staged for every node it links
+ * inside a staged subtree, at any depth -- rather than its parent kobject,
+ * which here is the glue directory and never opted in.
+ */
+static void staged_test_in_window_class_member(struct kunit *test)
+{
+ struct staged_window_ctx ctx = {};
+ struct device *root, *top;
+ struct class *class;
+
+ class = class_create("staged_kunit_in_window");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, class);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_class_destroy,
+ class), 0);
+
+ root = root_device_register("staged_kunit_class_member");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_root_unregister,
+ root), 0);
+
+ ctx.child_class = class;
+ top = staged_window_dev_add(test, &ctx, root, "class_top", 0);
+
+ /* the glue dir interposed: the member is not the top's own child */
+ KUNIT_EXPECT_PTR_NE(test, ctx.child->kobj.parent, &top->kobj);
+
+ KUNIT_EXPECT_EQ(test, dev_get_uevent_suppress(ctx.child), 0u);
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&ctx.child->kobj));
+ KUNIT_EXPECT_TRUE(test, child_visible(ctx.child->kobj.parent,
+ "in_window"));
+
+ kunit_release_action(test, staged_dev_cleanup, ctx.child);
+}
+
+/*
+ * A member a driver binds to inside the window. bus_probe_device() runs from
+ * the member's own device_add(), so the binding happens while the member is
+ * still suppressed and its KOBJ_BIND is dropped along with everything else.
+ * Successful closure must reconstruct it after the ADD: exactly one ADD
+ * followed by exactly one BIND passed through the child device's bus uevent
+ * callback. The callback runs upstream of netlink broadcast, so this pins
+ * kernel dispatch and ordering, not delivery to a consumer.
+ */
+static void staged_test_in_window_bind_replay(struct kunit *test)
+{
+ struct staged_window_ctx ctx = {};
+ struct device *root, *top;
+
+ KUNIT_ASSERT_EQ(test, bus_register(&staged_test_bus), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_bus_unregister,
+ (void *)&staged_test_bus), 0);
+
+ KUNIT_ASSERT_EQ(test, driver_register(&staged_test_driver), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_driver_unregister,
+ &staged_test_driver), 0);
+
+ root = root_device_register("staged_kunit_bind_replay");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_root_unregister,
+ root), 0);
+
+ ctx.child_bus = &staged_test_bus;
+ top = staged_window_dev_add(test, &ctx, root, "bind_top", 0);
+
+ /* the driver bound inside the window, so the BIND was suppressed too */
+ KUNIT_EXPECT_TRUE(test, ctx.bound_in_window);
+
+ /* the drain replayed exactly one ADD, then exactly one BIND */
+ KUNIT_EXPECT_EQ(test, ctx.add_uevents, 1u);
+ KUNIT_EXPECT_EQ(test, ctx.bind_uevents, 1u);
+ KUNIT_EXPECT_LT(test, ctx.add_seq, ctx.bind_seq);
+
+ KUNIT_EXPECT_EQ(test, dev_get_uevent_suppress(ctx.child), 0u);
+ KUNIT_EXPECT_PTR_EQ(test, ctx.child->driver, &staged_test_driver);
+ KUNIT_EXPECT_TRUE(test, child_visible(&top->kobj, "in_window"));
+
+ kunit_release_action(test, staged_dev_cleanup, ctx.child);
+}
+
+/*
+ * A member that owns its own suppression (the genhd shape; see
+ * device_defer_uevents()) is not taken into the window at all, and has to
+ * come out of it exactly as it went in: neither unsuppressed nor announced;
+ * the suppression the case observes in-window is the owner's.
+ */
+static void staged_test_in_window_foreign_suppress(struct kunit *test)
+{
+ struct staged_window_ctx ctx = { .suppress_child = true };
+ struct device *root, *top;
+
+ root = root_device_register("staged_kunit_foreign_suppress");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ staged_root_unregister,
+ root), 0);
+
+ top = staged_window_dev_add(test, &ctx, root, "foreign_top", 0);
+
+ /* the drain left the owner's state alone: not cleared, not replayed */
+ KUNIT_EXPECT_EQ(test, dev_get_uevent_suppress(ctx.child), 1u);
+
+ /* and the member was published with the window all the same */
+ KUNIT_EXPECT_FALSE(test, sd_is_staged(&ctx.child->kobj));
+ KUNIT_EXPECT_TRUE(test, child_visible(&top->kobj, "in_window"));
+
+ kunit_release_action(test, staged_dev_cleanup, ctx.child);
+}
+
static struct kunit_case staged_device_tests[] = {
KUNIT_CASE(staged_test_publish_end_to_end),
KUNIT_CASE(staged_test_duplicate_name),
@@ -1026,6 +1385,11 @@ static struct kunit_case staged_device_tests[] = {
KUNIT_CASE(staged_test_device_add_unwind),
KUNIT_CASE(staged_test_bus_klist),
KUNIT_CASE(staged_test_hashed_slot_collision),
+ KUNIT_CASE(staged_test_in_window_uevent_replay),
+ KUNIT_CASE(staged_test_in_window_uevent_unwind),
+ KUNIT_CASE(staged_test_in_window_class_member),
+ KUNIT_CASE(staged_test_in_window_bind_replay),
+ KUNIT_CASE(staged_test_in_window_foreign_suppress),
{}
};
diff --git a/fs/sysfs/dir.c b/fs/sysfs/dir.c
index 1694c01e8b84..33ee798d421c 100644
--- a/fs/sysfs/dir.c
+++ b/fs/sysfs/dir.c
@@ -114,6 +114,32 @@ int sysfs_publish_dir(struct kobject *kobj)
}
EXPORT_SYMBOL_IF_KUNIT(sysfs_publish_dir);
+/**
+ * sysfs_dir_staged - whether a kobject's directory is still unpublished
+ * @kobj: object to query
+ *
+ * True while @kobj's directory belongs to a staged subtree, either because it
+ * was created staged or because it was created underneath one -- kernfs marks
+ * every node it adds inside a staged subtree, so the answer holds at any depth.
+ *
+ * The read is advisory, in the shape kernfs_staged_lock() uses while it climbs:
+ * publication clears the flag under the staged subtree mutex, which this query
+ * does not take. A %true answer therefore means "staged a moment ago", and a
+ * caller acting on it must re-establish authority under whatever lock
+ * serializes it against publication. A %false answer cannot be stale for the
+ * caller that created the directory itself: the flag is set in that caller's
+ * own thread and publication only ever clears it.
+ *
+ * Return: %true if the directory is still staged.
+ */
+bool sysfs_dir_staged(const struct kobject *kobj)
+{
+ struct kernfs_node *kn = READ_ONCE(kobj->sd);
+
+ /* advisory read; the caller re-establishes authority (see above) */
+ return kn && (data_race(kn->flags) & KERNFS_STAGED);
+}
+
/**
* sysfs_remove_dir - remove an object's directory.
* @kobj: object.
diff --git a/include/linux/device.h b/include/linux/device.h
index 74701a8aa9d2..89dc57ab3c94 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -607,6 +607,21 @@ struct device_physical_location {
* sysfs-root lock cost stops scaling with the number of nodes in
* the directory, which matters when many devices (for example
* SR-IOV virtual functions) are registered in parallel.
+ * Devices registered from inside such a window -- synchronously,
+ * from the opted-in device_add() itself, as one of the hooks it
+ * calls may do; a foreign thread registering members is outside
+ * this contract -- have their uevents suppressed until
+ * publication. Every uevent emitted in the window is dropped;
+ * what is replayed afterwards is the addition, as KOBJ_ADD
+ * followed by KOBJ_BIND if a driver is bound by then.
+ * Publication has to succeed before queued members are
+ * announced. If top-level registration fails, queued members
+ * are not replayed and remain suppressed; registrations made by
+ * an add notification must be removed by its matching remove
+ * notification. A device that is already suppressed when it is
+ * detected is left untouched, that state belonging to whoever set
+ * it, and a device that has itself opted in cannot nest inside
+ * another window -- its publication is rejected.
* Must be set before device_add().
* @DEV_FLAG_COUNT: Number of defined struct_device_flags.
*/
diff --git a/include/linux/sysfs.h b/include/linux/sysfs.h
index b537a5b52e1a..5538858f31f7 100644
--- a/include/linux/sysfs.h
+++ b/include/linux/sysfs.h
@@ -398,6 +398,7 @@ struct sysfs_ops {
int __must_check sysfs_create_dir_ns(struct kobject *kobj, const struct ns_common *ns);
int __must_check sysfs_publish_dir(struct kobject *kobj);
+bool sysfs_dir_staged(const 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);
@@ -513,6 +514,11 @@ static inline int sysfs_publish_dir(struct kobject *kobj)
return 0;
}
+static inline bool sysfs_dir_staged(const struct kobject *kobj)
+{
+ return false;
+}
+
static inline void sysfs_remove_dir(struct kobject *kobj)
{
}
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 7/8] PCI/IOV: Register virtual functions through the staged sysfs path
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
` (5 preceding siblings ...)
2026-09-11 17:43 ` [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 8/8] vfio: Opt the group and vfio-dev class devices into staged sysfs Pavol Sakac
7 siblings, 0 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
SR-IOV enablement registers up to thousands of virtual functions, each
building its own sysfs directory tree under the sysfs root's
kernfs_rwsem. The sibling VF additions that an earlier patch in this
series runs concurrently within one PF, and VF enables running on several
PFs at once, all serialize on that one lock.
Opt VF registration in to staged registration by calling
dev_set_sysfs_staged() on the VF's device before pci_device_add() in
__pci_iov_add_virtfn(), the single path through which every VF is added:
the synchronous VF0 add, the async workers, and the pci_iov_add_virtfn()
wrapper. Each VF's directory is then built off the root lock and
published in one step.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
drivers/pci/iov.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/drivers/pci/iov.c b/drivers/pci/iov.c
index ac2ddda4bf14..79258d4791a3 100644
--- a/drivers/pci/iov.c
+++ b/drivers/pci/iov.c
@@ -387,6 +387,8 @@ static int __pci_iov_add_virtfn(struct pci_dev *dev, struct pci_bus *bus,
BUG_ON(rc);
}
+ /* must precede pci_device_add(); see DEV_FLAG_SYSFS_STAGED */
+ dev_set_sysfs_staged(&virtfn->dev);
pci_device_add(virtfn, virtfn->bus);
rc = pci_iov_sysfs_link(dev, virtfn, id);
if (rc)
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [RFC PATCH 8/8] vfio: Opt the group and vfio-dev class devices into staged sysfs
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
` (6 preceding siblings ...)
2026-09-11 17:43 ` [RFC PATCH 7/8] PCI/IOV: Register virtual functions through the staged sysfs path Pavol Sakac
@ 2026-09-11 17:43 ` Pavol Sakac
7 siblings, 0 replies; 11+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:43 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich
Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
The vfio group chardev and the vfio-dev class device are created once per
VF during a registration storm and satisfy the staged-sysfs opt-in
conditions trivially: class devices never match drivers, no
class_interface consumers exist for either class, and the KOBJ_ADD uevent
fires after publication, so no consumer can observe a partially built
directory. Without this, the concurrent registrations that creating the
group chardev outside vfio.group_lock unleashes convoy on the eager
kernfs_rwsem path instead.
Both devices live in class glue directories, which staged registration
covers.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
drivers/vfio/group.c | 7 +++++++
drivers/vfio/vfio_main.c | 3 +++
2 files changed, 10 insertions(+)
diff --git a/drivers/vfio/group.c b/drivers/vfio/group.c
index 692381151303..ee41e1253f3d 100644
--- a/drivers/vfio/group.c
+++ b/drivers/vfio/group.c
@@ -659,6 +659,13 @@ vfio_group_find_or_create(struct device *dev, struct iommu_group *iommu_group,
list_add(&new->vfio_next, &vfio.group_list);
mutex_unlock(&vfio.group_lock);
+ /*
+ * Class devices never match drivers and vfio has no class_interface
+ * consumers, so nothing can observe the group chardev's directory
+ * before device_add() publishes it in one step.
+ */
+ dev_set_sysfs_staged(&new->dev);
+
/*
* Hold back device_add()'s KOBJ_ADD until publication; on failure,
* suppression also keeps the device_add() unwind from emitting an
diff --git a/drivers/vfio/vfio_main.c b/drivers/vfio/vfio_main.c
index 423ead48aafe..7abdfbd54b9d 100644
--- a/drivers/vfio/vfio_main.c
+++ b/drivers/vfio/vfio_main.c
@@ -383,6 +383,9 @@ static int __vfio_register_dev(struct vfio_device *device,
goto err_out;
}
+ /* Staged opt-in: same conditions as vfio_group_find_or_create(). */
+ dev_set_sysfs_staged(&device->device);
+
ret = vfio_device_add(device);
if (ret)
goto err_out;
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration
2026-09-11 17:43 ` [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration Pavol Sakac
@ 2026-09-12 13:14 ` Andy Shevchenko
0 siblings, 0 replies; 11+ messages in thread
From: Andy Shevchenko @ 2026-09-12 13:14 UTC (permalink / raw)
To: Pavol Sakac
Cc: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich, driver-core, linux-kernel, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
On Fri, Sep 11, 2026 at 07:43:37PM +0200, Pavol Sakac wrote:
> Add a staged_device KUnit suite under its own
> CONFIG_STAGED_DEVICE_KUNIT_TEST symbol exercising the staged mechanism.
> kobject_add() on a kobject with ->sd_staged builds the directory staged
> but does not publish it, since only device_add() does, so the tests
> drive the window directly: stage, then populate and observe, then
> sysfs_publish_dir(). The cases cover publication of plain, named-group
> and merged-group content, the failure modes of publication itself, abort
> without publication, foreign-thread access, concurrent windows including
> two that share one hashed staged mutex, glue-directory sharing, and
> bus-klist membership by the time device_add() returns. The suite also
> covers the KERNFS_STAGED_TOP/KERNFS_HAS_MMAP aliasing on a staged
> mmap-capable bin file.
>
> The publication-misuse cases intentionally trigger the publication
> guards' WARN backtraces, and the duplicate-name cases trigger
> sysfs_warn_dup() splats: those backtraces are expected output of a
> passing run, not failures. The suite is therefore unsuitable for
> runners that set panic_on_warn, which turns the first intentional splat
> into a panic, and does not follow KUNIT_ALL_TESTS: it runs only when its
> symbol is enabled explicitly.
...
> drivers/base/test/.kunitconfig | 1 +
Same question, why is this file got modified?
...
> +++ b/drivers/base/test/.kunitconfig
> @@ -1,3 +1,4 @@
> CONFIG_KUNIT=y
> CONFIG_DM_KUNIT_TEST=y
> CONFIG_GLUE_DIR_KUNIT_TEST=y
> +CONFIG_STAGED_DEVICE_KUNIT_TEST=y
It seems this series is based on some other series?
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window
2026-09-11 17:43 ` [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window Pavol Sakac
@ 2026-09-12 13:20 ` Andy Shevchenko
0 siblings, 0 replies; 11+ messages in thread
From: Andy Shevchenko @ 2026-09-12 13:20 UTC (permalink / raw)
To: Pavol Sakac
Cc: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
Danilo Krummrich, driver-core, linux-kernel, Xu Yang,
Bartosz Golaszewski, Bjorn Helgaas, linux-pci, Alex Williamson,
kvm, nh-open-source
On Fri, Sep 11, 2026 at 07:43:38PM +0200, Pavol Sakac wrote:
> Staged registration publishes a device's subtree in one step, so nothing
> observes the device before it is complete. That does not hold for a
> device registered from inside the window: device_add() calls hooks that
> can register a child of the device being added, and such a child takes
> the eager path and announces itself with KOBJ_ADD while its own path
> resolves to nothing. Nothing replays that event, so a consumer that
> cannot open the DEVPATH never learns of the child. Neither opt-in in
> this series reaches that case: nothing attaches a wakeup source to a
> PCI VF or to the VFIO class devices, and pci_acpi_setup() enables
> wakeup only for a bridge that can do D3. It is reachable by code the
> opt-in caller does not control: dpm_sysfs_add() registers a
> wakeup-source device whenever power.wakeup is already attached, and
> wakeup_source_register() registers one directly for a device that is
> already registered, which a staged device is. The driver core
> therefore handles the case rather than forbidding it in the opt-in
> contract.
>
> Defer such a registration, composing the idiom block/genhd.c already uses
> for a disk's partition tree: suppress the uevents, complete the tree,
> then unsuppress and replay KOBJ_ADD. A device whose directory kernfs
> marked staged is enqueued suppressed on the window of the nearest
> ancestor that opted in, and the window is drained as the ancestor's
> device_add() returns, just after its own KOBJ_ADD, each member's ADD
> delivered in registration order followed by BIND where a driver bound
> in the window. An ancestor whose owner suppresses its uevent and
> replays it after registration will see members announced before its
> replayed ADD. The window lives in a global hashtable keyed by the
> opted-in device, so no struct grows.
>
> Members are expected to be registered synchronously by the opted-in
> device_add() that owns the window, the only shape the driver core can
> reason about here. Raw uevent_suppress is not reused as the detection
> marker, precisely because a subsystem may already own it; a device found
> already suppressed is left out of the window entirely. An in-window
> KOBJ_CHANGE is dropped rather than replayed, as in the genhd case, since
> only the addition can be reconstructed afterwards. A failed opted-in
> registration closes its window without replaying anything and leaves
> suppression set, so a member's KOBJ_REMOVE is dropped too and userspace
> never sees a removal for an addition it never saw.
>
> The staged_device suite gains five cases for the window: the replay of a
> member's addition once the owner's device_add() returns, the aborted
> window that replays nothing and leaves suppression set, a member behind
> a class glue directory, the reconstructed KOBJ_BIND of a driver bound in
> the window, and a member found already suppressed being left out of the
> window.
>
> If you would rather not carry this until a caller needs it, it can be
> dropped and the restriction stated as an opt-in condition instead.
...
> + member->dev = get_device(dev);
> +
> + spin_lock(&staged_windows_lock);
> + hash_for_each_possible(staged_windows, win, node, (unsigned long)top) {
> + if (win->top != top)
> + continue;
> + dev_set_uevent_suppress(dev, 1);
> + list_add_tail(&member->node, &win->members);
> + member = NULL;
> + break;
> + }
> + spin_unlock(&staged_windows_lock);
> +
> + if (member) {
> + put_device(dev);
With this condition it makes sense to use the same argument, id est
member->dev.
> + kfree(member);
> + }
...
> +/*
> + * Records the child's replayed uevents as they are dispatched through the
> + * bus uevent callback -- which runs upstream of netlink broadcast, so this
> + * observes action-specific kernel dispatch and ordering, not reception by
> + * any consumer. Scoped to the active context's child: the same static bus
> + * serves another case, and teardown emits further events.
> + */
> +static int staged_test_bus_uevent(const struct device *dev,
> + struct kobj_uevent_env *env)
> +{
> + struct staged_window_ctx *c = staged_window_ctx;
> + int i;
Why signed?
> + if (!c || dev != c->child)
> + return 0;
> +
> + for (i = 0; i < env->envp_idx; i++) {
for (int i = 0; i < env->envp_idx; i++) {
> + if (!strcmp(env->envp[i], "ACTION=add")) {
> + c->add_uevents++;
> + c->add_seq = ++c->uevent_seq;
> + } else if (!strcmp(env->envp[i], "ACTION=bind")) {
> + c->bind_uevents++;
> + c->bind_seq = ++c->uevent_seq;
Why preincrements?
> + }
> + }
Missing blank line.
> + return 0;
> +}
...
> +static struct attribute in_window_attr = {
> + .name = "in_window_attr", .mode = 0644,
> +};
I believe you haven't read the generated code. Do you understand what it does
(I mean the whole your patch and patch series)?
...
> +static const struct attribute_group *in_window_grps[] = {
> + &in_window_grp, NULL,
> +};
Same style issue.
Also we have __ATRIBUTE_GROUPS() macro.
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-09-12 13:20 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 17:43 [RFC PATCH 0/8] kernfs, driver core: staged sysfs registration Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 1/8] kernfs: factor out reusable directory helpers Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 2/8] kernfs: add staged directory creation and publication Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 3/8] sysfs: add opt-in " Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 4/8] driver core: Register opted-in devices through the staged sysfs path Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration Pavol Sakac
2026-09-12 13:14 ` Andy Shevchenko
2026-09-11 17:43 ` [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window Pavol Sakac
2026-09-12 13:20 ` Andy Shevchenko
2026-09-11 17:43 ` [RFC PATCH 7/8] PCI/IOV: Register virtual functions through the staged sysfs path Pavol Sakac
2026-09-11 17:43 ` [RFC PATCH 8/8] vfio: Opt the group and vfio-dev class devices into staged sysfs Pavol Sakac
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox