From 5f92fc4a93880bb7c622cabecf60ef37299955ef Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Sat, 24 Jan 2026 19:18:29 +0100 Subject: [PATCH] tests: Add comprehensive tests for glnx_chaseat --- tests/meson.build | 1 + tests/test-libglnx-chase.c | 406 +++++++++++++++++++++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 tests/test-libglnx-chase.c diff --git a/tests/meson.build b/tests/meson.build index 6c46b45c..5b773e8e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -34,6 +34,7 @@ if get_option('tests') test_names = [ 'backports', + 'chase', 'errors', 'fdio', 'macros', diff --git a/tests/test-libglnx-chase.c b/tests/test-libglnx-chase.c new file mode 100644 index 00000000..05d1fffa --- /dev/null +++ b/tests/test-libglnx-chase.c @@ -0,0 +1,406 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2026 Red Hat, Inc. + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "libglnx-config.h" +#include "libglnx.h" +#include +#include +#include +#include +#include + +#include "libglnx-testlib.h" + +#define GLNX_CHASE_DEBUG_NO_OPENAT2 (1 << 31) +#define GLNX_CHASE_DEBUG_NO_OPEN_TREE (1 << 30) + +const char *test_paths[] = { + "file/baz", + "file/baz/", + "file/baz/.", + "file/baz/../baz", + "file////baz/..//baz", + "file////baz/..//../file/baz", +}; + +static ino_t +get_ino (int fd) +{ + int r; + struct stat st; + + r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + g_assert_cmpint (r, >=, 0); + + return st.st_ino; +} + +static ino_t +path_get_ino (const char *path) +{ + int r; + struct stat st; + + r = fstatat (AT_FDCWD, path, &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + g_assert_cmpint (r, >=, 0); + + return st.st_ino; +} + +static char * +get_abspath (int dfd, + const char *path) +{ + g_autofree char *proc_fd_path = NULL; + g_autofree char *abs = NULL; + g_autoptr(GError) error = NULL; + + proc_fd_path = g_strdup_printf ("/proc/self/fd/%d", dfd); + abs = glnx_readlinkat_malloc (AT_FDCWD, proc_fd_path, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (abs); + + return g_strdup_printf ("%s/%s", abs, path); +} + +static void +check_chase (int dfd, + const char *path, + GlnxChaseFlags flags, + int expected_ino) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int chase_fd = -1; + + /* let's try to test the openat2 impl */ + chase_fd = glnx_chaseat (dfd, path, flags, &error); + g_assert_no_error (error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + g_clear_fd (&chase_fd, NULL); + + /* let's try to test the open_tree impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | GLNX_CHASE_DEBUG_NO_OPENAT2, + &error); + g_assert_no_error (error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + g_clear_fd (&chase_fd, NULL); + + /* let's try to test the openat impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | + GLNX_CHASE_DEBUG_NO_OPENAT2 | + GLNX_CHASE_DEBUG_NO_OPEN_TREE, + &error); + g_assert_no_error (error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + g_clear_fd (&chase_fd, NULL); +} + +static void +check_chase_error (int dfd, + const char *path, + GlnxChaseFlags flags, + GQuark err_domain, + gint err_code) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int chase_fd = -1; + + /* let's try to test the openat2 impl */ + chase_fd = glnx_chaseat (dfd, path, flags, &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); + + /* let's try to test the open_tree impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | GLNX_CHASE_DEBUG_NO_OPENAT2, + &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); + + /* let's try to test the openat impl */ + chase_fd = glnx_chaseat (dfd, path, + flags | + GLNX_CHASE_DEBUG_NO_OPENAT2 | + GLNX_CHASE_DEBUG_NO_OPEN_TREE, + &error); + g_assert_cmpint (chase_fd, <, 0); + g_assert_error (error, err_domain, err_code); + g_clear_error (&error); +} + +static void +test_chase_relative (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int expected_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + check_chase (AT_FDCWD, test_paths[i], 0, expected_ino); + + check_chase_error (AT_FDCWD, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_relative_fd (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int expected_ino; + glnx_autofd int cwdfd = -1; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (cwdfd, >=, 0); + + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + check_chase (cwdfd, test_paths[i], 0, expected_ino); + + check_chase_error (cwdfd, "nope", 0, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_absolute (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int expected_ino; + glnx_autofd int cwdfd = -1; + g_autofree char *proc_fd_path = NULL; + g_autofree char *cwd_path = NULL; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + expected_ino = get_ino (dfd); + + cwdfd = openat (AT_FDCWD, ".", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (cwdfd, >=, 0); + + cwd_path = get_abspath (cwdfd, ""); + + for (size_t i = 0; i < G_N_ELEMENTS (test_paths); i++) + { + g_autofree char *abspath = NULL; + + abspath = g_strdup_printf ("%s/%s", cwd_path, test_paths[i]); + check_chase (AT_FDCWD, abspath, 0, expected_ino); + } + + check_chase_error (AT_FDCWD, "/nope/nope/nope/345298308497623012313243543", 0, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND); +} + +static void +test_chase_link (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + int link_ino; + int target_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "file/baz", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (dfd, >=, 0); + + g_assert_cmpint (symlinkat ("file/baz", AT_FDCWD, "link"), ==, 0); + + target_ino = get_ino (dfd); + link_ino = path_get_ino ("link"); + + check_chase (AT_FDCWD, "link", 0, target_ino); + check_chase (AT_FDCWD, "link/", 0, target_ino); + check_chase (AT_FDCWD, "link///", 0, target_ino); + check_chase (AT_FDCWD, "link/.//.", 0, target_ino); + check_chase (AT_FDCWD, "link", 0, target_ino); + + check_chase (AT_FDCWD, "link", GLNX_CHASE_NOFOLLOW, link_ino); + check_chase (AT_FDCWD, "./file/../link", GLNX_CHASE_NOFOLLOW, link_ino); + check_chase (AT_FDCWD, "link/", GLNX_CHASE_NOFOLLOW, target_ino); + check_chase (AT_FDCWD, "././link/.", GLNX_CHASE_NOFOLLOW, target_ino); + check_chase (AT_FDCWD, "link/.//", GLNX_CHASE_NOFOLLOW, target_ino); + + check_chase (AT_FDCWD, "link", + GLNX_CHASE_NOFOLLOW | GLNX_CHASE_RESOLVE_NO_SYMLINKS, + link_ino); + check_chase_error (AT_FDCWD, "link", + GLNX_CHASE_RESOLVE_NO_SYMLINKS, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); +} + +static void +test_chase_resolve (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int foo_dfd = -1; + glnx_autofd int bar_dfd = -1; + g_autofree char *foo_abspath = NULL; + int ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755, + &foo_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (foo_dfd, >=, 0); + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755, + &bar_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (bar_dfd, >=, 0); + + foo_abspath = get_abspath (foo_dfd, ""); + + g_assert_cmpint (symlinkat ("..", foo_dfd, "link1"), ==, 0); + g_assert_cmpint (symlinkat ("bar/../..", foo_dfd, "link2"), ==, 0); + g_assert_cmpint (symlinkat (foo_abspath, foo_dfd, "link3"), ==, 0); + g_assert_cmpint (symlinkat ("/bar", foo_dfd, "link4"), ==, 0); + g_assert_cmpint (symlinkat ("link1/foo", foo_dfd, "link5"), ==, 0); + g_assert_cmpint (symlinkat ("link7", foo_dfd, "link6"), ==, 0); + g_assert_cmpint (symlinkat ("link6", foo_dfd, "link7"), ==, 0); + + ino = get_ino (bar_dfd); + + /* A bunch of different ways to get from CWD and foo to bar */ + check_chase (foo_dfd, "./bar", 0, ino); + check_chase (foo_dfd, "../foo/bar", 0, ino); + check_chase (foo_dfd, "link1/foo/bar", 0, ino); + check_chase (AT_FDCWD, "foo/link1/foo/bar", 0, ino); + check_chase (foo_dfd, "link2/foo/bar", 0, ino); + check_chase (AT_FDCWD, ".///foo/./link2/foo/bar", 0, ino); + check_chase (foo_dfd, "link3/bar", 0, ino); + check_chase (AT_FDCWD, ".///foo/./link3/bar", 0, ino); + check_chase (foo_dfd, "link5/bar", 0, ino); + + /* check that NO_SYMLINKS works with a component in the middle */ + check_chase_error (AT_FDCWD, "foo/link3/bar", + GLNX_CHASE_RESOLVE_NO_SYMLINKS, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); + + /* link6 points to link 7, points to link6, ... This should error out! */ + check_chase_error (foo_dfd, "link6/bar", 0, + G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS); + + /* Test with links which never go below the dfd */ + check_chase (AT_FDCWD, "foo/link1/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + ino); + check_chase (AT_FDCWD, "foo/link2/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + ino); + /* An absolute link is always below the dfd */ + check_chase_error (AT_FDCWD, "foo/link3/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + + /* Same, but from foo instead of cwd */ + check_chase_error (foo_dfd, "link1/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + check_chase_error (foo_dfd, "link2/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + check_chase_error (foo_dfd, "link3/foo/bar", + GLNX_CHASE_RESOLVE_BENEATH, + G_IO_ERROR, G_IO_ERROR_FAILED); + + /* Check that trying to be below the dfd with RESOLVE_IN_ROOT resolves to the + * dfd itself */ + check_chase (foo_dfd, "link1/bar", + GLNX_CHASE_RESOLVE_IN_ROOT, + ino); + check_chase (foo_dfd, "link2/bar", + GLNX_CHASE_RESOLVE_IN_ROOT, + ino); + /* The absolute link is relative to dfd with RESOLVE_IN_ROOT, so this + * fails... */ + check_chase_error (foo_dfd, "link3", + GLNX_CHASE_RESOLVE_IN_ROOT, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + /* ... but the link /bar resolves correctly from foo as dfd. */ + check_chase (foo_dfd, "link4", + GLNX_CHASE_RESOLVE_IN_ROOT, + ino); +} + +static void +test_chase_resolve_in_root_absolute (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int foo_dfd = -1; + glnx_autofd int bar_dfd = -1; + glnx_autofd int baz_dfd = -1; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo", 0755, + &foo_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (foo_dfd, >=, 0); + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar", 0755, + &bar_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (bar_dfd, >=, 0); + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "foo/bar/baz", 0755, + &baz_dfd, + NULL, &error)); + g_assert_no_error (error); + g_assert_cmpint (baz_dfd, >=, 0); + + /* Test the absolute symlink doesn't break tracking of the root level */ + g_assert_cmpint (symlinkat ("/..", baz_dfd, "link1"), ==, 0); + + /* We should not be able to break out of the root! */ + check_chase (bar_dfd, "./baz/link1", GLNX_CHASE_RESOLVE_IN_ROOT, get_ino (bar_dfd)); +} + +int main (int argc, char **argv) +{ + _GLNX_TEST_SCOPED_TEMP_DIR; + int ret; + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/chase-relative", test_chase_relative); + g_test_add_func ("/chase-relative-fd", test_chase_relative_fd); + g_test_add_func ("/chase-absolute", test_chase_absolute); + g_test_add_func ("/chase-link", test_chase_link); + g_test_add_func ("/chase-resolve", test_chase_resolve); + g_test_add_func ("/chase-resolve-in-root-absolute", test_chase_resolve_in_root_absolute); + + ret = g_test_run(); + + return ret; +}