Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
/*_test
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += $(KHDR_INCLUDES)
CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined $(TOOLS_INCLUDES)
TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test emptypath_test
# gcc requires -static-libasan in order to ensure that Address Sanitizer's
# library is the first one loaded. However, clang already statically links the
# Address Sanitizer if -fsanitize is specified. Therefore, simply omit
# -static-libasan for clang builds.
ifeq ($(LLVM),)
CFLAGS += -static-libasan
endif
LOCAL_HDRS += helpers.h
include ../../lib.mk
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include "kselftest_harness.h"
#ifndef O_EMPTYPATH
#define O_EMPTYPATH (1 << 26)
#endif
#define EMPTYPATH_TEST_FILE "/tmp/emptypath_test"
FIXTURE(emptypath) {
int opath_fd;
};
FIXTURE_SETUP(emptypath)
{
int fd;
self->opath_fd = -1;
fd = open(EMPTYPATH_TEST_FILE, O_CREAT | O_WRONLY, S_IRWXU);
ASSERT_GE(fd, 0) {
TH_LOG("create %s: %s", EMPTYPATH_TEST_FILE, strerror(errno));
}
close(fd);
self->opath_fd = open(EMPTYPATH_TEST_FILE, O_PATH);
ASSERT_GE(self->opath_fd, 0) {
TH_LOG("open %s O_PATH: %s", EMPTYPATH_TEST_FILE, strerror(errno));
}
}
FIXTURE_TEARDOWN(emptypath)
{
if (self->opath_fd >= 0)
close(self->opath_fd);
unlink(EMPTYPATH_TEST_FILE);
}
/* An empty path is rejected with ENOENT unless O_EMPTYPATH is set. */
TEST_F(emptypath, without_flag_returns_enoent)
{
int fd = openat(self->opath_fd, "", O_RDONLY);
if (fd >= 0)
close(fd);
ASSERT_LT(fd, 0) {
TH_LOG("empty path without O_EMPTYPATH unexpectedly succeeded");
}
EXPECT_EQ(errno, ENOENT) {
TH_LOG("expected ENOENT, got %s", strerror(errno));
}
}
/* O_EMPTYPATH reopens the O_PATH fd through an empty path. */
TEST_F(emptypath, reopens_opath_fd)
{
int fd = openat(self->opath_fd, "", O_RDONLY | O_EMPTYPATH);
if (fd < 0 && errno == EINVAL)
SKIP(return, "O_EMPTYPATH not supported");
ASSERT_GE(fd, 0) {
TH_LOG("O_EMPTYPATH failed: %s", strerror(errno));
}
close(fd);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
* Copyright (C) 2026 Amutable GmbH
*/
#ifndef __RESOLVEAT_H__
#define __RESOLVEAT_H__
#define _GNU_SOURCE
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <limits.h>
#include <linux/types.h>
#include <linux/unistd.h>
#include <linux/openat2.h>
#include "kselftest_harness.h"
#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */
#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0
__maybe_unused
static bool needs_openat2(const struct open_how *how)
{
return how->resolve != 0;
}
__maybe_unused
static int raw_openat2(int dfd, const char *path, void *how, size_t size)
{
int ret = syscall(__NR_openat2, dfd, path, how, size);
return ret >= 0 ? ret : -errno;
}
__maybe_unused
static int sys_openat2(int dfd, const char *path, struct open_how *how)
{
return raw_openat2(dfd, path, how, sizeof(*how));
}
__maybe_unused
static int sys_openat(int dfd, const char *path, struct open_how *how)
{
int ret = openat(dfd, path, how->flags, how->mode);
return ret >= 0 ? ret : -errno;
}
__maybe_unused
static int sys_renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, unsigned int flags)
{
int ret = syscall(__NR_renameat2, olddirfd, oldpath,
newdirfd, newpath, flags);
return ret >= 0 ? ret : -errno;
}
__maybe_unused
static int touchat(int dfd, const char *path)
{
int fd = openat(dfd, path, O_CREAT, 0700);
if (fd >= 0)
close(fd);
return fd;
}
__maybe_unused
static char *fdreadlink(struct __test_metadata *_metadata, int fd)
{
char *target, *tmp;
ASSERT_GT(asprintf(&tmp, "/proc/self/fd/%d", fd), 0);
target = malloc(PATH_MAX);
ASSERT_NE(target, NULL);
memset(target, 0, PATH_MAX);
ASSERT_GT(readlink(tmp, target, PATH_MAX), 0);
free(tmp);
return target;
}
__maybe_unused
static bool fdequal(struct __test_metadata *_metadata, int fd,
int dfd, const char *path)
{
char *fdpath, *dfdpath, *other;
bool cmp;
fdpath = fdreadlink(_metadata, fd);
dfdpath = fdreadlink(_metadata, dfd);
if (!path) {
ASSERT_GT(asprintf(&other, "%s", dfdpath), 0);
} else if (*path == '/') {
ASSERT_GT(asprintf(&other, "%s", path), 0);
} else {
ASSERT_GT(asprintf(&other, "%s/%s", dfdpath, path), 0);
}
cmp = !strcmp(fdpath, other);
free(fdpath);
free(dfdpath);
free(other);
return cmp;
}
static bool openat2_supported = false;
__attribute__((constructor))
static void __detect_openat2_supported(void)
{
struct open_how how = {};
int fd;
BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0);
/* Check openat2(2) support. */
fd = sys_openat2(AT_FDCWD, ".", &how);
openat2_supported = (fd >= 0);
if (fd >= 0)
close(fd);
}
#endif /* __RESOLVEAT_H__ */
@@ -0,0 +1,362 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__ // Use ll64
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "helpers.h"
#include "kselftest_harness.h"
/*
* O_LARGEFILE is set to 0 by glibc.
* XXX: This is wrong on {mips, parisc, powerpc, sparc}.
*/
#undef O_LARGEFILE
#ifdef __aarch64__
#define O_LARGEFILE 0x20000
#else
#define O_LARGEFILE 0x8000
#endif
struct open_how_ext {
struct open_how inner;
uint32_t extra1;
char pad1[128];
uint32_t extra2;
char pad2[128];
uint32_t extra3;
};
struct struct_test {
const char *name;
struct open_how_ext arg;
size_t size;
int err;
};
struct flag_test {
const char *name;
struct open_how how;
int err;
};
FIXTURE(openat2) {};
FIXTURE_SETUP(openat2)
{
if (!openat2_supported)
SKIP(return, "openat2(2) not supported");
}
FIXTURE_TEARDOWN(openat2) {}
/*
* Verify that the struct size and misalignment handling for openat2(2) is
* correct, including that is_zeroed_user() works.
*/
TEST_F(openat2, struct_argument_sizes)
{
int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 };
struct struct_test tests[] = {
/* Normal struct. */
{ .name = "normal struct",
.arg.inner.flags = O_RDONLY,
.size = sizeof(struct open_how) },
/* Bigger struct, with zeroed out end. */
{ .name = "bigger struct (zeroed out)",
.arg.inner.flags = O_RDONLY,
.size = sizeof(struct open_how_ext) },
/* TODO: Once expanded, check zero-padding. */
/* Smaller than version-0 struct. */
{ .name = "zero-sized 'struct'",
.arg.inner.flags = O_RDONLY, .size = 0, .err = -EINVAL },
{ .name = "smaller-than-v0 struct",
.arg.inner.flags = O_RDONLY,
.size = OPEN_HOW_SIZE_VER0 - 1, .err = -EINVAL },
/* Bigger struct, with non-zero trailing bytes. */
{ .name = "bigger struct (non-zero data in first 'future field')",
.arg.inner.flags = O_RDONLY, .arg.extra1 = 0xdeadbeef,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
{ .name = "bigger struct (non-zero data in middle of 'future fields')",
.arg.inner.flags = O_RDONLY, .arg.extra2 = 0xfeedcafe,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
{ .name = "bigger struct (non-zero data at end of 'future fields')",
.arg.inner.flags = O_RDONLY, .arg.extra3 = 0xabad1dea,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++) {
struct struct_test *test = &tests[i];
struct open_how_ext how_ext = test->arg;
for (int j = 0; j < ARRAY_SIZE(misalignments); j++) {
int fd, misalign = misalignments[j];
void *copy = NULL, *how_copy = &how_ext;
char *fdpath = NULL;
if (misalign) {
/*
* Explicitly misalign the structure copying it with the given
* (mis)alignment offset. The other data is set to be non-zero to
* make sure that non-zero bytes outside the struct aren't checked
*
* This is effectively to check that is_zeroed_user() works.
*/
copy = malloc(misalign + sizeof(how_ext));
how_copy = copy + misalign;
memset(copy, 0xff, misalign);
memcpy(how_copy, &how_ext, sizeof(how_ext));
}
fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size);
if (fd >= 0) {
fdpath = fdreadlink(_metadata, fd);
close(fd);
}
if (test->err >= 0) {
EXPECT_GE(fd, 0) {
TH_LOG("openat2 with %s [misalign=%d] should succeed, got %d (%s)",
test->name, misalign,
fd, strerror(-fd));
}
} else {
EXPECT_EQ(test->err, fd) {
if (fdpath)
TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d['%s']",
test->name, misalign,
test->err,
strerror(-test->err),
fd, fdpath);
else
TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d (%s)",
test->name, misalign,
test->err,
strerror(-test->err),
fd, strerror(-fd));
}
}
free(copy);
free(fdpath);
}
}
}
/* Verify openat2(2) flag and mode validation. */
TEST_F(openat2, flag_validation)
{
struct flag_test tests[] = {
/* O_TMPFILE is incompatible with O_PATH and O_CREAT. */
{ .name = "incompatible flags (O_TMPFILE | O_PATH)",
.how.flags = O_TMPFILE | O_PATH | O_RDWR, .err = -EINVAL },
{ .name = "incompatible flags (O_TMPFILE | O_CREAT)",
.how.flags = O_TMPFILE | O_CREAT | O_RDWR, .err = -EINVAL },
/* O_PATH only permits certain other flags to be set ... */
{ .name = "compatible flags (O_PATH | O_CLOEXEC)",
.how.flags = O_PATH | O_CLOEXEC },
{ .name = "compatible flags (O_PATH | O_DIRECTORY)",
.how.flags = O_PATH | O_DIRECTORY },
{ .name = "compatible flags (O_PATH | O_NOFOLLOW)",
.how.flags = O_PATH | O_NOFOLLOW },
/* ... and others are absolutely not permitted. */
{ .name = "incompatible flags (O_PATH | O_RDWR)",
.how.flags = O_PATH | O_RDWR, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_CREAT)",
.how.flags = O_PATH | O_CREAT, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_EXCL)",
.how.flags = O_PATH | O_EXCL, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_NOCTTY)",
.how.flags = O_PATH | O_NOCTTY, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_DIRECT)",
.how.flags = O_PATH | O_DIRECT, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_LARGEFILE)",
.how.flags = O_PATH | O_LARGEFILE, .err = -EINVAL },
/* ->mode must only be set with O_{CREAT,TMPFILE}. */
{ .name = "non-zero how.mode and O_RDONLY",
.how.flags = O_RDONLY, .how.mode = 0600, .err = -EINVAL },
{ .name = "non-zero how.mode and O_PATH",
.how.flags = O_PATH, .how.mode = 0600, .err = -EINVAL },
{ .name = "valid how.mode and O_CREAT",
.how.flags = O_CREAT, .how.mode = 0600 },
{ .name = "valid how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR, .how.mode = 0600 },
/* ->mode must only contain 0777 bits. */
{ .name = "invalid how.mode and O_CREAT",
.how.flags = O_CREAT,
.how.mode = 0xFFFF, .err = -EINVAL },
{ .name = "invalid (very large) how.mode and O_CREAT",
.how.flags = O_CREAT,
.how.mode = 0xC000000000000000ULL, .err = -EINVAL },
{ .name = "invalid how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.mode = 0x1337, .err = -EINVAL },
{ .name = "invalid (very large) how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.mode = 0x0000A00000000000ULL, .err = -EINVAL },
/* ->resolve flags must not conflict. */
{ .name = "incompatible resolve flags (BENEATH | IN_ROOT)",
.how.flags = O_RDONLY,
.how.resolve = RESOLVE_BENEATH | RESOLVE_IN_ROOT,
.err = -EINVAL },
/* ->resolve must only contain RESOLVE_* flags. */
{ .name = "invalid how.resolve and O_RDONLY",
.how.flags = O_RDONLY,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_CREAT",
.how.flags = O_CREAT,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_PATH",
.how.flags = O_PATH,
.how.resolve = 0x1337, .err = -EINVAL },
/* currently unknown upper 32 bit rejected. */
{ .name = "currently unknown bit (1 << 63)",
.how.flags = O_RDONLY | (1ULL << 63),
.how.resolve = 0, .err = -EINVAL },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++) {
int fd, fdflags = -1;
char *path, *fdpath = NULL;
struct flag_test *test = &tests[i];
path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : ".";
unlink(path);
fd = sys_openat2(AT_FDCWD, path, &test->how);
if (fd < 0 && fd == -EOPNOTSUPP) {
/*
* Skip the testcase if it failed because not supported
* by FS. (e.g. a valid O_TMPFILE combination on NFS)
*/
TH_LOG("openat2 with %s not supported by FS -- skipping",
test->name);
continue;
}
if (test->err >= 0) {
EXPECT_GE(fd, 0) {
TH_LOG("openat2 with %s should succeed, got %d (%s)",
test->name, fd, strerror(-fd));
}
if (fd >= 0) {
int otherflags;
fdpath = fdreadlink(_metadata, fd);
fdflags = fcntl(fd, F_GETFL);
otherflags = fcntl(fd, F_GETFD);
close(fd);
ASSERT_GE(fdflags, 0);
ASSERT_GE(otherflags, 0);
/* O_CLOEXEC isn't shown in F_GETFL. */
if (otherflags & FD_CLOEXEC)
fdflags |= O_CLOEXEC;
/* O_CREAT is hidden from F_GETFL. */
if (test->how.flags & O_CREAT)
fdflags |= O_CREAT;
if (!(test->how.flags & O_LARGEFILE))
fdflags &= ~O_LARGEFILE;
EXPECT_EQ(fdflags, (int)test->how.flags) {
TH_LOG("openat2 with %s: flags mismatch %X != %llX",
test->name, fdflags,
(unsigned long long)test->how.flags);
}
}
} else {
EXPECT_EQ(test->err, fd) {
if (fd >= 0) {
fdpath = fdreadlink(_metadata, fd);
TH_LOG("openat2 with %s should fail with %d (%s), got %d['%s']",
test->name, test->err,
strerror(-test->err),
fd, fdpath);
} else {
TH_LOG("openat2 with %s should fail with %d (%s), got %d (%s)",
test->name, test->err,
strerror(-test->err),
fd, strerror(-fd));
}
}
if (fd >= 0)
close(fd);
}
free(fdpath);
}
}
#ifndef OPENAT2_REGULAR
#define OPENAT2_REGULAR ((__u64)1 << 32)
#endif
#ifndef EFTYPE
#define EFTYPE 134
#endif
/* Kernel-internal carrier for OPENAT2_REGULAR (see __O_REGULAR in fcntl.h). */
#ifndef __O_REGULAR
#define __O_REGULAR (1 << 30)
#endif
/* Verify that OPENAT2_REGULAR rejects non-regular files with EFTYPE. */
TEST_F(openat2, regular_flag)
{
struct open_how how = {
.flags = OPENAT2_REGULAR | O_RDONLY,
};
int fd;
fd = sys_openat2(AT_FDCWD, "/dev/null", &how);
if (fd == -ENOENT)
SKIP(return, "/dev/null does not exist");
EXPECT_EQ(-EFTYPE, fd) {
TH_LOG("openat2 with OPENAT2_REGULAR should fail with %d (%s), got %d (%s)",
-EFTYPE, strerror(EFTYPE), fd, strerror(-fd));
}
if (fd >= 0)
close(fd);
}
/* open()/openat() must keep ignoring the internal __O_REGULAR bit. */
TEST(legacy_openat_ignores_o_regular)
{
int fd;
fd = openat(AT_FDCWD, "/dev/null", O_RDONLY | __O_REGULAR);
if (fd < 0 && errno == ENOENT)
SKIP(return, "/dev/null does not exist");
ASSERT_GE(fd, 0) {
TH_LOG("legacy openat() must ignore the __O_REGULAR carrier bit, got errno %d (%s)",
errno, strerror(errno));
}
close(fd);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <syscall.h>
#include <limits.h>
#include <unistd.h>
#include "helpers.h"
#include "kselftest_harness.h"
#define ROUNDS 400000
/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
pid_t spawn_attack(struct __test_metadata *_metadata,
int dirfd, char *a, char *b)
{
pid_t child = fork();
if (child != 0)
return child;
/* If the parent (the test process) dies, kill ourselves too. */
ASSERT_EQ(prctl(PR_SET_PDEATHSIG, SIGKILL), 0);
/* Swap @a and @b. */
for (;;)
renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
exit(1);
}
/*
* Construct a test directory with the following structure:
*
* root/
* |-- a/
* | `-- c/
* `-- b/
*/
FIXTURE(rename_attack) {
int dfd;
int afd;
pid_t child;
};
FIXTURE_SETUP(rename_attack)
{
char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
self->dfd = -1;
self->afd = -1;
self->child = 0;
/* Make the top-level directory. */
ASSERT_NE(mkdtemp(dirname), NULL);
self->dfd = open(dirname, O_PATH | O_DIRECTORY);
ASSERT_GE(self->dfd, 0);
ASSERT_EQ(mkdirat(self->dfd, "a", 0755), 0);
ASSERT_EQ(mkdirat(self->dfd, "b", 0755), 0);
ASSERT_EQ(mkdirat(self->dfd, "a/c", 0755), 0);
self->afd = openat(self->dfd, "a", O_PATH);
ASSERT_GE(self->afd, 0);
self->child = spawn_attack(_metadata, self->dfd, "a/c", "b");
ASSERT_GT(self->child, 0);
}
FIXTURE_TEARDOWN(rename_attack)
{
if (self->child > 0)
kill(self->child, SIGKILL);
if (self->afd >= 0)
close(self->afd);
if (self->dfd >= 0)
close(self->dfd);
}
FIXTURE_VARIANT(rename_attack) {
int resolve;
const char *name;
};
FIXTURE_VARIANT_ADD(rename_attack, resolve_beneath) {
.resolve = RESOLVE_BENEATH,
.name = "RESOLVE_BENEATH",
};
FIXTURE_VARIANT_ADD(rename_attack, resolve_in_root) {
.resolve = RESOLVE_IN_ROOT,
.name = "RESOLVE_IN_ROOT",
};
TEST_F_TIMEOUT(rename_attack, test, 120)
{
int escapes = 0, successes = 0, other_errs = 0, exdevs = 0, eagains = 0;
char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
struct open_how how = {
.flags = O_PATH,
.resolve = variant->resolve,
};
if (!openat2_supported) {
how.resolve = 0;
TH_LOG("openat2(2) unsupported -- using openat(2) instead");
}
for (int i = 0; i < ROUNDS; i++) {
int fd;
if (openat2_supported)
fd = sys_openat2(self->afd, victim_path, &how);
else
fd = sys_openat(self->afd, victim_path, &how);
if (fd < 0) {
if (fd == -EAGAIN)
eagains++;
else if (fd == -EXDEV)
exdevs++;
else if (fd == -ENOENT)
escapes++; /* escaped outside and got ENOENT... */
else
other_errs++; /* unexpected error */
} else {
if (fdequal(_metadata, fd, self->afd, NULL))
successes++;
else
escapes++; /* we got an unexpected fd */
}
if (fd >= 0)
close(fd);
}
TH_LOG("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d",
eagains, exdevs, other_errs, successes);
ASSERT_EQ(escapes, 0) {
TH_LOG("rename attack with %s (%d runs, got %d escapes)",
variant->name, ROUNDS, escapes);
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,561 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "helpers.h"
#include "kselftest_harness.h"
struct resolve_test {
const char *name;
const char *dir;
const char *path;
struct open_how how;
bool pass;
union {
int err;
const char *path;
} out;
};
/*
* Verify a single resolve test case. This must be called from within a TEST_F
* function with _metadata in scope.
*/
static void verify_resolve_test(struct __test_metadata *_metadata,
int rootfd, int hardcoded_fd,
const struct resolve_test *test)
{
struct open_how how = test->how;
int dfd, fd;
char *fdpath = NULL;
/* Auto-set O_PATH. */
if (!(how.flags & O_CREAT))
how.flags |= O_PATH;
if (test->dir)
dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
else
dfd = dup(rootfd);
ASSERT_GE(dfd, 0) TH_LOG("failed to open dir '%s': %m", test->dir ?: ".");
ASSERT_EQ(dup2(dfd, hardcoded_fd), hardcoded_fd);
fd = sys_openat2(dfd, test->path, &how);
if (test->pass) {
EXPECT_GE(fd, 0) {
TH_LOG("%s: expected success, got %d (%s)",
test->name, fd, strerror(-fd));
}
if (fd >= 0) {
EXPECT_TRUE(fdequal(_metadata, fd, rootfd, test->out.path)) {
fdpath = fdreadlink(_metadata, fd);
TH_LOG("%s: wrong path '%s', expected '%s'",
test->name, fdpath,
test->out.path ?: ".");
free(fdpath);
}
}
} else {
EXPECT_EQ(test->out.err, fd) {
if (fd >= 0) {
fdpath = fdreadlink(_metadata, fd);
TH_LOG("%s: expected %d (%s), got %d['%s']",
test->name, test->out.err,
strerror(-test->out.err), fd, fdpath);
free(fdpath);
} else {
TH_LOG("%s: expected %d (%s), got %d (%s)",
test->name, test->out.err,
strerror(-test->out.err),
fd, strerror(-fd));
}
}
}
if (fd >= 0)
close(fd);
close(dfd);
}
/*
* Construct a test directory with the following structure:
*
* root/
* |-- procexe -> /proc/self/exe
* |-- procroot -> /proc/self/root
* |-- root/
* |-- mnt/ [mountpoint]
* | |-- self -> ../mnt/
* | `-- absself -> /mnt/
* |-- etc/
* | `-- passwd
* |-- creatlink -> /newfile3
* |-- reletc -> etc/
* |-- relsym -> etc/passwd
* |-- absetc -> /etc/
* |-- abssym -> /etc/passwd
* |-- abscheeky -> /cheeky
* `-- cheeky/
* |-- absself -> /
* |-- self -> ../../root/
* |-- garbageself -> /../../root/
* |-- passwd -> ../cheeky/../etc/../etc/passwd
* |-- abspasswd -> /../cheeky/../etc/../etc/passwd
* |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
* `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
*/
FIXTURE(openat2_resolve) {
int rootfd;
int hardcoded_fd;
char *hardcoded_fdpath;
char *procselfexe;
};
FIXTURE_SETUP(openat2_resolve)
{
char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
int dfd, tmpfd;
self->rootfd = -1;
self->hardcoded_fd = -1;
self->hardcoded_fdpath = NULL;
self->procselfexe = NULL;
/* NOTE: We should be checking for CAP_SYS_ADMIN here... */
if (geteuid() != 0)
SKIP(return, "all tests require euid == 0");
if (!openat2_supported)
SKIP(return, "openat2(2) not supported");
/* Unshare and make /tmp a new directory. */
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(mount("", "/tmp", "", MS_PRIVATE, ""), 0);
/* Make the top-level directory. */
ASSERT_NE(mkdtemp(dirname), NULL);
dfd = open(dirname, O_PATH | O_DIRECTORY);
ASSERT_GE(dfd, 0);
/* A sub-directory which is actually used for tests. */
ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
ASSERT_GE(tmpfd, 0);
close(dfd);
dfd = tmpfd;
ASSERT_EQ(symlinkat("/proc/self/exe", dfd, "procexe"), 0);
ASSERT_EQ(symlinkat("/proc/self/root", dfd, "procroot"), 0);
ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
/* There is no mountat(2), so use chdir. */
ASSERT_EQ(mkdirat(dfd, "mnt", 0755), 0);
ASSERT_EQ(fchdir(dfd), 0);
ASSERT_EQ(mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""), 0);
ASSERT_EQ(symlinkat("../mnt/", dfd, "mnt/self"), 0);
ASSERT_EQ(symlinkat("/mnt/", dfd, "mnt/absself"), 0);
ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0);
ASSERT_GE(touchat(dfd, "etc/passwd"), 0);
ASSERT_EQ(symlinkat("/newfile3", dfd, "creatlink"), 0);
ASSERT_EQ(symlinkat("etc/", dfd, "reletc"), 0);
ASSERT_EQ(symlinkat("etc/passwd", dfd, "relsym"), 0);
ASSERT_EQ(symlinkat("/etc/", dfd, "absetc"), 0);
ASSERT_EQ(symlinkat("/etc/passwd", dfd, "abssym"), 0);
ASSERT_EQ(symlinkat("/cheeky", dfd, "abscheeky"), 0);
ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0);
ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0);
ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0);
ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0);
ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd",
dfd, "cheeky/passwd"), 0);
ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd",
dfd, "cheeky/abspasswd"), 0);
ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd",
dfd, "cheeky/dotdotlink"), 0);
ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd",
dfd, "cheeky/garbagelink"), 0);
self->rootfd = dfd;
self->hardcoded_fd = open("/dev/null", O_RDONLY);
ASSERT_GE(self->hardcoded_fd, 0);
ASSERT_GE(asprintf(&self->hardcoded_fdpath, "self/fd/%d",
self->hardcoded_fd), 0);
ASSERT_GE(asprintf(&self->procselfexe, "/proc/%d/exe", getpid()), 0);
}
FIXTURE_TEARDOWN(openat2_resolve)
{
free(self->procselfexe);
free(self->hardcoded_fdpath);
if (self->hardcoded_fd >= 0)
close(self->hardcoded_fd);
if (self->rootfd >= 0)
close(self->rootfd);
}
/* Attempts to cross the dirfd should be blocked with -EXDEV. */
TEST_F(openat2_resolve, resolve_beneath)
{
struct resolve_test tests[] = {
/* Attempts to cross dirfd should be blocked. */
{ .name = "[beneath] jump to /",
.path = "/", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] absolute link to $root",
.path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained absolute links to $root",
.path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] jump outside $root",
.path = "..", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] temporary jump outside $root",
.path = "../root/", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] symlink temporary jump outside $root",
.path = "cheeky/self", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained symlink temporary jump outside $root",
.path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] garbage links to $root",
.path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained garbage links to $root",
.path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
/* Only relative paths that stay inside dirfd should work. */
{ .name = "[beneath] ordinary path to 'root'",
.path = "root", .how.resolve = RESOLVE_BENEATH,
.out.path = "root", .pass = true },
{ .name = "[beneath] ordinary path to 'etc'",
.path = "etc", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc", .pass = true },
{ .name = "[beneath] ordinary path to 'etc/passwd'",
.path = "etc/passwd", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc/passwd", .pass = true },
{ .name = "[beneath] relative symlink inside $root",
.path = "relsym", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc/passwd", .pass = true },
{ .name = "[beneath] chained-'..' relative symlink inside $root",
.path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc/passwd", .pass = true },
{ .name = "[beneath] absolute symlink component outside $root",
.path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] absolute symlink target outside $root",
.path = "abssym", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] absolute path outside $root",
.path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] cheeky absolute path outside $root",
.path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained cheeky absolute path outside $root",
.path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
/* Tricky paths should fail. */
{ .name = "[beneath] tricky '..'-chained symlink outside $root",
.path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
.path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] tricky garbage link outside $root",
.path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] tricky absolute + garbage link outside $root",
.path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* All attempts to cross the dirfd will be scoped-to-root. */
TEST_F(openat2_resolve, resolve_in_root)
{
struct resolve_test tests[] = {
{ .name = "[in_root] jump to /",
.path = "/", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] absolute symlink to /root",
.path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] chained absolute symlinks to /root",
.path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] '..' at root",
.path = "..", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] '../root' at root",
.path = "../root/", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] relative symlink containing '..' above root",
.path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] garbage link to /root",
.path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] chained garbage links to /root",
.path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] relative path to 'root'",
.path = "root", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] relative path to 'etc'",
.path = "etc", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc", .pass = true },
{ .name = "[in_root] relative path to 'etc/passwd'",
.path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] relative symlink to 'etc/passwd'",
.path = "relsym", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
.path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
.path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] absolute symlink to 'etc/passwd'",
.path = "abssym", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] absolute path 'etc/passwd'",
.path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] cheeky absolute path 'etc/passwd'",
.path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
.path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky '..'-chained symlink outside $root",
.path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
.path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
.path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky garbage link outside $root",
.path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute + garbage link outside $root",
.path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
.path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
/* O_CREAT should handle trailing symlinks correctly. */
{ .name = "[in_root] O_CREAT of relative path inside $root",
.path = "newfile1", .how.flags = O_CREAT,
.how.mode = 0700,
.how.resolve = RESOLVE_IN_ROOT,
.out.path = "newfile1", .pass = true },
{ .name = "[in_root] O_CREAT of absolute path",
.path = "/newfile2", .how.flags = O_CREAT,
.how.mode = 0700,
.how.resolve = RESOLVE_IN_ROOT,
.out.path = "newfile2", .pass = true },
{ .name = "[in_root] O_CREAT of tricky symlink outside root",
.path = "/creatlink", .how.flags = O_CREAT,
.how.mode = 0700,
.how.resolve = RESOLVE_IN_ROOT,
.out.path = "newfile3", .pass = true },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* Crossing mount boundaries should be blocked. */
TEST_F(openat2_resolve, resolve_no_xdev)
{
struct resolve_test tests[] = {
/* Crossing *down* into a mountpoint is disallowed. */
{ .name = "[no_xdev] cross into $mnt",
.path = "mnt", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] cross into $mnt/",
.path = "mnt/", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] cross into $mnt/.",
.path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Crossing *up* out of a mountpoint is disallowed. */
{ .name = "[no_xdev] goto mountpoint root",
.dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV,
.out.path = "mnt", .pass = true },
{ .name = "[no_xdev] cross up through '..'",
.dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] temporary cross up through '..'",
.dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] temporary relative symlink cross up",
.dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] temporary absolute symlink cross up",
.dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Jumping to "/" is ok, but later components cannot cross. */
{ .name = "[no_xdev] jump to / directly",
.dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
.out.path = "/", .pass = true },
{ .name = "[no_xdev] jump to / (from /) directly",
.dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
.out.path = "/", .pass = true },
{ .name = "[no_xdev] jump to / then proc",
.path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] jump to / then tmp",
.path = "/tmp", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Magic-links are blocked since they can switch vfsmounts. */
{ .name = "[no_xdev] cross through magic-link to self/root",
.dir = "/proc", .path = "self/root", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] cross through magic-link to self/cwd",
.dir = "/proc", .path = "self/cwd", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Except magic-link jumps inside the same vfsmount. */
{ .name = "[no_xdev] jump through magic-link to same procfs",
.dir = "/proc", .path = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
.out.path = "/proc", .pass = true, },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* Procfs-style magic-link resolution should be blocked. */
TEST_F(openat2_resolve, resolve_no_magiclinks)
{
struct resolve_test tests[] = {
/* Regular symlinks should work. */
{ .name = "[no_magiclinks] ordinary relative symlink",
.path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.path = "etc/passwd", .pass = true },
/* Magic-links should not work. */
{ .name = "[no_magiclinks] symlink to magic-link",
.path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] normal path to magic-link",
.path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
.path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_MAGICLINKS,
.out.path = self->procselfexe, .pass = true },
{ .name = "[no_magiclinks] symlink to magic-link path component",
.path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] magic-link path component",
.path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
.path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* All symlink resolution should be blocked. */
TEST_F(openat2_resolve, resolve_no_symlinks)
{
struct resolve_test tests[] = {
/* Normal paths should work. */
{ .name = "[no_symlinks] ordinary path to '.'",
.path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = NULL, .pass = true },
{ .name = "[no_symlinks] ordinary path to 'root'",
.path = "root", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "root", .pass = true },
{ .name = "[no_symlinks] ordinary path to 'etc'",
.path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "etc", .pass = true },
{ .name = "[no_symlinks] ordinary path to 'etc/passwd'",
.path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "etc/passwd", .pass = true },
/* Regular symlinks are blocked. */
{ .name = "[no_symlinks] relative symlink target",
.path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] relative symlink component",
.path = "reletc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] absolute symlink target",
.path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] absolute symlink component",
.path = "absetc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] cheeky garbage link",
.path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] cheeky absolute + garbage link",
.path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] cheeky absolute + absolute symlink",
.path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
/* Trailing symlinks with NO_FOLLOW. */
{ .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
.path = "relsym", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "relsym", .pass = true },
{ .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
.path = "abssym", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "abssym", .pass = true },
{ .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
.path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "cheeky/garbagelink", .pass = true },
{ .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
.path = "abscheeky/absself", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
.path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
TEST_HARNESS_MAIN