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 <[email protected]>
---
 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/selftests/filesystems/config
index 7231710d5ce7..9f45bc493a30 100644
--- a/tools/testing/selftests/filesystems/config
+++ b/tools/testing/selftests/filesystems/config
@@ -1,5 +1,6 @@
 CONFIG_CGROUPS=y
 CONFIG_CGROUP_PIDS=y
+CONFIG_FHANDLE=y
 CONFIG_NAMESPACES=y
 CONFIG_NET=y
 CONFIG_NET_NS=y
diff --git a/tools/testing/selftests/filesystems/kernfs_test.c 
b/tools/testing/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)
        }
 }
 
+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 = 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 == EOPNOTSUPP || err == EPERM || err == 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 = open_by_handle_at(mfd, &fh->h, O_PATH);
+
+       if (fd < 0)
+               return errno != 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 = open(self->scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+       ASSERT_GE(mfd, 0);
+
+       if (kernfs_encode(procs, &fh)) {
+               int err = 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 = 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 = open_by_handle_at(mfd, &fh.h, O_PATH);
+       EXPECT_LT(fd, 0);
+       if (fd >= 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 = 0, rounds = 0;
+       struct timespec end;
+
+       mfd = open(self->scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+       ASSERT_GE(mfd, 0);
+
+       clock_gettime(CLOCK_MONOTONIC, &end);
+       end.tv_sec += FH_STRESS_SECS;
+
+       while (!stress_deadline(&end)) {
+               char victim[PATH_MAX], procs[PATH_MAX];
+               int last = -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 = 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 == 1 && !fh_can_decode(mfd, &fh)) {
+                       rmdir(victim);
+                       SKIP(goto out,
+                            "open_by_handle_at: no CAP_DAC_READ_SEARCH");
+               }
+
+               pid = fork();
+               ASSERT_GE(pid, 0);
+               if (pid == 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 = 0; i < FH_DECODE_CAP; i++) {
+                       fd = open_by_handle_at(mfd, &fh.h, O_PATH);
+                       if (fd < 0)
+                               break;
+                       if (last >= 0)
+                               close(last);
+                       last = fd;
+               }
+               ASSERT_EQ(waitpid(pid, NULL, 0), pid);
+
+               if (last >= 0) {
+                       if (access(victim, F_OK) && errno == ENOENT &&
+                           !fstat(last, &st) && st.st_nlink != 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);
 
        strcpy(self->mnt, "/tmp/kernfs_selftest_sysfs.XXXXXX");
-       ASSERT_NE(mkdtemp(self->mnt), NULL);
+       if (!mkdtemp(self->mnt))
+               SKIP(return, "mkdtemp: %s", strerror(errno));
 
        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 shows
  * 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 = opendir(self->net);
        ASSERT_NE(d, NULL);
        while ((de = readdir(d))) {
-               if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
+               if (de->d_type != 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);
 }
 
+static int netdev_rename(const char *from, const char *to)
+{
+       struct ifreq ifr = {};
+       int sk, ret;
+
+       sk = 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 = 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/<dev> 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 = 0, bad = 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 = 0; i < RENAME_READERS; i++) {
+               pid_t pid = fork();
+
+               ASSERT_GE(pid, 0);
+               if (pid == 0) {
+                       struct stat rst;
+                       char c;
+
+                       close(done[1]);
+                       while (read(done[0], &c, 1) < 0 && errno == EAGAIN) {
+                               if (lstat(old_path, &rst) && errno != ENOENT)
+                                       _exit(20);
+                               if (lstat(new_path, &rst) && errno != ENOENT)
+                                       _exit(21);
+                       }
+                       _exit(0);
+               }
+               pids[n++] = pid;
+       }
+       close(done[0]);
+
+       for (i = 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 = 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
-- 
2.53.0-Meta


Reply via email to