From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-010.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-010.esa.us-west-2.outbound.mail-perimeter.amazon.com [52.12.53.23]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B3AB6356754; Fri, 11 Sep 2026 17:44:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=52.12.53.23 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148672; cv=none; b=Al/WTmuLhMzdvRAtX47wKeZOPwChmP+RWDQICY/66akbsjK9TyXBfgsVn7jXH9zwJQlAyIzvHo38lSlOXOVAiF1I14Y7klN3UD5LfEtbs55olXrlRPCSl6twDmuY1t9tz8QeU4fbSp2bcOs9oJa2V8z34yEfwFx0YRe89P/sZAw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148672; c=relaxed/simple; bh=8r4gc3xKoWIXaeHEbv2OHiJwyaQA1NKjsYvm9JZrpKM=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=PQ/QxDpGjh6DXTDfQH7JdGj8FMLKT6O/e7DC1F5pgz+UhgUJA96Zu0ln7hjcNqem67GVWUZUplUjV1LMvTALFwSCU+qjDuQ32ZfWGGNO6p9sV5UPY8WDgffgOPYWZJSTamm39Uwf9bulewniLezW2BiEgxgRMa7gAPAxlzBlvtE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=pqiqPXC7; arc=none smtp.client-ip=52.12.53.23 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="pqiqPXC7" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148667; x=1820684667; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=6iOtJLCOMaVu1u7N9u6FCZ3yXZzuE0TLkNT/UH0NMLg=; b=pqiqPXC7pojfsduBDyeGRYIfzCrnODduaNceQknsHQRdO5qLv11DnCdR 6F/mnGUaI0Pw4f05b19DAQaWRkmhyzqUbzPsBUWphWPAVYm6qT02LX7FY qiy+wGB6Anu9lQJrsJFLGCoBa2JOQ+17NX5Kl4a1GHJiPLcLjGK3SSdbK poPJMfDW4WhVvnXe/fVA7UKEZURXVQyQIPUxS4mm7siTozsGs4sFyo/Z9 sCIUeKmLneZWeoEs8CeDd3FbI4sIKqRUj4mn5sGHMAt7CJwjYZvHdrAUy f6qZaAhBVvc/YCbYdle/tLBZ2QOV3+79NkZHD6L8wbWcbeUQsNxbjO6T+ Q==; X-CSE-ConnectionGUID: d9ta3Z/UTFish95eKkRVXA== X-CSE-MsgGUID: 38T1FJ6wQVmiXLKH9NcH2w== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28317879" Received: from ip-10-5-0-115.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.0.115]) by internal-pdx-out-010.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:44:18 +0000 Received: from EX19MTAUWB001.ant.amazon.com [205.251.233.51:6341] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.6.93:2525] with esmtp (Farcaster) id 9b6f30e1-fa4b-42b7-8f8b-344e3e7b0c22; Fri, 11 Sep 2026 17:44:18 +0000 (UTC) X-Farcaster-Flow-ID: 9b6f30e1-fa4b-42b7-8f8b-344e3e7b0c22 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWB001.ant.amazon.com (10.250.64.248) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:44:18 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:44:15 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 1/8] kernfs: factor out reusable directory helpers Date: Fri, 11 Sep 2026 19:43:33 +0200 Message-ID: <20260911174414.97060-1-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D037UWB002.ant.amazon.com (10.13.138.121) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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, r= ight); } =20 +/* 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 =3D parent ? parent->iattr : NULL; + + if (ps_iattr) { + ktime_get_real_ts64(&ps_iattr->ia_ctime); + ps_iattr->ia_mtime =3D 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 =3D 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); =20 + 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 =3D __kernfs_link_sibling(kn); + if (ret) + return ret; + /* successfully added, account subdir number */ + kn_parent =3D kernfs_parent(kn); down_write(&kernfs_root(kn)->kernfs_iattr_rwsem); if (kernfs_type(kn) =3D=3D KERNFS_DIR) kn_parent->dir.subdirs++; @@ -934,7 +965,6 @@ struct kernfs_node *kernfs_find_and_get_node_by_id(stru= ct kernfs_root *root, int kernfs_add_one(struct kernfs_node *kn) { struct kernfs_root *root =3D 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) =20 /* Update timestamps on the parent */ down_write(&root->kernfs_iattr_rwsem); - - ps_iattr =3D parent->iattr; - if (ps_iattr) { - ktime_get_real_ts64(&ps_iattr->ia_ctime); - ps_iattr->ia_mtime =3D ps_iattr->ia_ctime; - } - + kernfs_update_parent_times(parent); up_write(&root->kernfs_iattr_rwsem); =20 /* @@ -990,25 +1014,24 @@ int kernfs_add_one(struct kernfs_node *kn) } =20 /** - * 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 =3D parent->dir.children.rb_node; bool has_ns =3D kernfs_ns_enabled(parent); unsigned int hash; =20 - lockdep_assert_held(&kernfs_root(parent)->kernfs_rwsem); - if (has_ns !=3D (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 ker= nfs_node *parent, return NULL; } =20 +/* + * The asserted kernfs_rwsem hold (write or read) also covers the RCU-mana= ged + * 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 kernf= s_root *root) return root->kn; } =20 +/* + * 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 =3D kernfs_new_node(parent, name, mode | S_IFDIR, uid, gid, + KERNFS_DIR | extra_flags); + if (!kn) + return ERR_PTR(-ENOMEM); + + kn->dir.root =3D parent->dir.root; + kn->ns =3D ns; + kn->priv =3D 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 kern= fs_node *parent, int rc; =20 /* allocate */ - kn =3D kernfs_new_node(parent, name, mode | S_IFDIR, - uid, gid, KERNFS_DIR); - if (!kn) - return ERR_PTR(-ENOMEM); - - kn->dir.root =3D parent->dir.root; - kn->ns =3D ns; - kn->priv =3D priv; + kn =3D __kernfs_create_dir(parent, name, mode, uid, gid, priv, ns, 0); + if (IS_ERR(kn)) + return kn; =20 /* link in */ rc =3D kernfs_add_one(kn); @@ -1272,15 +1327,12 @@ struct kernfs_node *kernfs_create_empty_dir(struct = kernfs_node *parent, int rc; =20 /* allocate */ - kn =3D 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 =3D __kernfs_create_dir(parent, name, 0555, + GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, NULL, NULL, 0); + if (IS_ERR(kn)) + return kn; =20 kn->flags |=3D KERNFS_EMPTY_DIR; - kn->dir.root =3D parent->dir.root; - kn->ns =3D NULL; - kn->priv =3D NULL; =20 /* link in */ rc =3D 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 =3D - parent ? parent->iattr : NULL; - down_write(&kernfs_root(kn)->kernfs_iattr_rwsem); =20 kernfs_clear_inode_nlink(pos); =20 /* update timestamps on the parent */ - if (ps_iattr) { - ktime_get_real_ts64(&ps_iattr->ia_ctime); - ps_iattr->ia_mtime =3D ps_iattr->ia_ctime; - } + kernfs_update_parent_times(parent); =20 up_write(&kernfs_root(kn)->kernfs_iattr_rwsem); kernfs_put(pos); --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-002.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-002.esa.us-west-2.outbound.mail-perimeter.amazon.com [44.246.1.125]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1014838DC6C; Fri, 11 Sep 2026 17:44:57 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=44.246.1.125 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148711; cv=none; b=XXj0ubYEM0RkiZyHms75QG5cdzbg3N6I+7w1Tl85TWPz2/3PNlG9vil/CqyCojXyPfIWanx++6EXKDWbGalva89rECjrQNzHBWyNNZLYQtSCmDGW6yNeq29Y6fuvXY+/TYDF7MHZl/6cAWXXjudwIq+WRkFuCMdverun2j6FE2w= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148711; c=relaxed/simple; bh=u5AI544MjmW3ixW1cudPQM3iDWgpRHlEHxhYxBli50k=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=aoOHVb6JsFAmbpZkiENYgF7caXWAPK2Fcv5Vi0NKkw8ppF88C7AN6fyDlmiVLUgs6//jyy0TmBPtwjAhCLBqslk/ZGe14Jb+MT7T7Zy0Q+HHb0k2GYMatbhVE0bGQSpkyWeg3oRChDD+PXXQKw8m9EaiK6e5qnbS+PFzJdONOsw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=DuqyauPP; arc=none smtp.client-ip=44.246.1.125 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="DuqyauPP" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148700; x=1820684700; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=yflPJiaY48xoge6J7Zyps37Hewsbf2B/H30hL8plHtw=; b=DuqyauPPSLBHPfRZJbvdWl/OxPthk1m57voIwrqspDi7ZxD8z9uXMn3+ +8A7UH6IWBTkJ2OW/JhVhPQxW1FnBKxh6nran/GJ5T7QGiLMD4GDEyO9H UmMKsaQfgBSKD/42lksSbBCwhwhSq215K8QX/nNTuYb8CrNjMmeipEwfr tUh66NrGLOa47zHoa6Xx0VN53Npphtv2yr58gVk0mXHOM4iGkBZz+iB47 vHfxWx4WL3xyUOfE3KWxxAnM+D0zh43KSVI56oRuXFMO74ElZjrmU7qtW zrFN91ZpntMkwn1DRSxG8zAFgLjKeHx2Y7X/cpvwDPl9nvF94W01HL1lE Q==; X-CSE-ConnectionGUID: pxu7p6JGSg2BJMYZ2wnrbg== X-CSE-MsgGUID: lybOeRnSQWu2yp+vXMdnkQ== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28450836" Received: from ip-10-5-6-203.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.6.203]) by internal-pdx-out-002.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:44:53 +0000 Received: from EX19MTAUWC002.ant.amazon.com [205.251.233.111:10763] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.22.113:2525] with esmtp (Farcaster) id 9ab215f7-0d7b-41d1-9ccb-0792ab733d75; Fri, 11 Sep 2026 17:44:52 +0000 (UTC) X-Farcaster-Flow-ID: 9ab215f7-0d7b-41d1-9ccb-0792ab733d75 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWC002.ant.amazon.com (10.250.64.143) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:44:52 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:44:49 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 2/8] kernfs: add staged directory creation and publication Date: Fri, 11 Sep 2026 19:43:34 +0200 Message-ID: <20260911174414.97060-2-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D040UWB001.ant.amazon.com (10.13.138.82) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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_no= de *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; } =20 +/* 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) =3D=3D 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 =3D 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. Publicati= on + * and teardown are one-way, so retries terminate. The climb needs no loc= k: + * 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 sa= y. + */ +static struct mutex *kernfs_staged_lock(struct kernfs_node *parent) +{ + for (;;) { + struct kernfs_node *top =3D parent; + struct mutex *lock; + + for (;;) { + /* advisory; authoritative re-check under L(top) */ + unsigned short flags =3D READ_ONCE(top->flags); + + if (!(flags & KERNFS_STAGED)) + return NULL; + /* STAGED_TOP is DIR-only (see its definition) */ + if ((flags & KERNFS_TYPE_MASK) =3D=3D KERNFS_DIR && + (flags & KERNFS_STAGED_TOP)) + break; + /* staged interior: climb */ + top =3D kernfs_staged_parent(top); + } + + lock =3D 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 =3D kernfs_ns_enabled(parent); + + if (has_ns !=3D (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) !=3D 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 *loc= k) +{ + struct kernfs_node *parent =3D kernfs_staged_parent(kn); + int ret; + + lockdep_assert_held(lock); + + ret =3D 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 =3D kernfs_name_hash(kernfs_rcu_name(kn), kn->ns); + ret =3D __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) =3D=3D 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 =3D kernfs_root(kn); struct kernfs_node *parent; - bool has_ns; + struct mutex *lock; int ret; =20 + /* + * Staged parent: link under its subtree mutex, off the per-root + * rwsems. NULL once published, then the locked path below runs. + */ + lock =3D kernfs_staged_lock(kernfs_staged_parent(kn)); + if (lock) { + ret =3D kernfs_add_one_staged(kn, lock); + mutex_unlock(lock); + return ret; + } + down_write(&root->kernfs_rwsem); parent =3D kernfs_parent(kn); =20 - ret =3D -EINVAL; - has_ns =3D kernfs_ns_enabled(parent); - if (WARN(has_ns !=3D (bool)kn->ns, KERN_WARNING "kernfs: ns %s in '%s' fo= r '%s'\n", - has_ns ? "required" : "invalid", - kernfs_rcu_name(parent), kernfs_rcu_name(kn))) - goto out_unlock; - - if (kernfs_type(parent) !=3D KERNFS_DIR) - goto out_unlock; - - ret =3D -ENOENT; - if (parent->flags & (KERNFS_REMOVING | KERNFS_EMPTY_DIR)) + ret =3D kernfs_add_precheck(parent, kn); + if (ret) goto out_unlock; =20 kn->hash =3D 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 in= side + * 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 ke= rnfs_node *parent, const struct ns_common *ns) { struct kernfs_node *kn; - struct kernfs_root *root =3D kernfs_root(parent); + struct kernfs_root *root; + struct mutex *lock; + + /* staged parent: serialize on its subtree mutex; NULL once published */ + lock =3D kernfs_staged_lock(parent); + if (lock) { + rcu_read_lock(); + kn =3D __kernfs_find_ns(parent, name, ns); + rcu_read_unlock(); + kernfs_get(kn); + mutex_unlock(lock); + return kn; + } =20 + root =3D kernfs_root(parent); down_read(&root->kernfs_rwsem); kn =3D kernfs_find_ns(parent, name, ns); kernfs_get(kn); @@ -1313,6 +1495,41 @@ struct kernfs_node *kernfs_create_dir_ns(struct kern= fs_node *parent, return ERR_PTR(rc); } =20 +/** + * 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 collecti= on + * 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 stag= ed + * 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 *k= n) { lockdep_assert_held_write(&kernfs_root(kn)->kernfs_rwsem); =20 - kn->flags |=3D KERNFS_ACTIVATED; + /* flags is read locklessly by the staged funnel; mark its racers */ + WRITE_ONCE(kn->flags, kn->flags | KERNFS_ACTIVATED); =20 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); } =20 +/** + * 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 colli= de + * with a live sibling, link @kn into the parent's children collection + * (bumping the directory revision that invalidates negative dentries), th= en + * walk the subtree clearing KERNFS_STAGED and activating every node. + * + * Readers see the transition via lock pairing -- kernfs_rwsem for VFS rea= ders, + * 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 mak= es it + * visible with kernfs_activate(). + * + * @kn must still be staged, and its parent must not itself be staged (a s= taged + * parent's children are serialized by a different subtree mutex); no in-t= ree + * caller does either. + * + * Return: %0 on success, -EEXIST on a name collision, -ENOENT if the pare= nt 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 =3D kernfs_staged_parent(kn); + struct kernfs_root *root =3D 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 =3D !(root->flags & KERNFS_ROOT_CREATE_DEACTIVATED); + lock =3D kernfs_staged_mutex_ptr(kn); + + down_write(&root->kernfs_rwsem); + mutex_lock(lock); + + /* authoritative re-check: the advisory reads above can race */ + ret =3D -EINVAL; + if (WARN_ON_ONCE(!(READ_ONCE(kn->flags) & KERNFS_STAGED))) + goto out; + + ret =3D -ENOENT; + if (parent->flags & (KERNFS_REMOVING | KERNFS_EMPTY_DIR)) + goto out; + + /* the write hold covers the RCU-managed name from here on */ + kn->hash =3D kernfs_name_hash(kernfs_rcu_name(kn), kn->ns); + + ret =3D 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 =3D NULL; + while ((pos =3D kernfs_next_descendant_post(pos, kn))) { + WRITE_ONCE(pos->flags, + pos->flags & ~kernfs_staged_clear_mask(pos)); + if (activate) + kernfs_activate_one(pos); + } + ret =3D 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); =20 if (show) { - kn->flags &=3D ~KERNFS_HIDDEN; + WRITE_ONCE(kn->flags, kn->flags & ~KERNFS_HIDDEN); if (kn->flags & KERNFS_ACTIVATED) kernfs_activate_one(kn); } else { - kn->flags |=3D 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_n= ode *kn) } } =20 +/** + * 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 r= each + * 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 pari= ty, + * 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 *loc= k) +{ + struct kernfs_node *pos; + + lockdep_assert_held(lock); + + do { + struct kernfs_node *parent; + + pos =3D kernfs_leftmost_descendant(kn); + kernfs_get(pos); + parent =3D 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) =3D=3D 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 !=3D 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 =3D NULL; while ((pos =3D kernfs_next_descendant_post(pos, kn))) { - pos->flags |=3D 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; =20 if (!kn) return; =20 + /* staged subtree: tear down under its mutex; NULL once published */ + lock =3D kernfs_staged_lock(kn); + if (lock) { + kernfs_remove_staged(kn, lock); + mutex_unlock(lock); + return; + } + root =3D kernfs_root(kn); =20 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 |=3D KERNFS_SUICIDAL; + WRITE_ONCE(kn->flags, kn->flags | KERNFS_SUICIDAL); __kernfs_remove(kn); - kn->flags |=3D KERNFS_SUICIDED; + WRITE_ONCE(kn->flags, kn->flags | KERNFS_SUICIDED); ret =3D true; } else { wait_queue_head_t *waitq =3D &kernfs_root(kn)->deactivate_waitq; @@ -1949,6 +2333,7 @@ int kernfs_remove_by_name_ns(struct kernfs_node *pare= nt, const char *name, { struct kernfs_node *kn; struct kernfs_root *root; + struct mutex *lock; =20 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 *par= ent, const char *name, return -ENOENT; } =20 + /* staged parent: serialize on its subtree mutex; NULL once published */ + lock =3D kernfs_staged_lock(parent); + if (lock) { + bool found; + + rcu_read_lock(); + kn =3D __kernfs_find_ns(parent, name, ns); + rcu_read_unlock(); + found =3D kn; + if (kn) { + kernfs_get(kn); + kernfs_remove_staged(kn, lock); + kernfs_put(kn); + } + mutex_unlock(lock); + return found ? 0 : -ENOENT; + } + root =3D 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; =20 - for (count =3D 0; count < NR_KERNFS_LOCKS; count++) + for (count =3D 0; count < NR_KERNFS_LOCKS; count++) { mutex_init(&kernfs_locks->node_mutex[count]); + mutex_init(&kernfs_locks->staged_mutex[count]); + } } =20 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]; }; =20 enum kernfs_node_type { @@ -118,6 +127,28 @@ enum kernfs_node_flag { KERNFS_EMPTY_DIR =3D 0x1000, KERNFS_HAS_RELEASE =3D 0x2000, KERNFS_REMOVING =3D 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 =3D 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 =3D KERNFS_HAS_MMAP, }; =20 /* @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); } =20 +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, --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-002.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-002.esa.us-west-2.outbound.mail-perimeter.amazon.com [44.246.1.125]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BE4CA3C1412; Fri, 11 Sep 2026 17:45:28 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=44.246.1.125 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148732; cv=none; b=XTFsGSzqErN15PI0lUUV3pYBafZ7wD3YBHYk9ueacNpoRBqpKBnDV+KM8axud4peEYd3llBlRE51l46oV9jit0DEiP0NggabHrVtsoj7S3qadq6ekdJaDw0axppgD9v2fyFaDf67farsamZSQtEdf1b9sx1g6ixcvtzhIfi8gPk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148732; c=relaxed/simple; bh=N0xMLmQWTfo32a7EoonV5A+AHDG36d/UGOK+UwGgnus=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=KLaYRRnTjB87OcvhJ+/f4m3YrhuOI1+aOCCzonfUrW6t2TIAFPMZbDb37E6BJr8VJZN0qvmQc6gNSPYWFlXxnFy+A4ECmr7Jpx6KVnTrolia9KMCqUdBI5vtqWER7wAJNHJDvJzdYUKqHaFtIXjDp499uDDrs98TCiuQg0IPqZU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=a2c795US; arc=none smtp.client-ip=44.246.1.125 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="a2c795US" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148729; x=1820684729; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=asf+3vYzEZL5yRPty1Oajrmz0lrx99bnZzGiGsft6Lo=; b=a2c795USGgaVQzkOpUoZLzds+MwEMc0eNWfSk8oSB6O86WnGx6AoU+59 w2L52eWlDuFXolB/u96l73cQ8/aAIFORERMDjXc5LGJSuGdZYDI42KUUA 0UY16jXS2SUTA8RY5dy26LuOXMe1zMVpSTUkJO449VmJh561kiKgsFpmx GKVh+T4Jn6l3+vzSQSws4pIifrQKFCuPjS8UAtlqQw7F5Kj3GCyEQNhS1 6NKaGBar8TWOdEVPaHKQ1yJaJJUOllNiodrJqs9ViHonhhKWqyaQ2jJeV 26tSIACtUwzxc15/dZXLuB+J/1JK/BMDaqV3AydtdkLIBnMMq+THXNK49 g==; X-CSE-ConnectionGUID: P13SAVyuQE2ytQPa9TSm4w== X-CSE-MsgGUID: h4qo6XZUQdqa3N6FPu4lDg== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28450871" Received: from ip-10-5-6-203.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.6.203]) by internal-pdx-out-002.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:45:26 +0000 Received: from EX19MTAUWA001.ant.amazon.com [205.251.233.236:18050] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.4.34:2525] with esmtp (Farcaster) id 60b44e65-d9c1-4d2f-8af6-1b829b845994; Fri, 11 Sep 2026 17:45:26 +0000 (UTC) X-Farcaster-Flow-ID: 60b44e65-d9c1-4d2f-8af6-1b829b845994 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWA001.ant.amazon.com (10.250.64.217) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:45:26 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:45:23 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 3/8] sysfs: add opt-in staged directory creation and publication Date: Fri, 11 Sep 2026 19:43:35 +0200 Message-ID: <20260911174414.97060-3-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D038UWC002.ant.amazon.com (10.13.139.238) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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 @@ =20 #define pr_fmt(fmt) "sysfs: " fmt =20 +#include #include #include #include @@ -56,18 +57,63 @@ int sysfs_create_dir_ns(struct kobject *kobj, const str= uct ns_common *ns) =20 kobject_get_ownership(kobj, &uid, &gid); =20 - kn =3D kernfs_create_dir_ns(parent, kobject_name(kobj), 0755, uid, gid, - kobj, ns); + if (kobject_sd_staged(kobj)) + kn =3D kernfs_create_dir_ns_staged(parent, kobject_name(kobj), + 0755, uid, gid, kobj, ns); + else + kn =3D kernfs_create_dir_ns(parent, kobject_name(kobj), 0755, + uid, gid, kobj, ns); if (IS_ERR(kn)) { if (PTR_ERR(kn) =3D=3D -EEXIST) sysfs_warn_dup(parent, kobject_name(kobj)); return PTR_ERR(kn); } =20 - kobj->sd =3D 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; } =20 +/** + * 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 s= taged + * 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 =3D kernfs_publish(kobj->sd); + if (ret =3D=3D -EEXIST) { + struct kernfs_node *parent =3D 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; =20 #ifdef CONFIG_DEBUG_KOBJECT_RELEASE struct delayed_work release; #endif }; =20 +/** + * 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 =3D 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); =20 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 =20 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; } =20 +static inline int sysfs_publish_dir(struct kobject *kobj) +{ + return 0; +} + static inline void sysfs_remove_dir(struct kobject *kobj) { } --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-013.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-013.esa.us-west-2.outbound.mail-perimeter.amazon.com [34.218.115.239]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B06DF4A68A0; Fri, 11 Sep 2026 17:46:01 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=34.218.115.239 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148764; cv=none; b=gw6//7GdwHzF9OK426ew6B+hUga4vrGaERwYVfcKZxfWr6ZLEwcGQRlmnKZ9gphvCj8BmzG4mjfWHu2dnnXuf+CnjL7nxFn6FANak8ZKiwQK2+5GIc7p5RAhBPNb555G9lGA5mLTTN0mMvfnbxkjZzemIrAsak0E66tTWIH8xeI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148764; c=relaxed/simple; bh=6Jm+/yA08ADWx8YuefYsn7Jfo5CbJtNY4gBhQGkg/n8=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=FAQ37zvqHiTZMebBNUNPaZvPyFhcbDUa8CTOMklx7T4U4U44VW3/LkCcSg8dQNhdHNbt+RwGpYigwr9YbKeX8FWWBNmOWDHwa1SNNRecbceN4bkD60yVykqMxaxNL+4IEVM+MXKRxiQVI2/QMme7p0iurIZY/rDtsQwmVSSy/JQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=sMtp+Vln; arc=none smtp.client-ip=34.218.115.239 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="sMtp+Vln" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148762; x=1820684762; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=uG/E/cyk03CM0gEHgOG/UKkzaznPxEh34D8eww4v9Bk=; b=sMtp+VlnB6z205i3Ecl3rpTJbfomut7os2MWVyQe63f89r7MTM+rVdHH BhJK9cgUDyu3It5pzqERidA0WMMQrHxGSGCoU/JVmMLSzBTk9XkK+GNnQ w1KhsjhPPhyIiQyMCLsSAnw1VSXPiR7E+xtAT15gPYwfQXh6PfsEyiJbr 3pKGHwg/99uLAsA35yvGYBg017kIiV2KrRJ+Z1jM/fhxqXqsQOnLzAg9V vqrJklBqlq3IWKCW2OTJ9dZv549xgj5sgsFriIAlboFmLdORc/moXzxTU iM1RnxSjiOsXpUx2Aptn698O8RqvlP8xsvGXLwiSkME6MMuj2Z/O3p+Ol w==; X-CSE-ConnectionGUID: jKIpnsKwRmC2bq01T329Vw== X-CSE-MsgGUID: 3bWMuIMSTQSq3/49ZGqnNA== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28234943" Received: from ip-10-5-12-219.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.12.219]) by internal-pdx-out-013.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:46:00 +0000 Received: from EX19MTAUWC001.ant.amazon.com [205.251.233.105:21540] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.43.236:2525] with esmtp (Farcaster) id 24028455-f724-4a4f-9274-2dc7c3f15751; Fri, 11 Sep 2026 17:46:00 +0000 (UTC) X-Farcaster-Flow-ID: 24028455-f724-4a4f-9274-2dc7c3f15751 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWC001.ant.amazon.com (10.250.64.174) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:46:00 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:45:57 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 4/8] driver core: Register opted-in devices through the staged sysfs path Date: Fri, 11 Sep 2026 19:43:36 +0200 Message-ID: <20260911174414.97060-4-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D036UWB001.ant.amazon.com (10.13.139.133) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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); =20 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; =20 - 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; =20 out_subsys: @@ -603,6 +612,24 @@ int bus_add_device(struct device *dev) return error; } =20 +/** + * bus_add_device_publish - finish bus registration deferred past publicat= ion + * @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 =3D 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 =3D kobj; =20 + /* + * 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) =3D=3D 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); =20 + /* + * 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 =3D sysfs_publish_dir(&dev->kobj); + if (error) + goto DevAttrError; + bus_add_device_publish(dev); + } + if (MAJOR(dev->devt)) { error =3D 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 .offli= ne(). + * @DEV_FLAG_SYSFS_STAGED: Opt in to staged sysfs registration. device_ad= d() + * 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 =3D 7, DEV_FLAG_OFFLINE_DISABLED =3D 8, DEV_FLAG_OFFLINE =3D 9, + DEV_FLAG_SYSFS_STAGED =3D 10, =20 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); =20 #undef __create_dev_flag_accessors =20 --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-007.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-007.esa.us-west-2.outbound.mail-perimeter.amazon.com [52.34.181.151]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AD96A36B915; Fri, 11 Sep 2026 17:46:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=52.34.181.151 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148800; cv=none; b=O0ylRXQ0+lNTt8lDpXANFg+gZDnaJ3oVrD8n2GQ7KxuWNT+nRkRoSQcPHuNPD+G/wzjjwQWeYoacCBq2jen2CiO3qGp2X6l4difE40ajZ2OqAriYGZI1DRgAM7/oz2onNBkrq1Bmtg7PeVQoLPq7cN6rZwDv8dYYvQ+MhiVwduk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148800; c=relaxed/simple; bh=tYEEM+pqTIMxcUpu4QB6yzFG3Y9a5nUHZSyCWIQI0Ns=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=tTyecAi4735/WuTeJPBuFWE6l2j1ac0yVYE+MEW/ow0zdib1egwCQhth4Bg/BqPVzG9ennaGBG6TgIji58HV4Uc9PsPfKlLcx0yWUNhd01off51+SgGsZftFJEKPOWF/JSlWggHMKEwq0v+X139fYj5AqwOjXN9xrgPkSNGgsNw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=V6xTqt6m; arc=none smtp.client-ip=52.34.181.151 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="V6xTqt6m" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148797; x=1820684797; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=sKZ64wdWH+BmLrf29VVwFVMXgrosaMvFB76d7QnCB1I=; b=V6xTqt6mGHPv2Wi1s96NxuCjTJfZ/6ryMHMGrnaPu7DjwvywdgaLdAXt hl1XKNTtz3+9Qf3nfxDhpnzL5DKjgGj+UrnJL1lP89Ybzm5X5k/gAea+/ Sgpi92nbRFMv6wuMDI9akHHTomp7lTUNgi6jJ5JDL9IRBYVlYtIvwkxfx vE+TmnT18tuP9ISPj/ZOiVrE6JkrZ3gwgSbfKaUiRjacDOdeuJUlpa8N7 HzKCH+T9OcIntFMBZBbzbYcpXXBFTrtLY0Mqry4X3SaaF3S3GIsJrVreS e6Cw9OKDimfoXNv4qdmqbP06GgWYMUwu8ykL6rmP9VVDDqDquN9/0OVNU Q==; X-CSE-ConnectionGUID: ikbIdqU5SNSCAo0oKN0PlQ== X-CSE-MsgGUID: CWTblsGiRCuunVM3KEfvxQ== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28437819" Received: from ip-10-5-9-48.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.9.48]) by internal-pdx-out-007.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:46:34 +0000 Received: from EX19MTAUWB002.ant.amazon.com [205.251.233.48:13077] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.57.27:2525] with esmtp (Farcaster) id 8ce50a3e-98ee-478b-89d7-fda118f60ff4; Fri, 11 Sep 2026 17:46:34 +0000 (UTC) X-Farcaster-Flow-ID: 8ce50a3e-98ee-478b-89d7-fda118f60ff4 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWB002.ant.amazon.com (10.250.64.231) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:46:34 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:46:31 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 5/8] drivers: base: test: Add KUnit suite for staged sysfs registration Date: Fri, 11 Sep 2026 19:43:37 +0200 Message-ID: <20260911174414.97060-5-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D040UWB001.ant.amazon.com (10.13.138.82) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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=3Dy CONFIG_DM_KUNIT_TEST=3Dy CONFIG_GLUE_DIR_KUNIT_TEST=3Dy +CONFIG_STAGED_DEVICE_KUNIT_TEST=3Dy 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. =20 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) +=3D test_async_drive= r_probe.o =20 obj-$(CONFIG_DM_KUNIT_TEST) +=3D root-device-test.o obj-$(CONFIG_DM_KUNIT_TEST) +=3D platform-device-test.o +obj-$(CONFIG_STAGED_DEVICE_KUNIT_TEST) +=3D staged-device-test.o =20 obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) +=3D property-entry-test.o CFLAGS_property-entry-test.o +=3D $(DISABLE_STRUCTLEAK_PLUGIN) diff --git a/drivers/base/test/staged-device-test.c b/drivers/base/test/sta= ged-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 sys= fs + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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 *at= tr, + char *buf) +{ + return 0; +} + +static const struct sysfs_ops staged_sysfs_ops =3D { + .show =3D 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 =3D { + .sysfs_ops =3D &staged_sysfs_ops, + .release =3D 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 =3D kernfs_find_and_get(parent->sd, name); + bool found =3D !!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 directl= y, 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 =3D 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 =3D 0; i < n; i++) { + struct attribute *a =3D kunit_kzalloc(test, sizeof(*a), GFP_KERNEL); + char *nm; + + if (!a) + return -ENOMEM; + sysfs_attr_init(a); + nm =3D kunit_kmalloc(test, 16, GFP_KERNEL); + if (!nm) + return -ENOMEM; + snprintf(nm, 16, "attr%d", i); + a->name =3D nm; + a->mode =3D 0644; + ret =3D 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 =3D kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, priv); + + priv->parent =3D kobject_create_and_add("staged_kunit", kernel_kobj); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, priv->parent); + + test->priv =3D priv; + return 0; +} + +static void staged_test_exit(struct kunit *test) +{ + struct staged_test_priv *priv =3D 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 =3D { .name =3D "grp_attr", .mode =3D 064= 4 }; +static struct attribute *named_grp_attrs[] =3D { &grp_attr, NULL }; +static const struct attribute_group named_grp =3D { + .name =3D "ngroup", + .attrs =3D named_grp_attrs, +}; + +static struct attribute merge_attr =3D { .name =3D "merged", .mode =3D 064= 4 }; +static struct attribute *merge_attrs[] =3D { &merge_attr, NULL }; +static const struct attribute_group merge_grp =3D { + .name =3D "ngroup", + .attrs =3D merge_attrs, +}; + +static void staged_test_publish_end_to_end(struct kunit *test) +{ + struct staged_test_priv *priv =3D test->priv; + struct kobject *kobj =3D staged_child_alloc(test); + unsigned long parent_subdirs =3D sd_subdirs(priv->parent); + struct kernfs_node *ngroup_kn, *merged_kn; + int ret; + + ret =3D 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 =3D sysfs_create_file(kobj, &grp_attr); + KUNIT_EXPECT_EQ(test, ret, 0); + ret =3D sysfs_create_group(kobj, &named_grp); + KUNIT_EXPECT_EQ(test, ret, 0); + ret =3D sysfs_merge_group(kobj, &merge_grp); + KUNIT_EXPECT_EQ(test, ret, 0); + + ret =3D 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 =3D kernfs_find_and_get(kobj->sd, "ngroup"); + KUNIT_ASSERT_NOT_NULL(test, ngroup_kn); + merged_kn =3D 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 clean= ly, + * leaving the winner and the parent undisturbed. + */ +static void staged_test_duplicate_name(struct kunit *test) +{ + struct staged_test_priv *priv =3D test->priv; + struct kobject *win =3D staged_child_alloc(test); + struct kobject *lose =3D staged_child_alloc(test); + unsigned long base =3D 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 =3D 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 =3D test->priv; + struct kobject *outer =3D staged_child_alloc(test); + struct kobject *inner =3D 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 stage= d: + * 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 =3D test->priv; + struct kobject *kobj =3D 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 =3D { + .attr =3D { .name =3D "mmap_file", .mode =3D 0444 }, + .size =3D PAGE_SIZE, + .mmap =3D staged_mmap_stub, +}; + +/* + * KERNFS_STAGED_TOP aliases KERNFS_HAS_MMAP in one flag bit, and publicat= ion + * 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 fi= le + * 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 =3D test->priv; + struct kobject *kobj =3D 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 =3D 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 =3D 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 =3D data; + struct kernfs_node *kn; + + c->create_ret =3D 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 =3D kernfs_find_and_get(c->kobj->sd, c->notify_name); + c->notify_target_present =3D !!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 =3D test->priv; + struct kobject *kobj =3D staged_child_alloc(test); + struct task_struct *t; + struct foreign_ctx c =3D {}; + long rc; + + c.kobj =3D kobj; + c.new_attr =3D kunit_kzalloc(test, sizeof(*c.new_attr), GFP_KERNEL); + c.rm_attr =3D 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 =3D "foreign_new"; + c.new_attr->mode =3D 0644; + c.rm_attr->name =3D "to_remove"; + c.rm_attr->mode =3D 0644; + c.notify_name =3D "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 =3D kthread_run(foreign_fn, &c, "staged_foreign"); + KUNIT_ASSERT_FALSE(test, IS_ERR(t)); + + rc =3D 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 =3D test->priv; + unsigned long base_subdirs =3D sd_subdirs(priv->parent); + int base_count =3D atomic_read(&priv->parent->sd->count); + int i; + + for (i =3D 0; i < 2; i++) { + struct kobject *kobj =3D 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; publ= ish; + * the removed group is absent, other content present. + */ +static void staged_test_partial_group_abort(struct kunit *test) +{ + struct staged_test_priv *priv =3D test->priv; + struct kobject *kobj =3D 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 orp= han + * tears down cleanly. + */ +static void staged_test_parent_removed(struct kunit *test) +{ + struct staged_test_priv *priv =3D test->priv; + struct kobject *mid, *kobj; + int ret; + + /* an intermediate published parent we can remove mid-window */ + mid =3D kobject_create_and_add("mid", priv->parent); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, mid); + + kobj =3D 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 =3D 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 >=3D 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 exac= tly + * one at publish, independent of node count. + */ +static void staged_test_o1_root_cost(struct kunit *test) +{ + struct staged_test_priv *priv =3D test->priv; + struct kobject *kobj =3D staged_child_alloc(test); + unsigned long base =3D 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 =3D data; + struct attribute *attrs[WINDOW_NODES] =3D {}; + struct staged_kobj *sk; + struct kobject *kobj; + int i; + + sk =3D kzalloc_obj(*sk); + if (!sk) { + w->add_ret =3D -ENOMEM; + complete(&w->done); + return 0; + } + kobject_init(&sk->kobj, &staged_ktype); + kobject_set_sd_staged(&sk->kobj, true); + kobj =3D &sk->kobj; + + w->add_ret =3D 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 =3D 0; i < WINDOW_NODES; i++) { + struct attribute *a =3D kzalloc_obj(*a); + + if (!a) + break; + sysfs_attr_init(a); + a->name =3D kasprintf(GFP_KERNEL, "attr%d", i); + a->mode =3D 0644; + if (sysfs_create_file(kobj, a)) { + kfree((void *)a->name); + kfree(a); + break; + } + attrs[i] =3D a; + } + + w->publish_ret =3D sysfs_publish_dir(kobj); + + kobject_del(kobj); + kobject_put(kobj); /* release frees the container */ + + /* attributes outlive their files; free them after teardown */ + for (i =3D 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 =3D test->priv; + struct window_ctx w1 =3D { .parent =3D priv->parent, .name =3D "winA" }; + struct window_ctx w2 =3D { .parent =3D priv->parent, .name =3D "winB" }; + struct task_struct *t1, *t2; + long r1, r2; + + init_completion(&w1.done); + init_completion(&w2.done); + + t1 =3D kthread_run(window_fn, &w1, "staged_winA"); + /* nothing spawned yet if this aborts */ + KUNIT_ASSERT_FALSE(test, IS_ERR(t1)); + t2 =3D 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 =3D wait_for_completion_timeout(&w1.done, msecs_to_jiffies(10000)); + r2 =3D 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 t= o the + * staged path. Freed by staged_dev_release() when the last reference dro= ps; + * 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 =3D kzalloc_obj(*dev); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + + device_initialize(dev); + dev->parent =3D parent; + dev->bus =3D bus; + dev->release =3D staged_dev_release; + if (staged) + dev_set_sysfs_staged(dev); + + ret =3D 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 defer= red + * 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 =3D staged_dev_alloc(test, NULL, NULL, name, true); + dev->class =3D class; + + ret =3D 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 >=3D 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 =3D { .name =3D "glue_attr", .mode =3D 0= 644 }; + +static void staged_test_glue_dir(struct kunit *test) +{ + struct device *dev_a, *dev_b, *dev_a2; + struct class *class; + + class =3D 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 =3D staged_class_dev_add(test, class, "glueA"); + dev_b =3D 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 =3D 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 se= es 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 =3D 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 =3D 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 =3D sd_subdirs(&root->kobj); + + /* same name, staged: the directory is built, publication collides */ + staged =3D staged_dev_alloc(test, root, NULL, "collide", true); + ret =3D 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 =3D 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 =3D 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 =3D { + .name =3D "staged_kunit_bus", +}; + +static int staged_test_driver_probe(struct device *dev) +{ + return 0; +} + +static struct device_driver staged_test_driver =3D { + .name =3D "staged_kunit_drv", + .bus =3D &staged_test_bus, + .owner =3D THIS_MODULE, + .probe =3D 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 =3D data; + + scan->seen++; + if (dev =3D=3D scan->want) + scan->found =3D 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. Asser= t 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 =3D {}; + 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 =3D 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 =3D 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 win= dow + * than the hashed mutex array has slots, so by pigeonhole at least two st= aged + * 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 =3D test->priv; + unsigned long base =3D sd_subdirs(priv->parent); + struct kobject **kids; + unsigned int *slots; + bool collided =3D false; + int i, j; + + kids =3D kunit_kcalloc(test, STAGED_SLOT_WINDOWS, sizeof(*kids), GFP_KERN= EL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, kids); + slots =3D kunit_kcalloc(test, STAGED_SLOT_WINDOWS, sizeof(*slots), GFP_KE= RNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, slots); + + for (i =3D 0; i < STAGED_SLOT_WINDOWS; i++) { + kids[i] =3D 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] =3D 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 =3D 0; i < STAGED_SLOT_WINDOWS && !collided; i++) + for (j =3D i + 1; j < STAGED_SLOT_WINDOWS; j++) + if (slots[i] =3D=3D slots[j]) { + collided =3D true; + break; + } + KUNIT_EXPECT_TRUE(test, collided); + + for (i =3D 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 =3D 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 =3D 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[] =3D { + 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 =3D { + .name =3D "staged_device", + .init =3D staged_test_init, + .exit =3D staged_test_exit, + .test_cases =3D staged_device_tests, +}; + +kunit_test_suite(staged_device_test_suite); + +MODULE_DESCRIPTION("KUnit tests for staged sysfs directory registration"); +MODULE_LICENSE("GPL"); --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-008.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-008.esa.us-west-2.outbound.mail-perimeter.amazon.com [52.42.203.116]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 77AB549504E; Fri, 11 Sep 2026 17:47:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=52.42.203.116 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148835; cv=none; b=Ly6ZtQO5Gs9IG5HIdSCpCWz2G6WVj9bTOS3jez/VPsCxsK/vVTjjqiGuRO0VI27Ri1WOVW7bmeKtvXiLzstEMXPqHk5K40/aKQzVg1Jr7gFCkoeuraP9DJMvCttGQCkrpZzI0M2zJF7R+Zyimec7/jLW0btSYHl1N/Ph3Abq5a0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148835; c=relaxed/simple; bh=FvDNAs1nt85rTHybp22wpHzMFjt6BIQ5aV6U6y0fiV8=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=IffvyC5rf3fY8041CPMjhJzzQaloBeyTZLqC+8VQazMwAa4dkLyoTrRYNGo/q29iE69dRvXSWG/eUUXnlXSUnffYxvOJKJDEoM12xX7rYt8eAR2ScjY+j18zrxUyrFeOmLcDSN7ME4NqbgVQ+pBbyiu5KJv62N3lPA7VOBq7hyk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=R1B8H1+i; arc=none smtp.client-ip=52.42.203.116 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="R1B8H1+i" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148831; x=1820684831; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=JJbbk3LWF9sIhfos1AEb1lxm5c8t9hJGGm6Fe8bvX4A=; b=R1B8H1+ius2Equ5rxgAw0Hpz73nqL7R3L2Cx6rMmpCnx8bc29HHjCNf3 dSsQUkKLv8OFb/LkMW2yrTkwsNvObgaTM1z1Tncgb+l313NwFsKJX30u5 Yyws0MKU61MLaYNi8JjlWKKhnEyUD7rtN209aQbQ6lMS4J/ltW6MmPurM dU3/q2P7QUsizz3kNur3svPGUztl6kydwU1x1GZYSEZ0LqIp1UI7Niq8Z h2wasY8I5kqpm4R6N1gfQriSIhS+iETw0MdfwOl3Eie8LVMGzg1T0Y0Lp p/znf9vdEpsoiCl+SkxclHqCHvAngn3O13nRQAo5jtDc+/CSizdpsBJBO A==; X-CSE-ConnectionGUID: Er41EoCTRJSU+LVC3bObyw== X-CSE-MsgGUID: WJgDU8iyTbejOMgRaxa3nQ== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28480116" Received: from ip-10-5-12-219.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.12.219]) by internal-pdx-out-008.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:47:09 +0000 Received: from EX19MTAUWA002.ant.amazon.com [205.251.233.178:1792] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.6.93:2525] with esmtp (Farcaster) id 081a47fd-4d6d-4715-9a6a-ce953a42307e; Fri, 11 Sep 2026 17:47:08 +0000 (UTC) X-Farcaster-Flow-ID: 081a47fd-4d6d-4715-9a6a-ce953a42307e Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWA002.ant.amazon.com (10.250.64.202) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:47:08 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:47:05 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , 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 Message-ID: <20260911174414.97060-6-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D036UWC002.ant.amazon.com (10.13.139.242) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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; } =20 +/* + * 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 registrati= on + * 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 devic= e, + * kobject or kernfs_node. Membership is decided under @staged_windows_lo= ck, + * 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 *t= op) +{ + win->top =3D 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, whi= ch + * 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 w= hose + * 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 =3D dev->parent; top; top =3D 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 =3D 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 =3D get_device(dev); + + spin_lock(&staged_windows_lock); + hash_for_each_possible(staged_windows, win, node, (unsigned long)top) { + if (win->top !=3D top) + continue; + dev_set_uevent_suppress(dev, 1); + list_add_tail(&member->node, &win->members); + member =3D 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 delivere= d 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 writ= ten by + * this thread. The registration and driver tests below are a guard again= st + * misuse, not an ordering contract with a thread registering members of i= ts + * 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 =3D 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 =3D -EINVAL; struct kobject *glue_dir =3D NULL; =20 @@ -3760,6 +3903,17 @@ int device_add(struct device *dev) goto Error; } =20 + /* + * 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); =20 @@ -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 =3D 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); =20 + /* 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 =3D get_glue_dir(dev); kobject_del(&dev->kobj); diff --git a/drivers/base/test/staged-device-test.c b/drivers/base/test/sta= ged-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) =20 /* Deferred bus-klist insertion, on a real bus with a real driver. */ =20 +/* 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 =3D { .name =3D "staged_kunit_bus", + .uevent =3D staged_test_bus_uevent, }; =20 static int staged_test_driver_probe(struct device *dev) @@ -1010,6 +1015,360 @@ static void staged_test_hashed_slot_collision(struc= t kunit *test) KUNIT_EXPECT_EQ(test, sd_subdirs(priv->parent), base); } =20 +/* 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 o= nce + * the window's top publishes. + * + * The seam: the hook has to run between kobject_add() and publication, an= d 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 i= nside + * 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=3Dadd bus callbacks */ + unsigned int bind_uevents; /* child ACTION=3Dbind 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 =3D staged_window_ctx; + int i; + + if (!c || dev !=3D c->child) + return 0; + + for (i =3D 0; i < env->envp_idx; i++) { + if (!strcmp(env->envp[i], "ACTION=3Dadd")) { + c->add_uevents++; + c->add_seq =3D ++c->uevent_seq; + } else if (!strcmp(env->envp[i], "ACTION=3Dbind")) { + c->bind_uevents++; + c->bind_seq =3D ++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 =3D 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 =3D staged_window_ctx; + + if (!c || c->hook_ran || kobj !=3D &c->top->kobj) + return attr->mode; + c->hook_ran =3D true; + + c->add_ret =3D device_add(c->child); + if (c->add_ret) + return attr->mode; + + c->suppressed_in_window =3D dev_get_uevent_suppress(c->child); + c->staged_in_window =3D sd_is_staged(&c->child->kobj); + c->bound_in_window =3D c->child->driver; + + if (c->delete_in_window) { + struct device *child =3D c->child; + + device_del(child); + c->suppressed_at_delete =3D dev_get_uevent_suppress(child); + c->child =3D 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 =3D { + .name =3D "in_window_attr", .mode =3D 0644, +}; + +static struct attribute *in_window_attrs[] =3D { &in_window_attr, NULL }; +static const struct attribute_group in_window_grp =3D { + .attrs =3D in_window_attrs, + .is_visible =3D staged_window_is_visible, +}; + +static const struct attribute_group *in_window_grps[] =3D { + &in_window_grp, NULL, +}; + +/* Register @name staged under @parent, with a child added inside its wind= ow. */ +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 =3D test; + top =3D staged_dev_alloc(test, parent, NULL, name, true); + top->groups =3D in_window_grps; + ctx->top =3D top; + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, + staged_dev_cleanup, + top), 0); + ctx->child =3D staged_dev_alloc(test, top, ctx->child_bus, "in_window", + false); + ctx->child->class =3D 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 =3D ctx; + ret =3D device_add(top); + staged_window_ctx =3D 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 on= ce + * 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 =3D {}; + struct device *root, *top; + + root =3D 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 =3D 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, an= d 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 =3D { .delete_in_window =3D true }; + struct staged_window_ctx retry_ctx =3D {}; + struct device *root, *eager, *top, *retry; + + root =3D 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 =3D 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 =3D 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 =3D 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 t= he + * member's own directory -- which kernfs marks staged for every node it l= inks + * 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 =3D {}; + struct device *root, *top; + struct class *class; + + class =3D 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 =3D 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 =3D class; + top =3D 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 els= e. + * Successful closure must reconstruct it after the ADD: exactly one ADD + * followed by exactly one BIND passed through the child device's bus ueve= nt + * 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 =3D {}; + 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 =3D 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 =3D &staged_test_bus; + top =3D 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 announce= d; + * 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 =3D { .suppress_child =3D true }; + struct device *root, *top; + + root =3D 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 =3D 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[] =3D { 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[] =3D { 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), {} }; =20 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); =20 +/** + * 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 becaus= e it + * was created staged or because it was created underneath one -- kernfs m= arks + * 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 c= limbs: + * publication clears the flag under the staged subtree mutex, which this = query + * does not take. A %true answer therefore means "staged a moment ago", a= nd 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 calle= r'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 =3D 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 { =20 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 *ko= bj) return 0; } =20 +static inline bool sysfs_dir_staged(const struct kobject *kobj) +{ + return false; +} + static inline void sysfs_remove_dir(struct kobject *kobj) { } --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-010.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-010.esa.us-west-2.outbound.mail-perimeter.amazon.com [52.12.53.23]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0DE2F4A484A; Fri, 11 Sep 2026 17:47:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=52.12.53.23 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148864; cv=none; b=XbqeQ5VW+Hhswuf9BLn4AD0UNsdCjVxh4RSZet22Xkc7Eiprh9E93t4+3oKDxO7RCo4un/Qo/a3Hp4gp52e1y9xIJyzlX+rp5pnM2GZMOB+WtvQQfNcoDK2dZinaJ/UwYB+pmTmnQmZDb774etg9Fumk7B8oyic9yxfZTOrOMHQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148864; c=relaxed/simple; bh=bOmqB4DT165vGlUkh4Mo3Agug0sCz6kPxVUhY41K2js=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=VQBlvhm18zAlzTwBJSATHY5CyCxSNexLT7x6dospFSuASINk0BvXfHxurCQ3/rOWJw/UOtT55SMFn/ySiok52B//ts31sW0Y0Lhv095RfAkEEb9tcQ1SR3UNxY1DBi3hhSC9FGXmGxizVzoxizMTFHVDtE4JoZrt/2AMo0LcOJs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=I0rjhOoo; arc=none smtp.client-ip=52.12.53.23 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="I0rjhOoo" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148863; x=1820684863; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=73c2Q3ChwgfM3U9RxYpDCf/lEjkTcCr872X8hHq50Sg=; b=I0rjhOooBOZNERaltiZ+bkMfKcl/s4K3NP5hpDTpO1QQdmOKASnoLLsT pF3sLW9nRlc6V9/b4uvD7qhDALN6DIYHY7EfTfmixAxMUEUWqxmkKaT/T OIcRi0KHAL5tP7HvaLTQnq+PNvNUNAne7czisF1kVrN2xDe/9Kb6926Bh Iabzxkeg+gqGZ30udOw2bADIL9hs4kCClCycD1PEnqGdrBV6gwKbY/wMg u/U2SkDGXlxWoUy0R8dTHKGQlgzILN41hE2et6gCDhy1IypLzn+hVH46w N/BydWG0MSB4uqeEc9vCLUIhxhf7h5v6pHbtpr3xRPsdaeWEWcks5CcaS w==; X-CSE-ConnectionGUID: E4dCvVnaQxqrNJJUSmCOyA== X-CSE-MsgGUID: e8NnEEYWSX2mdI/uHM0k5g== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28318161" Received: from ip-10-5-9-48.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.9.48]) by internal-pdx-out-010.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:47:42 +0000 Received: from EX19MTAUWB001.ant.amazon.com [205.251.233.51:32130] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.22.113:2525] with esmtp (Farcaster) id f22bb6e2-6ce1-44b4-9c36-35978ecb9669; Fri, 11 Sep 2026 17:47:42 +0000 (UTC) X-Farcaster-Flow-ID: f22bb6e2-6ce1-44b4-9c36-35978ecb9669 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWB001.ant.amazon.com (10.250.64.248) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:47:42 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:47:39 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 7/8] PCI/IOV: Register virtual functions through the staged sysfs path Date: Fri, 11 Sep 2026 19:43:39 +0200 Message-ID: <20260911174414.97060-7-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D031UWC004.ant.amazon.com (10.13.139.246) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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, st= ruct pci_bus *bus, BUG_ON(rc); } =20 + /* must precede pci_device_add(); see DEV_FLAG_SYSFS_STAGED */ + dev_set_sysfs_staged(&virtfn->dev); pci_device_add(virtfn, virtfn->bus); rc =3D pci_iov_sysfs_link(dev, virtfn, id); if (rc) --=20 2.47.3 From nobody Fri Sep 25 13:54:44 2026 Received: from pdx-out-015.esa.us-west-2.outbound.mail-perimeter.amazon.com (pdx-out-015.esa.us-west-2.outbound.mail-perimeter.amazon.com [50.112.246.219]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 267334A5C23; Fri, 11 Sep 2026 17:48:20 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=50.112.246.219 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148902; cv=none; b=SHxKQGyC4Z6eKH7JIvuZYhXYq1XmMCgkqg/X2zUxrtIy9HmWnXmJj5suqrkVH79rZm61YxAAcySCC2h1wJB4fRG/mUQCLCXiVo/OC1XziNeDUloQVIjDPY050PLIDD0GAG+OV+glqZAh1EwMuZ0TnodS3PICOglowAaUfYaPnAo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789148902; c=relaxed/simple; bh=9leqayE49Oyz1EhyEm9zFmsiRGQbZooy9v4epT1KDJM=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=l6czlpXHATVZIsnEpKmUzXH5kl5ptvNChdtBjCYIlkeu8OZeY+EdZF4E8g29q7X4QHAZubYZ7QXpMiqN8XOWaWB09vXnNzA5KMfrEh63Cd8SBFzaOjb6AwepjqcE3uzRBE8gKUToZTd8ucEXpMR8i7o7ppqB0DMkDPI90Ml7t3c= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de; spf=pass smtp.mailfrom=amazon.de; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b=oUOqp/Dy; arc=none smtp.client-ip=50.112.246.219 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=amazon.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=amazon.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=amazon.de header.i=@amazon.de header.b="oUOqp/Dy" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=amazon.de; i=@amazon.de; q=dns/txt; s=amazoncorp2; t=1789148900; x=1820684900; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=uOLhShYEEjRLR1coK3ZfM4Sg+hn51n7UIhIFHEx/jJ8=; b=oUOqp/Dy/g5rn9UlTq7Fn730aetogXEKZNYRhrGKtegphfwcfv8s59aW fTnoW79e5r0idQYIURSQLBW7mNlAoJUYO13aczzm3QPuooCu2wdrThvYp a+UHoF/9xJJtPHXYsXN+PZ6FGIHctkBsWvn2IVUv1VaGh8zS3P3RbBwcU O8+B1R+InJnwTL2D5BTiDfcDQZCrMSk7df2axqSHqjEWznXQziPceE8ik gCwEgKZG95HLDT5XWg4y1EJ48BLWzTEI/lqntczSka3+yOiUOo/gf3mqh /i3fRPcSswW9+fWelBj8QzvXlqYrAqsOuJ1yCIOzbyBqi4sNGlijLnwJU A==; X-CSE-ConnectionGUID: I2whZOZ8TV29O6pMEDxgVw== X-CSE-MsgGUID: S2z4XCtESgiShsGwJQ/O4Q== X-IronPort-AV: E=Sophos;i="6.27,97,1787011200"; d="scan'208";a="28227456" Received: from ip-10-5-0-115.us-west-2.compute.internal (HELO smtpout.naws.us-west-2.prod.farcaster.email.amazon.dev) ([10.5.0.115]) by internal-pdx-out-015.esa.us-west-2.outbound.mail-perimeter.amazon.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 11 Sep 2026 17:48:16 +0000 Received: from EX19MTAUWC002.ant.amazon.com [205.251.233.111:27899] by smtpin.naws.us-west-2.prod.farcaster.email.amazon.dev [10.0.57.27:2525] with esmtp (Farcaster) id 39b22f75-224a-4113-9cc0-1bfc055575e6; Fri, 11 Sep 2026 17:48:16 +0000 (UTC) X-Farcaster-Flow-ID: 39b22f75-224a-4113-9cc0-1bfc055575e6 Received: from EX19D001UWA001.ant.amazon.com (10.13.138.214) by EX19MTAUWC002.ant.amazon.com (10.250.64.143) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.45; Fri, 11 Sep 2026 17:48:15 +0000 Received: from dev-dsk-sakacpav-1a-480d1124.eu-west-1.amazon.com (172.19.96.155) by EX19D001UWA001.ant.amazon.com (10.13.138.214) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) id 15.2.2562.46; Fri, 11 Sep 2026 17:48:13 +0000 From: Pavol Sakac To: Greg Kroah-Hartman , Tejun Heo , "Rafael J . Wysocki" , Danilo Krummrich CC: , , "Andy Shevchenko" , Xu Yang , Bartosz Golaszewski , Bjorn Helgaas , , Alex Williamson , , Subject: [RFC PATCH 8/8] vfio: Opt the group and vfio-dev class devices into staged sysfs Date: Fri, 11 Sep 2026 19:43:40 +0200 Message-ID: <20260911174414.97060-8-sakacpav@amazon.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> References: <20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ClientProxiedBy: EX19D037UWC001.ant.amazon.com (10.13.139.197) To EX19D001UWA001.ant.amazon.com (10.13.138.214) Content-Type: text/plain; charset="utf-8" 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 --- 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 i= ommu_group *iommu_group, list_add(&new->vfio_next, &vfio.group_list); mutex_unlock(&vfio.group_lock); =20 + /* + * 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 *devi= ce, goto err_out; } =20 + /* Staged opt-in: same conditions as vfio_group_find_or_create(). */ + dev_set_sysfs_staged(&device->device); + ret =3D vfio_device_add(device); if (ret) goto err_out; --=20 2.47.3