From: Pavol Sakac <sakacpav@amazon.de>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Tejun Heo <tj@kernel.org>,
"Rafael J . Wysocki" <rafael@kernel.org>,
Danilo Krummrich <dakr@kernel.org>
Cc: <driver-core@lists.linux.dev>, <linux-kernel@vger.kernel.org>,
"Andy Shevchenko" <andriy.shevchenko@linux.intel.com>,
Xu Yang <xu.yang_2@nxp.com>,
Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>,
Bjorn Helgaas <bhelgaas@google.com>, <linux-pci@vger.kernel.org>,
Alex Williamson <alex@shazbot.org>, <kvm@vger.kernel.org>,
<nh-open-source@amazon.com>
Subject: [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window
Date: Fri, 11 Sep 2026 19:43:38 +0200 [thread overview]
Message-ID: <20260911174414.97060-6-sakacpav@amazon.de> (raw)
In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de>
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
next prev parent reply other threads:[~2026-09-11 17:47 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Pavol Sakac [this message]
2026-09-12 13:20 ` [RFC PATCH 6/8] driver core: Defer uevents for devices registered in a staged window 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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260911174414.97060-6-sakacpav@amazon.de \
--to=sakacpav@amazon.de \
--cc=alex@shazbot.org \
--cc=andriy.shevchenko@linux.intel.com \
--cc=bartosz.golaszewski@oss.qualcomm.com \
--cc=bhelgaas@google.com \
--cc=dakr@kernel.org \
--cc=driver-core@lists.linux.dev \
--cc=gregkh@linuxfoundation.org \
--cc=kvm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-pci@vger.kernel.org \
--cc=nh-open-source@amazon.com \
--cc=rafael@kernel.org \
--cc=tj@kernel.org \
--cc=xu.yang_2@nxp.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox