mirror of
https://github.com/torvalds/linux.git
synced 2026-07-28 10:09:10 +02:00
These tests were written in the early days of selftests' TAP support, the more modern kselftest harness is much easier to follow and maintain. The actual contents of the tests are unchanged by this change. Most of the diff involves switching from the E_* syscall wrappers we previously used to ASSERT_EQ(fn(...), 0) in tests and helper functions. The first pass of the migration was done using Claude, followed by a manual rework and review. Assisted-by: Claude:claude-4.6-opus Signed-off-by: Aleksa Sarai <aleksa@amutable.com> Link: https://patch.msgid.link/20260401-openat2-selftests-kunit-v2-4-ad153a07da0c@amutable.com Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
160 lines
3.5 KiB
C
160 lines
3.5 KiB
C
// 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
|