/* Test openat2. Copyright 2025-2026 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* Written by Paul Eggert and Adhemerval Zanella. */ #include #include #include "signature.h" SIGNATURE_CHECK (openat2, int, (int, char const *, struct open_how const *, size_t)); #include #include #include #include #include #include #include #if HAVE_DECL_ALARM # include #endif #include "ignore-value.h" #include "macros.h" #define BASE "test-openat2.t" #include "test-open.h" static int dfd = AT_FDCWD; static uint_least64_t resolve; /* Wrapper around openat2 to test open behavior. */ static int do_open (char const *name, int flags, ...) { mode_t mode = 0; if (flags & O_CREAT) { va_list arg; va_start (arg, flags); /* We have to use PROMOTED_MODE_T instead of mode_t, otherwise GCC 4 creates crashing code when 'mode_t' is smaller than 'int'. */ mode = va_arg (arg, PROMOTED_MODE_T); va_end (arg); } struct open_how how = { .flags = flags, .mode = mode, .resolve = resolve }; return openat2 (dfd, name, &how, sizeof how); } #define temp_dir BASE "temp_dir" static void do_prepare () { /* Remove any leftovers from a previous partial run. */ ignore_value (system ("rm -rf " BASE "*")); ASSERT (mkdirat (AT_FDCWD, temp_dir, 0700) == 0); dfd = openat2 (AT_FDCWD, temp_dir, (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY }), sizeof (struct open_how)); ASSERT (0 <= dfd); } static void do_prepare_symlinks () { /* Construct a test directory with the following structure: temp_dir/ |- dirlink -> subdir |- dirlinkslash -> subdir/ |- escaping_link -> / |- escaping_link_2 -> escaping_link |- some_file |- invalid_link -> some_file/invalid |- valid_link -> some_file |- subdir/ |- some_file |- aunt_link -> ../some_file */ ASSERT (symlinkat ("subdir", dfd, "dirlink") == 0); ASSERT (symlinkat ("subdir/", dfd, "dirlinkslash") == 0); ASSERT (symlinkat ("/", dfd, "escaping_link") == 0); ASSERT (symlinkat ("escaping_link", dfd, "escaping_link_2") == 0); ASSERT (symlinkat ("some_file/invalid", dfd, "invalid_link") == 0); ASSERT (symlinkat ("some_file", dfd, "valid_link") == 0); ASSERT (mkdirat (dfd, "subdir", 0700) == 0); ASSERT (symlinkat ("../some_file", dfd, "subdir/aunt_link") == 0); ASSERT (close (openat2 (dfd, "some_file", (&(struct open_how) { .flags = O_CREAT, .mode = 0600 }), sizeof (struct open_how))) == 0); ASSERT (close (openat2 (dfd, "subdir/some_file", (&(struct open_how) { .flags = O_CREAT, .mode = 0600 }), sizeof (struct open_how))) == 0); } static void do_test_struct () { static struct struct_test { struct open_how_ext { struct open_how inner; int extra1; int extra2; int extra3; } arg; size_t size; int err; } const tests[] = { { /* Zero size. */ .arg.inner.flags = O_RDONLY, .size = 0, .err = EINVAL, }, { /* Normal struct. */ .arg.inner.flags = O_RDONLY, .size = sizeof (struct open_how), }, { /* Larger struct, zeroed out the unused values. */ .arg.inner.flags = O_RDONLY, .size = sizeof (struct open_how_ext), }, { /* Larger struct, non-zeroed out the unused values. */ .arg.inner.flags = O_RDONLY, .arg.extra1 = 0xdeadbeef, .size = sizeof (struct open_how_ext), .err = E2BIG, }, { /* Larger struct, non-zeroed out the unused values. */ .arg.inner.flags = O_RDONLY, .arg.extra2 = 0xdeadbeef, .size = sizeof (struct open_how_ext), .err = E2BIG, }, }; for (struct struct_test const *t = tests; t < tests + countof (tests); t++) { int fd = openat2 (AT_FDCWD, ".", (struct open_how *) &t->arg, t->size); if (!t->err) ASSERT (close (fd) == 0); else { ASSERT (errno == t->err); ASSERT (fd == -1); } } } static void do_test_flags (void) { static struct flag_test { char const *filename; struct open_how how; int err; } const tests[] = { #ifdef O_PATH # ifdef O_TMPFILE /* O_TMPFILE is incompatible with O_PATH and O_CREAT. */ { .how.flags = O_TMPFILE | O_PATH | O_RDWR, .err = EINVAL }, { .how.flags = O_TMPFILE | O_CREAT | O_RDWR, .err = EINVAL }, # endif /* O_PATH only permits certain other flags to be set ... */ { .how.flags = O_PATH | O_CLOEXEC }, { .how.flags = O_PATH | O_DIRECTORY }, { .how.flags = O_PATH | O_NOFOLLOW }, /* ... and others are absolutely not permitted. */ { .how.flags = O_PATH | O_RDWR, .err = EINVAL }, { .how.flags = O_PATH | O_CREAT, .err = EINVAL }, { .how.flags = O_PATH | O_EXCL, .err = EINVAL }, { .how.flags = O_PATH | O_NOCTTY, .err = EINVAL }, { .how.flags = O_PATH | O_DIRECT, .err = EINVAL }, #endif /* ->mode must only be set with O_{CREAT,TMPFILE}. */ { .how.flags = O_RDONLY, .how.mode = 0600, .err = EINVAL }, #ifdef O_PATH { .how.flags = O_PATH, .how.mode = 0600, .err = EINVAL }, #endif { .how.flags = O_CREAT, .how.mode = 0600 }, #ifdef O_TMPFILE { .how.flags = O_TMPFILE | O_RDWR, .how.mode = 0600 }, #endif /* ->mode must only contain 07777 bits. */ { .how.flags = O_CREAT, .how.mode = 0xFFFF, .err = EINVAL }, { .how.flags = O_CREAT, .how.mode = 0xc000000000000000, .err = EINVAL }, #ifdef O_TMPFILE { .how.flags = O_TMPFILE | O_RDWR, .how.mode = 0x10000, .err = EINVAL }, { .how.flags = O_TMPFILE | O_RDWR, .how.mode = 0xa00000000000, .err = EINVAL }, #endif /* ->resolve flags must not conflict. */ { .how.flags = O_RDONLY, .how.resolve = RESOLVE_BENEATH | RESOLVE_IN_ROOT, .err = EINVAL }, /* ->resolve must only contain RESOLVE_* flags. */ { .how.flags = O_RDONLY, .how.resolve = 0x1337, .err = EINVAL }, { .how.flags = O_CREAT, .how.resolve = 0x1337, .err = EINVAL }, #ifdef O_TMPFILE { .how.flags = O_TMPFILE | O_RDWR, .how.resolve = 0x1337, .err = EINVAL }, #endif #ifdef O_PATH { .how.flags = O_PATH, .how.resolve = 0x1337, .err = EINVAL }, #endif /* currently unknown upper 32 bit rejected. */ { .how.flags = O_RDONLY | (1ull << 63), .how.resolve = 0, .err = EINVAL }, }; for (struct flag_test const *t = tests; t < tests + countof (tests); t++) { char const *filename = t->how.flags & O_CREAT ? "created" : "."; if (t->how.flags & O_CREAT) unlinkat (dfd, filename, 0); int fd = openat2 (dfd, filename, &t->how, sizeof (struct open_how)); if (fd < 0 && errno == EOPNOTSUPP) { /* Skip the testcase if FS does not support the operation (e.g. valid O_TMPFILE on NFS). */ continue; } if (!t->err) ASSERT (close (fd) == 0); else { ASSERT (errno == t->err); ASSERT (fd == -1); } } } static bool is_nofollow_error (int err) { return err == _GL_OPENAT_ESYMLINK; } static void do_test_resolve (void) { int fd; fd = openat2 (dfd, "dirlink", (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT (is_nofollow_error (errno)); ASSERT (fd == -1); fd = openat2 (dfd, "dirlinkslash", (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT (is_nofollow_error (errno)); ASSERT (fd == -1); fd = openat2 (dfd, "dirlink/nosuch", (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY, .resolve = RESOLVE_IN_ROOT, }), sizeof (struct open_how)); ASSERT (errno == ENOENT); ASSERT (fd == -1); fd = openat2 (dfd, "dirlinkslash/nosuch", (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY, .resolve = RESOLVE_IN_ROOT, }), sizeof (struct open_how)); ASSERT (errno == ENOENT); ASSERT (fd == -1); /* ESCAPING_LINK links to /tmp, which escapes the temporary test directory. */ fd = openat2 (dfd, "escaping_link", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT ((errno == EXDEV) | is_nofollow_error (errno)); ASSERT (fd == -1); /* Same as before, ESCAPING_LINK_2 links to ESCAPING_LINK. */ fd = openat2 (dfd, "escaping_link_2", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT ((errno == EXDEV) | is_nofollow_error (errno)); ASSERT (fd == -1); /* ESCAPING_LINK links to the temporary directory itself (dfd). */ fd = openat2 (dfd, "escaping_link", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT ((errno == EXDEV) | is_nofollow_error (errno)); ASSERT (fd == -1); /* Although it points to a valid file in same path, the link refers to an absolute path. */ fd = openat2 (dfd, "invalid_link", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT ((errno == EXDEV) | is_nofollow_error (errno)); ASSERT (fd == -1); fd = openat2 (dfd, "valid_link", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT (is_nofollow_error (errno)); ASSERT (fd == -1); fd = openat2 (dfd, "should-not-work", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_IN_ROOT | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT ((errno == ENOENT) | is_nofollow_error (errno)); ASSERT (fd == -1); fd = openat2 (dfd, "subdir/aunt_link", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH, }), sizeof (struct open_how)); ASSERT (close (fd) == 0); { int subdfd = openat2 (dfd, "subdir", (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY, .resolve = RESOLVE_IN_ROOT | RESOLVE_NO_SYMLINKS, }), sizeof (struct open_how)); ASSERT (0 <= subdfd); /* Open the file within the subdir with both tst-openat2 and tst-openat2/subdir file descriptors. */ fd = openat2 (subdfd, "some_file", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_IN_ROOT, }), sizeof (struct open_how)); ASSERT (close (fd) == 0); fd = openat2 (dfd, "subdir/some_file", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_IN_ROOT, }), sizeof (struct open_how)); ASSERT (close (fd) == 0); /* Check that aunt_link cannot escape subdir. */ fd = openat2 (subdfd, "aunt_link", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH, }), sizeof (struct open_how)); ASSERT (errno == EXDEV); ASSERT (fd == -1); /* Check that RESOLVE_BENEATH works with trailing "/./". */ fd = openat2 (dfd, "subdir/./", (&(struct open_how) { .flags = O_RDONLY, .resolve = RESOLVE_BENEATH, }), sizeof (struct open_how)); ASSERT (close (fd) == 0); ASSERT (close (subdfd) == 0); } } static void do_test_basic () { int fd; fd = openat2 (dfd, "some-file", (&(struct open_how) { .flags = O_CREAT|O_RDWR|O_EXCL, .mode = 0666, }), sizeof (struct open_how)); ASSERT (0 <= fd); ASSERT (write (fd, "hello", 5) == 5); /* Before closing the file, try using this file descriptor to open another file. This must fail. */ { int fd2 = openat2 (fd, "should-not-work", (&(struct open_how) { .flags = O_CREAT|O_RDWR|O_EXCL, .mode = 0666, }), sizeof (struct open_how)); ASSERT (errno == ENOTDIR); ASSERT (fd2 == -1); } ASSERT (close (fd) == 0); ASSERT (unlinkat (dfd, "some-file", 0) == 0); ASSERT (unlinkat (dfd, "dirlink", 0) == 0); ASSERT (unlinkat (dfd, "dirlinkslash", 0) == 0); ASSERT (unlinkat (dfd, "escaping_link", 0) == 0); ASSERT (unlinkat (dfd, "escaping_link_2", 0) == 0); ASSERT (unlinkat (dfd, "invalid_link", 0) == 0); ASSERT (unlinkat (dfd, "some_file", 0) == 0); ASSERT (unlinkat (dfd, "subdir/aunt_link", 0) == 0); ASSERT (unlinkat (dfd, "subdir/some_file", 0) == 0); ASSERT (unlinkat (dfd, "subdir", AT_REMOVEDIR) == 0); ASSERT (unlinkat (dfd, "valid_link", 0) == 0); ASSERT (close (dfd) == 0); fd = openat2 (dfd, "some-file", (&(struct open_how) { .flags = O_CREAT|O_RDWR|O_EXCL, .mode = 0666, }), sizeof (struct open_how)); ASSERT (errno == EBADF); ASSERT (fd == -1); ASSERT (unlinkat (AT_FDCWD, temp_dir, AT_REMOVEDIR) == 0); } int main () { int result; struct open_how ro = { .flags = O_RDONLY }; /* Test behavior for invalid file descriptors. */ { errno = 0; ASSERT (openat2 (AT_FDCWD == -2 ? -1 : -2, "foo", &ro, sizeof ro) == -1); ASSERT (errno == EBADF); } { close (99); errno = 0; ASSERT (openat2 (99, "foo", &ro, sizeof ro) == -1); ASSERT (errno == EBADF); } /* Basic checks. */ result = test_open (do_open, false); dfd = open (".", O_RDONLY); ASSERT (0 <= dfd); ASSERT (test_open (do_open, false) == result); ASSERT (close (dfd) == 0); do_test_struct (); do_prepare (); do_test_flags (); #if !(defined _WIN32 && !defined __CYGWIN__) do_prepare_symlinks (); do_test_resolve (); do_test_basic (); #endif /* Check that even when *-safer modules are in use, plain openat2 can land in fd 0. Do this test last, since it is destructive to stdin. */ ASSERT (close (STDIN_FILENO) == 0); ASSERT (openat2 (AT_FDCWD, ".", &ro, sizeof ro) == STDIN_FILENO); { dfd = open (".", O_RDONLY); ASSERT (STDIN_FILENO < dfd); ASSERT (chdir ("..") == 0); ASSERT (close (STDIN_FILENO) == 0); ASSERT (openat2 (dfd, ".", &ro, sizeof ro) == STDIN_FILENO); ASSERT (close (dfd) == 0); } return result ? result : test_exit_status; }