From nobody Sun Sep 27 02:54:49 2026 Received: from mta1.migadu.com (out-69.mta1.migadu.com [95.215.58.69]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 011B61A9F91 for ; Sat, 5 Sep 2026 19:16:32 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.69 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635795; cv=none; b=miNkyRyGhdWsuULTkj0k2/Y+4DiquomCp5O5YWphqLL23e0JqI2v+O50lsWrx21lO4NZeJgvOwoHJvV4gDM1bKxBb1MNsTsI7MUee/uktqT6Ei7CN5paVcfv0r9M6UYymHdkbl6/4VX+L+/xTTzrqBp3/P4PlULmSRPgFV+4n0w= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635795; c=relaxed/simple; bh=M7EdkrcJhqSM7+RxcD+4/cEw0xbBmnI1xssOM5aYOHI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=BBOlI2KANL9WyQeQxtC1RvTMop08YwLFKYNzru4EWZ0YZwO8fxzLyep1A2JExxfw9gq/71NYsgZmoG62crWZFfCje/QI8k1n4oRlhMUmyJvCFqWd1gOhZru+Y59eEyKY9eiGDre66qOFuz/1hj/8b+QktlaS06LrkLtXg0O1Cjg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=OYktoySk; arc=none smtp.client-ip=95.215.58.69 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="OYktoySk" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=M7EdkrcJhqSM7+RxcD+4/cEw0xbBmnI1xssOM5aYOHI=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788635791; v=1; x=1789240591; b=OYktoySkRDQ5A9tJLGpJa8L1aMhVqRWMnbANe8C1TrCBUaCOxI5KwufICljQoUSXyMNhBL50 DWw94ojoCvI6nvX81yLfCs8LeT9ZzcFypoGRvPCMIVfrD044je0tHhcbP4Q/DEFd2EPKfD5GgI2 pevAVyUUsgGtlcLFhzEpkF0Y= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 826c53d273b91ede; Sat, 05 Sep 2026 19:16:31 +0000 X-Mizu-Trace-ID: 826c53d273b91ede X-Migadu-Flow: FLOW_OUT From: Shakeel Butt To: Greg Kroah-Hartman , Tejun Heo , Christian Brauner Cc: Meta kernel team , linux-kselftest@vger.kernel.org, driver-core@lists.linux.dev, linux-kernel@vger.kernel.org Subject: [PATCH v2 1/4] selftests: cover kernfs file handles and same-parent rename Date: Sat, 5 Sep 2026 12:16:10 -0700 Message-ID: <20260905191613.3143937-2-shakeel.butt@linux.dev> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260905191613.3143937-1-shakeel.butt@linux.dev> References: <20260905191613.3143937-1-shakeel.butt@linux.dev> 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 Content-Type: text/plain; charset="utf-8" The kernfs tests only reach kernfs through lookup and readdir. Two paths are not covered: file handles, which find a node without a lookup through its parent, and rename of a node that keeps its parent. Add three tests: - decode a file handle, live and after the node is gone; - decode while the node is being removed; - look up a name while an interface is renamed, which renames its /sys/class/net entry with the parent unchanged. Two small fixes while here. ns_tag_isolates_class_net now counts only symlinks, because bonding adds a bonding_masters attribute that is not a device. A failing mkdtemp() now skips instead of aborting. Assisted-by: LLM Signed-off-by: Shakeel Butt --- tools/testing/selftests/filesystems/config | 1 + .../selftests/filesystems/kernfs_test.c | 296 +++++++++++++++++- 2 files changed, 295 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/filesystems/config b/tools/testing/sel= ftests/filesystems/config index 7231710d5ce7..9f45bc493a30 100644 --- a/tools/testing/selftests/filesystems/config +++ b/tools/testing/selftests/filesystems/config @@ -1,5 +1,6 @@ CONFIG_CGROUPS=3Dy CONFIG_CGROUP_PIDS=3Dy +CONFIG_FHANDLE=3Dy CONFIG_NAMESPACES=3Dy CONFIG_NET=3Dy CONFIG_NET_NS=3Dy diff --git a/tools/testing/selftests/filesystems/kernfs_test.c b/tools/test= ing/selftests/filesystems/kernfs_test.c index 2178428b9665..6e74da91ebca 100644 --- a/tools/testing/selftests/filesystems/kernfs_test.c +++ b/tools/testing/selftests/filesystems/kernfs_test.c @@ -610,6 +610,189 @@ TEST_F(kernfs_cgroup, lookup_vs_create_remove_stress) } } =20 +struct kernfs_handle { + struct file_handle h; + unsigned char buf[MAX_HANDLE_SZ]; +}; + +static int kernfs_encode(const char *path, struct kernfs_handle *fh) +{ + int mount_id; + + memset(fh, 0, sizeof(*fh)); + fh->h.handle_bytes =3D sizeof(fh->buf); + return name_to_handle_at(AT_FDCWD, path, &fh->h, &mount_id, 0); +} + +/* + * Skip only where file handles do not work at all. ENOENT must still + * fail: mkdir leaves a negative dentry cached, so the name resolves only + * after ->d_revalidate() drops it. The encode tests revalidation too. + */ +static bool fh_unsupported(int err) +{ + return err =3D=3D EOPNOTSUPP || err =3D=3D EPERM || err =3D=3D ENOSYS; +} + +/* + * Decoding a file needs CAP_DAC_READ_SEARCH in the initial user + * namespace. Probe once so the tests skip instead of fail. + */ +static bool fh_can_decode(int mfd, struct kernfs_handle *fh) +{ + int fd =3D open_by_handle_at(mfd, &fh->h, O_PATH); + + if (fd < 0) + return errno !=3D EPERM; + close(fd); + return true; +} + +/* + * A file handle reaches a node without a lookup through its parent. A + * live node must decode. A removed one must not, because + * kernfs_find_and_get_node_by_id() refuses inactive nodes. + * + * Use O_PATH: opening a removed node fails with ENODEV, which would hide + * what is being tested. + */ +TEST_F(kernfs_cgroup, exportfs_decode_and_stale) +{ + char victim[PATH_MAX], procs[PATH_MAX]; + struct kernfs_handle fh; + struct stat st; + int mfd, fd; + + snprintf(victim, sizeof(victim), "%s/fh", self->scratch); + snprintf(procs, sizeof(procs), "%s/cgroup.procs", victim); + ASSERT_EQ(mkdir(victim, 0755), 0); + + /* Any fd on the filesystem identifies it to open_by_handle_at(). */ + mfd =3D open(self->scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(mfd, 0); + + if (kernfs_encode(procs, &fh)) { + int err =3D errno; + + close(mfd); + rmdir(victim); + ASSERT_TRUE(fh_unsupported(err)) + TH_LOG("name_to_handle_at: %s", strerror(err)); + SKIP(return, "name_to_handle_at: %s", strerror(err)); + } + + if (!fh_can_decode(mfd, &fh)) { + close(mfd); + rmdir(victim); + SKIP(return, "open_by_handle_at: no CAP_DAC_READ_SEARCH"); + } + + fd =3D open_by_handle_at(mfd, &fh.h, O_PATH); + ASSERT_GE(fd, 0); + EXPECT_EQ(fstat(fd, &st), 0); + EXPECT_EQ(st.st_nlink, 1); + EXPECT_EQ(close(fd), 0); + + ASSERT_EQ(rmdir(victim), 0); + + fd =3D open_by_handle_at(mfd, &fh.h, O_PATH); + EXPECT_LT(fd, 0); + if (fd >=3D 0) + close(fd); + else + EXPECT_EQ(errno, ESTALE); + + EXPECT_EQ(close(mfd), 0); +} + +#define FH_STRESS_SECS 2 +#define FH_DECODE_CAP 10000 + +/* + * Decode file handles while the node is being removed. A decode must + * answer with a usable handle or ESTALE, never garbage and never a hang. + * + * The link count is checked too. An inode that reaches the inode hash + * after the removal cleared link counts keeps the 1 it was born with, so + * it never gets an IN_DELETE_SELF. This has not been seen to fire: it + * needs the decode to stall between the lookup by id and the hash insert, + * and nothing there blocks. It is kept because it is cheap and only + * looks once the directory is gone, so it cannot fail falsely. + */ +TEST_F(kernfs_cgroup, exportfs_decode_vs_rmdir_stress) +{ + int mfd, bad =3D 0, rounds =3D 0; + struct timespec end; + + mfd =3D open(self->scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_GE(mfd, 0); + + clock_gettime(CLOCK_MONOTONIC, &end); + end.tv_sec +=3D FH_STRESS_SECS; + + while (!stress_deadline(&end)) { + char victim[PATH_MAX], procs[PATH_MAX]; + int last =3D -1, fd, i; + struct kernfs_handle fh; + struct stat st; + pid_t pid; + + snprintf(victim, sizeof(victim), "%s/fh%d", self->scratch, + rounds++); + snprintf(procs, sizeof(procs), "%s/cgroup.procs", victim); + if (mkdir(victim, 0755)) + break; + if (kernfs_encode(procs, &fh)) { + int err =3D errno; + + rmdir(victim); + ASSERT_TRUE(fh_unsupported(err)) + TH_LOG("name_to_handle_at: %s", strerror(err)); + SKIP(goto out, "name_to_handle_at: %s", strerror(err)); + } + if (rounds =3D=3D 1 && !fh_can_decode(mfd, &fh)) { + rmdir(victim); + SKIP(goto out, + "open_by_handle_at: no CAP_DAC_READ_SEARCH"); + } + + pid =3D fork(); + ASSERT_GE(pid, 0); + if (pid =3D=3D 0) { + rmdir_retry(victim); + _exit(0); + } + + /* + * Decode until the removal deactivates the node. Keep the + * last one that worked: it ran closest to the removal. + */ + for (i =3D 0; i < FH_DECODE_CAP; i++) { + fd =3D open_by_handle_at(mfd, &fh.h, O_PATH); + if (fd < 0) + break; + if (last >=3D 0) + close(last); + last =3D fd; + } + ASSERT_EQ(waitpid(pid, NULL, 0), pid); + + if (last >=3D 0) { + if (access(victim, F_OK) && errno =3D=3D ENOENT && + !fstat(last, &st) && st.st_nlink !=3D 0) + bad++; + close(last); + } + rmdir(victim); + } + + EXPECT_EQ(bad, 0) + TH_LOG("%d of %d rounds decoded a removed node whose inode kept its link= count", + bad, rounds); +out: + close(mfd); +} + /* * sysfs is namespace tagged (KERNFS_NS) and supports rename; cgroup2 does * neither. Run in a private netns with its own sysfs so the host is @@ -635,7 +818,8 @@ FIXTURE_SETUP(kernfs_netns) ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); =20 strcpy(self->mnt, "/tmp/kernfs_selftest_sysfs.XXXXXX"); - ASSERT_NE(mkdtemp(self->mnt), NULL); + if (!mkdtemp(self->mnt)) + SKIP(return, "mkdtemp: %s", strerror(errno)); =20 if (mount("none", self->mnt, "sysfs", 0, NULL)) { rmdir(self->mnt); @@ -662,6 +846,9 @@ FIXTURE_TEARDOWN(kernfs_netns) * depends on the modules the host has. Check the set instead -- * if_nametoindex() resolves in the current netns, so every name sysfs sho= ws * must resolve there, and the counts must agree. + * + * Count only symlinks. Not every entry is a device: bonding adds a + * bonding_masters attribute to /sys/class/net in every namespace. */ TEST_F(kernfs_netns, ns_tag_isolates_class_net) { @@ -674,7 +861,7 @@ TEST_F(kernfs_netns, ns_tag_isolates_class_net) d =3D opendir(self->net); ASSERT_NE(d, NULL); while ((de =3D readdir(d))) { - if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) + if (de->d_type !=3D DT_LNK) continue; EXPECT_NE(if_nametoindex(de->d_name), 0u) TH_LOG("%s is not in this netns", de->d_name); @@ -728,4 +915,109 @@ TEST_F(kernfs_netns, rename_is_revalidated) EXPECT_EQ(stat(new_path, &st), 0); } =20 +static int netdev_rename(const char *from, const char *to) +{ + struct ifreq ifr =3D {}; + int sk, ret; + + sk =3D socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sk < 0) + return -1; + strncpy(ifr.ifr_name, from, IFNAMSIZ - 1); + strncpy(ifr.ifr_newname, to, IFNAMSIZ - 1); + ret =3D ioctl(sk, SIOCSIFNAME, &ifr); + close(sk); + return ret; +} + +/* + * Bounded by a count, not by time: every rename is logged and not rate + * limited, so a timed loop would flood the kernel log. + */ +#define RENAME_FLIPS 200 +#define RENAME_READERS 4 + +/* + * Rename an interface while other tasks look up the names it moves + * between. This renames its /sys/class/net entry through + * kernfs_rename_ns() with the parent unchanged. + * + * The renamer checks what is certain: SIOCSIFNAME returns once the rename + * is done and nothing else renames here, so the new name must resolve and + * the old must not. The readers cannot check that, because the name can + * move between their two lstat() calls. They only check that a lookup + * returns success or ENOENT, and keep the lock busy while renames run. + * + * lstat() not stat(): /sys/class/net/ is a symlink and is renamed + * before the directory it points at, so the two are not atomic. + */ +TEST_F(kernfs_netns, rename_vs_lookup_stress) +{ + char old_path[PATH_MAX], new_path[PATH_MAX]; + pid_t pids[RENAME_READERS]; + int i, status, n =3D 0, bad =3D 0; + struct stat st; + int done[2]; + + snprintf(old_path, sizeof(old_path), "%s/lo", self->net); + snprintf(new_path, sizeof(new_path), "%s/%s", self->net, TEST_IFNAME); + + if (netdev_rename("lo", TEST_IFNAME)) + SKIP(return, "SIOCSIFNAME: %s", strerror(errno)); + if (netdev_rename(TEST_IFNAME, "lo")) + SKIP(return, "SIOCSIFNAME back: %s", strerror(errno)); + + /* Readers run until the renamer closes the write end. */ + ASSERT_EQ(pipe2(done, O_NONBLOCK | O_CLOEXEC), 0); + + for (i =3D 0; i < RENAME_READERS; i++) { + pid_t pid =3D fork(); + + ASSERT_GE(pid, 0); + if (pid =3D=3D 0) { + struct stat rst; + char c; + + close(done[1]); + while (read(done[0], &c, 1) < 0 && errno =3D=3D EAGAIN) { + if (lstat(old_path, &rst) && errno !=3D ENOENT) + _exit(20); + if (lstat(new_path, &rst) && errno !=3D ENOENT) + _exit(21); + } + _exit(0); + } + pids[n++] =3D pid; + } + close(done[0]); + + for (i =3D 0; i < RENAME_FLIPS; i++) { + if (netdev_rename("lo", TEST_IFNAME)) + break; + if (lstat(new_path, &st) || !lstat(old_path, &st)) { + bad++; + break; + } + if (netdev_rename(TEST_IFNAME, "lo")) + break; + if (lstat(old_path, &st) || !lstat(new_path, &st)) { + bad++; + break; + } + } + close(done[1]); + + for (i =3D 0; i < n; i++) { + ASSERT_EQ(waitpid(pids[i], &status, 0), pids[i]); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(WEXITSTATUS(status), 0); + } + + EXPECT_EQ(bad, 0) + TH_LOG("a completed rename left the wrong name resolving"); + + /* Leave the interface as the fixture found it. */ + netdev_rename(TEST_IFNAME, "lo"); +} + TEST_HARNESS_MAIN --=20 2.53.0-Meta From nobody Sun Sep 27 02:54:49 2026 Received: from mta1.migadu.com (out-74.mta1.migadu.com [95.215.58.74]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CFF8725A2DD for ; Sat, 5 Sep 2026 19:16:35 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.74 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635797; cv=none; b=cXSOS+YP/nxVVO8GUMBcjEFNzln6TMoziO5G36H9t1z7ApqKY0zUYIueeJnfs09FolnO177AIsNvqHNVDH/Ou8ZgjbvhSLjnPiJUwS76zpjS8ESH58iqIoVIKlLt+bIqmpPhhvHw5CNXALbzPYKASlt+nlEgOpn1FA8Je/AowVg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635797; c=relaxed/simple; bh=WBt1gcOUPcF87z43kqy+nFmn0uUqQjGuY1EyXbG1LBQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=d2137uC99xxxyb3FVGQdAFQNeWzCTjnH1rltZ3GHvUggphlyRAvjXKTr5envsjDyg4kI283ougDC2rUQfPO8Zy+sGrmQgAL0Itbwm9bjW1QT8EeO8oUaFOwr+Qq7XPvMRaq/9nff3z1VP5wSrcUd0jmZJb30CEeq+K+R+ZCWopU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=i1oub85v; arc=none smtp.client-ip=95.215.58.74 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="i1oub85v" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=WBt1gcOUPcF87z43kqy+nFmn0uUqQjGuY1EyXbG1LBQ=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788635793; v=1; x=1789240593; b=i1oub85vWS5K6QE1qpWSn0xDcsvPFitddUJfVsdJt04OzztOOdq4mmkC640W9AVl3+v5NcQd yt0mTPvyqwMs+1ywDKuxhmPBqkJtmuZBG++/7UoINEnc3ylpRbYh4BsjD6HSe8yKhWCKFTb4cyD wlk/5Dkb1EMu83MsznTWY7ps= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 122d4c13fd44d7d5; Sat, 05 Sep 2026 19:16:33 +0000 X-Mizu-Trace-ID: 122d4c13fd44d7d5 X-Migadu-Flow: FLOW_OUT From: Shakeel Butt To: Greg Kroah-Hartman , Tejun Heo , Christian Brauner Cc: Meta kernel team , linux-kselftest@vger.kernel.org, driver-core@lists.linux.dev, linux-kernel@vger.kernel.org Subject: [PATCH v2 2/4] kernfs: take kernfs_rename_lock for same-parent renames too Date: Sat, 5 Sep 2026 12:16:11 -0700 Message-ID: <20260905191613.3143937-3-shakeel.butt@linux.dev> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260905191613.3143937-1-shakeel.butt@linux.dev> References: <20260905191613.3143937-1-shakeel.butt@linux.dev> 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 Content-Type: text/plain; charset="utf-8" kernfs_rename_ns() takes kernfs_rename_lock only when the rename moves the node to a new parent. A rename that keeps the parent, like renaming a network interface, changes kernfs_node::name under kernfs_rwsem alone. So the lock covers ->__parent but not ->name, and a reader that wants a stable name has to take kernfs_rwsem, the lock every lookup needs. It is also a real bug. kernfs_path_from_node() holds the lock for reading and reads each ancestor's name once. One rename only moves the answer from the old path to the new one, but two renames inside one walk build a path that never existed: CPU0 CPU1 kernfs_path_from_node() on /a/b/c reads the name of a, gets "a" renames a to a2 renames b to b2 reads the name of b, gets "b2" returns "/a/b2/c" Only sysfs can hit this: sysfs_warn_dup() is the one caller on a root without KERNFS_ROOT_INVARIANT_PARENT. The rest are cgroup, which sets the flag, so it skips the lock and reads names under RCU alone. That case needs something else and is left alone here. So take the lock for both kinds of rename, and let kernfs_rcu_name() accept it, like kernfs_parent() already does for ->__parent. Renames are rare, the lock is per filesystem, and the locked section is at most three stores. It also gives a future rename counter one place to sit. Fixes: 741c10b096bc ("kernfs: Use RCU to access kernfs_node::name.") Acked-by: Tejun Heo Assisted-by: LLM Signed-off-by: Shakeel Butt --- fs/kernfs/dir.c | 28 +++++++++++++++------------- fs/kernfs/kernfs-internal.h | 9 ++++++++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c index cd7a8ff8b6b2..214c97130a8a 100644 --- a/fs/kernfs/dir.c +++ b/fs/kernfs/dir.c @@ -1808,6 +1808,7 @@ int kernfs_rename_ns(struct kernfs_node *kn, struct k= ernfs_node *new_parent, struct kernfs_node *old_parent; struct kernfs_root *root; const char *old_name; + bool reparent; int error; =20 /* can't move or rename root */ @@ -1857,25 +1858,26 @@ int kernfs_rename_ns(struct kernfs_node *kn, struct= kernfs_node *new_parent, */ kernfs_unlink_sibling(kn); =20 - /* rename_lock protects ->parent accessors */ - if (old_parent !=3D new_parent) { + reparent =3D old_parent !=3D new_parent; + if (reparent) kernfs_get(new_parent); - write_lock_irq(&root->kernfs_rename_lock); =20 + /* + * kernfs_rename_lock protects ->__parent, ->ns and ->name, so take it + * even when the parent does not change. + */ + write_lock_irq(&root->kernfs_rename_lock); + + if (reparent) rcu_assign_pointer(kn->__parent, new_parent); + WRITE_ONCE(kn->ns, new_ns); + if (new_name) + rcu_assign_pointer(kn->name, new_name); =20 - WRITE_ONCE(kn->ns, new_ns); - if (new_name) - rcu_assign_pointer(kn->name, new_name); + write_unlock_irq(&root->kernfs_rename_lock); =20 - write_unlock_irq(&root->kernfs_rename_lock); + if (reparent) kernfs_put(old_parent); - } else { - /* name assignment is RCU protected, parent is the same */ - WRITE_ONCE(kn->ns, new_ns); - if (new_name) - rcu_assign_pointer(kn->name, new_name); - } =20 kn->hash =3D kernfs_name_hash(new_name ?: old_name, kn->ns); kernfs_link_sibling(kn); diff --git a/fs/kernfs/kernfs-internal.h b/fs/kernfs/kernfs-internal.h index 20a0cf42ba8d..1609c1519698 100644 --- a/fs/kernfs/kernfs-internal.h +++ b/fs/kernfs/kernfs-internal.h @@ -117,7 +117,14 @@ static inline bool kernfs_rename_is_locked(const struc= t kernfs_node *kn) =20 static inline const char *kernfs_rcu_name(const struct kernfs_node *kn) { - return rcu_dereference_check(kn->name, kernfs_root_is_locked(kn)); + /* + * Like kernfs_node::__parent below, the name is only replaced under + * both kernfs_root::kernfs_rwsem and kernfs_root::kernfs_rename_lock, + * so either one keeps it, and the string it points at, stable. + */ + return rcu_dereference_check(kn->name, + kernfs_root_is_locked(kn) || + kernfs_rename_is_locked(kn)); } =20 static inline struct kernfs_node *kernfs_parent(const struct kernfs_node *= kn) --=20 2.53.0-Meta From nobody Sun Sep 27 02:54:49 2026 Received: from mta0.migadu.com (out-226.mta0.migadu.com [91.218.175.226]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5E3E02BDC2A for ; Sat, 5 Sep 2026 19:16:36 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.226 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635799; cv=none; b=gopAklW37ffzOWXHxUoFp67FkCzgAxKZm9g9mMp7b16PtkfH9MGFURbxuYsFRADL4wOrSvRAiwtQJKFXCrmRb2l46p8yw8knkAbzHKXye/17u1GkQxWBautNM1aj3jyMRX3oAMP047GYPb3WkjQEJZxoBs1Rz7NiWJg4/NhT3hQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635799; c=relaxed/simple; bh=cGmV+/3nJC8/nassXpZyq+BiguCr6AKUYC9cG846EPM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Vcv6Qcg28P/lE1uqS7M/NVStki53fQ9q4xPBs9o/tjwVNYoeizhKS82jjnwY2ccrG4lTQewXIfldN/XUF5K3O5t2ZEMFNo7ngaPGdEdUUdlyilo2J9T7cr2Wy1E774pXSHAo5tKizKiT9K0hXDoCpX7zB91xuxZtXjpZLqxvvKo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=ayzvBrmn; arc=none smtp.client-ip=91.218.175.226 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="ayzvBrmn" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=cGmV+/3nJC8/nassXpZyq+BiguCr6AKUYC9cG846EPM=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788635794; v=1; x=1789240594; b=ayzvBrmn/I6m526ojzaJbb/XU5tGL5O1WFhMXy85B2DULTpEg9avLLHv3L7unmSFVeghSuYG 5mIw97orW9PURoxfxZQxfLu6lqD5kEi3uOKsBj29Z6JKMCBaac68zecwUzaPlybYIC3uK4qeYbO 3MkEUD45YCfca0pgNGIx5ysE= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id ed91021f9d781103; Sat, 05 Sep 2026 19:16:34 +0000 X-Mizu-Trace-ID: ed91021f9d781103 X-Migadu-Flow: FLOW_OUT From: Shakeel Butt To: Greg Kroah-Hartman , Tejun Heo , Christian Brauner Cc: Meta kernel team , linux-kselftest@vger.kernel.org, driver-core@lists.linux.dev, linux-kernel@vger.kernel.org, stable@vger.kernel.org Subject: [PATCH v2 3/4] kernfs: don't lose IN_DELETE_SELF when decoding a file handle Date: Sat, 5 Sep 2026 12:16:12 -0700 Message-ID: <20260905191613.3143937-4-shakeel.butt@linux.dev> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260905191613.3143937-1-shakeel.butt@linux.dev> References: <20260905191613.3143937-1-shakeel.butt@linux.dev> 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 Content-Type: text/plain; charset="utf-8" __kernfs_remove() clears i_nlink on a node's inodes and finds them with ilookup(). A decode that has pinned the node but not yet hashed its inode is invisible to that pass: CPU0 CPU1 open_by_handle_at() kernfs_find_and_get_node_by_id() pins the node, still active rmdir() marks the subtree removing ilookup() finds no inode kernfs_get_inode() hashes an inode with i_nlink 1 Nothing fixes it later: kernfs_refresh_inode() never touches i_nlink for a file and skips it for a directory being removed. The inode keeps the 1 it was born with, and dentry_unlink_inode() sends IN_DELETE_SELF only at 0, so a watcher never learns the node went away. The other callers of kernfs_get_inode() are safe: those in fs/kernfs hold kernfs_rwsem, and cgroup_may_write() is covered by cgroup_mutex, which cgroup_destroy_locked() holds across kernfs_remove(). __kernfs_fh_to_dentry() has held nothing since exportfs support was added. Take kernfs_rwsem for reading, as ->get_parent already does, and cover the lookup as well as kernfs_get_inode(). __kernfs_remove() deactivates the whole subtree under the write lock, so under the read lock either the lookup refuses the node, or the inode is hashed before the ilookup() pass runs. The same holds for ->fh_to_parent, since a node cannot be active while an ancestor is being removed. Reproduced with a 300ms delay between the lookup and kernfs_get_inode(), decoding a handle for a file in a cgroup directory while another task rmdir()s it: st_nlink is 1 without this patch and 0 with it. ->get_parent still has a window of its own. It takes the same lock but has no active check, so reconnect_path() can build an inode for an ancestor that is already gone. That needs the active test rather than a lock, and changes what ->get_parent returns, so it is left to the series that reworks these paths. Fixes: eea5d2bb34ba ("kernfs: Send IN_DELETE_SELF and IN_IGNORED") Cc: stable@vger.kernel.org Acked-by: Tejun Heo Assisted-by: LLM Signed-off-by: Shakeel Butt --- fs/kernfs/mount.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/fs/kernfs/mount.c b/fs/kernfs/mount.c index f183a96778b9..c15ba6357162 100644 --- a/fs/kernfs/mount.c +++ b/fs/kernfs/mount.c @@ -124,22 +124,32 @@ static struct dentry *__kernfs_fh_to_dentry(struct su= per_block *sb, return NULL; } =20 - kn =3D kernfs_find_and_get_node_by_id(info->root, id); - if (!kn) - return ERR_PTR(-ESTALE); + /* + * Hold kernfs_rwsem across the lookup as well as kernfs_get_inode(). + * __kernfs_remove() deactivates the subtree and clears i_nlink on its + * inodes under the write lock, so under the read lock either + * kernfs_find_and_get_node_by_id() refuses the node, or the inode is + * in the inode hash before the ilookup() pass goes looking for it. + */ + scoped_guard(rwsem_read, &info->root->kernfs_rwsem) { + kn =3D kernfs_find_and_get_node_by_id(info->root, id); + if (!kn) + return ERR_PTR(-ESTALE); =20 - if (get_parent) { - struct kernfs_node *parent; + if (get_parent) { + struct kernfs_node *parent; =20 - parent =3D kernfs_get_parent(kn); + parent =3D kernfs_get_parent(kn); + kernfs_put(kn); + kn =3D parent; + if (!kn) + return ERR_PTR(-ESTALE); + } + + inode =3D kernfs_get_inode(sb, kn); kernfs_put(kn); - kn =3D parent; - if (!kn) - return ERR_PTR(-ESTALE); } =20 - inode =3D kernfs_get_inode(sb, kn); - kernfs_put(kn); return d_obtain_alias(inode); } =20 --=20 2.53.0-Meta From nobody Sun Sep 27 02:54:49 2026 Received: from mta1.migadu.com (out-83.mta1.migadu.com [95.215.58.83]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5653B17C203 for ; Sat, 5 Sep 2026 19:16:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.83 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635803; cv=none; b=uC6gMy36mGsre7VkC63ei2y/IadSY7N94f6ggT79ldAGAl5XEWpIMQ20bpJqqrFad+bPvSBcb69LdbuMMaAyoe5qbAE7GZMfw8TdIDuwiixGvhTXZilxN40d9auMbU0Q7ET4ZOtXF+Laf5qGtFPwRRHLxVkniOMZctL6xHlp+cI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788635803; c=relaxed/simple; bh=B7GMcmolD2T4Q/YoXp324L2Yk7TLtKzhIO850KXzPmo=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=mlPV0+xmE4ySNikLxEreL50AdKrvkekYgZ1NGscIGxKwV2vvyb5XEtDDlrKHy/bA70oz2t8Adl9CuYJBY+LxAW1HLU5vuzMMyHZpm+ffn8GQS0Y2Q39uKtrNNwASIH1ViedyHqNAOyR2kCCIo73Cooy8JP9XsVLZayb82iaLz8k= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Z1btD1VS; arc=none smtp.client-ip=95.215.58.83 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Z1btD1VS" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=B7GMcmolD2T4Q/YoXp324L2Yk7TLtKzhIO850KXzPmo=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788635800; v=1; x=1789240600; b=Z1btD1VSkIcyATo5dTpvZO5bWxhKYGYQxcEgjnkyD1lkQ87UZ+UVSQTmEEnWQLt/0S0O8tMJ Kt613VwwjPxDf1pNkLWeipv7FNiw8Qp5p2qxMO+14edtZdIwylmlG3L3vEuSK5nQaPTKP2iCJrZ YflB+vcUulW8B/V2gKgTiWwI= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 52e3934d4d77491d; Sat, 05 Sep 2026 19:16:40 +0000 X-Mizu-Trace-ID: 52e3934d4d77491d X-Migadu-Flow: FLOW_OUT From: Shakeel Butt To: Greg Kroah-Hartman , Tejun Heo , Christian Brauner Cc: Meta kernel team , linux-kselftest@vger.kernel.org, driver-core@lists.linux.dev, linux-kernel@vger.kernel.org Subject: [PATCH v2 4/4] kernfs: fix up the unlocked attribute reads on the creation paths Date: Sat, 5 Sep 2026 12:16:13 -0700 Message-ID: <20260905191613.3143937-5-shakeel.butt@linux.dev> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260905191613.3143937-1-shakeel.butt@linux.dev> References: <20260905191613.3143937-1-shakeel.butt@linux.dev> 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 Content-Type: text/plain; charset="utf-8" Two creation paths read a live node's attributes without holding kernfs_iattr_rwsem, which kernfs_iop_setattr() takes for writing. They need different fixes. kernfs_create_link() copies the target's ia_uid and then its ia_gid into the new link. A chown of the target between the two reads leaves the link with the old uid and the new gid, an owner the target never had. Read both under the rwsem. kernfs_new_node() reads the parent's mode and ia_gid for S_ISGID inheritance. Either value is fine there: the node does not exist yet, so nothing orders a racing chmod or chown against the creation. Taking the rwsem would only pick between two answers that are both right. Mark the reads with READ_ONCE() instead. The pointer that leads to them is already fine: __kernfs_iattrs() publishes kernfs_node::iattr with try_cmpxchg(), and both sides read it with READ_ONCE(), like the rest of fs/kernfs. The Fixes tag is for the symlink half. kernfs_create_link() has read the pair unlocked since it started copying the target's owner; only the name of the lock its writer takes has changed. The READ_ONCE() markings are not a fix. Fixes: 488dee96bb62 ("kernfs: allow creating kernfs objects with arbitrary = uid/gid") Acked-by: Tejun Heo Assisted-by: LLM Signed-off-by: Shakeel Butt --- fs/kernfs/dir.c | 12 +++++++++--- fs/kernfs/symlink.c | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c index 214c97130a8a..07abf59f0264 100644 --- a/fs/kernfs/dir.c +++ b/fs/kernfs/dir.c @@ -736,13 +736,19 @@ struct kernfs_node *kernfs_new_node(struct kernfs_nod= e *parent, { struct kernfs_node *kn; =20 - if (parent->mode & S_ISGID) { + /* + * The mode and the gid below are read unlocked on purpose: they feed + * a node that does not exist yet, so nothing orders a racing chmod or + * chown against this creation. + */ + if (READ_ONCE(parent->mode) & S_ISGID) { /* this code block imitates inode_init_owner() for * kernfs */ + struct kernfs_iattrs *attrs =3D READ_ONCE(parent->iattr); =20 - if (parent->iattr) - gid =3D parent->iattr->ia_gid; + if (attrs) + gid =3D READ_ONCE(attrs->ia_gid); =20 if (flags & KERNFS_DIR) mode |=3D S_ISGID; diff --git a/fs/kernfs/symlink.c b/fs/kernfs/symlink.c index 90e2b3221b83..3e53105d3abf 100644 --- a/fs/kernfs/symlink.c +++ b/fs/kernfs/symlink.c @@ -31,9 +31,20 @@ struct kernfs_node *kernfs_create_link(struct kernfs_nod= e *parent, kuid_t uid =3D GLOBAL_ROOT_UID; kgid_t gid =3D GLOBAL_ROOT_GID; =20 - if (target->iattr) { - uid =3D target->iattr->ia_uid; - gid =3D target->iattr->ia_gid; + /* + * A symlink takes its owner from its target, so both fields have to + * come from the same moment: read them under kernfs_iattr_rwsem, or + * a chown of the target racing this could leave the link with the + * old uid and the new gid. The section ends before kernfs_add_one() + * takes kernfs_rwsem. + */ + scoped_guard(rwsem_read, &kernfs_root(target)->kernfs_iattr_rwsem) { + struct kernfs_iattrs *attrs =3D READ_ONCE(target->iattr); + + if (attrs) { + uid =3D attrs->ia_uid; + gid =3D attrs->ia_gid; + } } =20 kn =3D kernfs_new_node(parent, name, S_IFLNK|0777, uid, gid, KERNFS_LINK); --=20 2.53.0-Meta