mirror of
https://git.savannah.gnu.org/git/gnulib.git
synced 2026-10-24 00:48:56 +03:00
* tests/test-openat2.c (do_test_basic): Close fd before removing its file. This fixes a file descriptor leak, and ports the test to NFS, where removing an unclosed file leaves behind a .nfs* file that prevents the parent directory from being removed.
644 lines
17 KiB
C
644 lines
17 KiB
C
/* 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 <https://www.gnu.org/licenses/>. */
|
|
|
|
/* Written by Paul Eggert and Adhemerval Zanella. */
|
|
|
|
#include <config.h>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include "signature.h"
|
|
SIGNATURE_CHECK (openat2, int, (int, char const *,
|
|
struct open_how const *, size_t));
|
|
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdcountof.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#if HAVE_DECL_ALARM
|
|
# include <signal.h>
|
|
#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;
|
|
}
|