linux/tools/testing/selftests/filesystems/openat2/rename_attack_test.c
Aleksa Sarai 215be76e02
selftests: openat2: migrate to kselftest harness
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>
2026-05-21 10:53:29 +02:00

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