[LTP] [PATCH STAGING v4 09/16] fchroot06: test path walks under failfs root
Cyril Hrubis
chrubis@suse.cz
Wed Oct 14 13:39:34 CEST 2026
Hi!
> Verify that once the root is in failfs only lookups anchored at a
> file descriptor keep working: relative lookups from the working
> directory and from a pre-opened directory fd succeed, absolute
> symlinks fail with EOPNOTSUPP and ".." walks clamp at the top of the
> mount tree, landing on the real root.
>
> Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com>
> ---
> runtest/staging | 1 +
> testcases/kernel/syscalls/fchroot/.gitignore | 1 +
> testcases/kernel/syscalls/fchroot/fchroot06.c | 126 ++++++++++++++++++++++++++
> 3 files changed, 128 insertions(+)
>
> diff --git a/runtest/staging b/runtest/staging
> index 49f51d637..8b6b1ecd9 100644
> --- a/runtest/staging
> +++ b/runtest/staging
> @@ -5,3 +5,4 @@ fchroot02 fchroot02
> fchroot03 fchroot03
> fchroot04 fchroot04
> fchroot05 fchroot05
> +fchroot06 fchroot06
> diff --git a/testcases/kernel/syscalls/fchroot/.gitignore b/testcases/kernel/syscalls/fchroot/.gitignore
> index 0697f10eb..12151270a 100644
> --- a/testcases/kernel/syscalls/fchroot/.gitignore
> +++ b/testcases/kernel/syscalls/fchroot/.gitignore
> @@ -3,3 +3,4 @@ fchroot02
> fchroot03
> fchroot04
> fchroot05
> +fchroot06
> diff --git a/testcases/kernel/syscalls/fchroot/fchroot06.c b/testcases/kernel/syscalls/fchroot/fchroot06.c
> new file mode 100644
> index 000000000..8f678344f
> --- /dev/null
> +++ b/testcases/kernel/syscalls/fchroot/fchroot06.c
> @@ -0,0 +1,126 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (C) 2026 SUSE LLC Andrea Cervesato <andrea.cervesato@suse.com>
> + */
> +
> +/*\
> + * Test path walks under the failfs root.
> + *
> + * Once :manpage:`fchroot(2)` moved the process root into failfs, only
> + * lookups anchored at a file descriptor keep working:
> + *
> + * - lookups relative to the working directory, which stays in the real
> + * filesystem, keep working
> + * - lookups anchored at a pre-opened directory fd keep working, including
> + * resolution of relative symlinks
> + * - absolute symlinks restart the walk at the failfs root and fail with
> + * ``EOPNOTSUPP``
> + * - ".." walks clamp at the top of the mount tree, not at the failfs
> + * root, so walking up from the working directory lands on the real
> + * root
> + *
> + * Root is required because entering failfs with the ``FD_FAILFS_ROOT``
> + * sentinel requires ``CAP_SYS_CHROOT``.
> + *
> + * The test runs in a forked child so the root of the parent process is
> + * left untouched.
> + */
> +
> +#define _GNU_SOURCE
> +#include <fcntl.h>
> +#include <sys/stat.h>
> +#include "tst_test.h"
> +#include "lapi/fcntl.h"
> +#include "lapi/syscalls.h"
> +#include "tst_safe_file_at.h"
> +
> +#define RELDIR "rel"
> +#define ABSDIR "abs"
> +
> +static char upwards[PATH_MAX];
> +
> +static void run(void)
> +{
> + if (SAFE_FORK())
> + return;
> +
> + struct stat realroot, st;
> + int dfd, fd;
> +
> + SAFE_STAT("/", &realroot);
> + dfd = SAFE_OPEN(".", O_RDONLY | O_DIRECTORY);
> +
> + TST_EXP_PASS(tst_syscall(__NR_fchroot, FD_FAILFS_ROOT, 0),
> + "fchroot() with the FD_FAILFS_ROOT sentinel");
> +
> + fd = TST_EXP_FD(openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY));
> + if (fd != -1)
> + SAFE_CLOSE(fd);
> +
> + fd = TST_EXP_FD(openat(dfd, "canary", O_WRONLY | O_CREAT, 0600));
> + if (fd != -1) {
> + SAFE_WRITE(SAFE_WRITE_ALL, fd, "x", 1);
> + SAFE_CLOSE(fd);
> + }
> +
> + fd = TST_EXP_FD(openat(dfd, RELDIR, O_RDONLY));
> + if (fd != -1)
> + SAFE_CLOSE(fd);
> +
> + TST_EXP_FAIL2(openat(dfd, ABSDIR, O_RDONLY), EOPNOTSUPP,
> + "resolution of an absolute symlink");
> +
> + fd = TST_EXP_FD(openat(AT_FDCWD, upwards, O_PATH));
> + if (fd != -1) {
> + SAFE_FSTAT(fd, &st);
> + SAFE_CLOSE(fd);
> +
> + TST_EXP_EXPR(st.st_dev == realroot.st_dev &&
> + st.st_ino == realroot.st_ino,
> + "'..' walk clamps at the top of the mount tree");
> + }
> +
> + SAFE_CLOSE(dfd);
> +
> + exit(0);
Here as well, no need for the exit(0);
> +}
> +
> +static void setup(void)
> +{
> + char *tmpdir;
> + char abs_path[PATH_MAX];
> + struct stat root_st, st;
> + int fd, off;
> +
> + tmpdir = tst_tmpdir_path();
> + snprintf(abs_path, sizeof(abs_path), "%s/%s", tmpdir, "target");
> +
> + SAFE_TOUCH("target", 0644, NULL);
> + SAFE_SYMLINK("target", RELDIR);
> + SAFE_SYMLINK(abs_path, ABSDIR);
> +
> + SAFE_STAT("/", &root_st);
> +
> + off = snprintf(upwards, sizeof(upwards), "..");
> + while (1) {
> + fd = SAFE_OPENAT(AT_FDCWD, upwards, O_PATH);
> + SAFE_FSTAT(fd, &st);
> + SAFE_CLOSE(fd);
Why not just SAFE_FSTAT(upwards, &st) ?
> + if (st.st_dev == root_st.st_dev && st.st_ino == root_st.st_ino)
> + break;
> +
> + if ((size_t)off + sizeof("/..") > sizeof(upwards))
> + tst_brk(TBROK, "Path to root too long");
> +
> + off += snprintf(upwards + off, sizeof(upwards) - off, "/..");
This can probably be simplified to:
strcpy(upwards + off, "../");
off += 3;
The string is constant and we already checked that it fits, there is no
need to use the big hammer (snprintf).
And the same for the initial snprintf(), just strcpy(upwards, ".."); off = 2;
> + }
> +}
> +
> +static struct tst_test test = {
> + .setup = setup,
> + .test_all = run,
> + .needs_root = 1,
> + .needs_tmpdir = 1,
> + .forks_child = 1,
> +};
>
> --
> 2.51.0
>
>
> --
> Mailing list info: https://lists.linux.it/listinfo/ltp
--
Cyril Hrubis
chrubis@suse.cz
More information about the ltp
mailing list