162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0-or-later
262306a36Sopenharmony_ci/*
362306a36Sopenharmony_ci * Author: Aleksa Sarai <cyphar@cyphar.com>
462306a36Sopenharmony_ci * Copyright (C) 2018-2019 SUSE LLC.
562306a36Sopenharmony_ci */
662306a36Sopenharmony_ci
762306a36Sopenharmony_ci#define _GNU_SOURCE
862306a36Sopenharmony_ci#include <errno.h>
962306a36Sopenharmony_ci#include <fcntl.h>
1062306a36Sopenharmony_ci#include <sched.h>
1162306a36Sopenharmony_ci#include <sys/stat.h>
1262306a36Sopenharmony_ci#include <sys/types.h>
1362306a36Sopenharmony_ci#include <sys/mount.h>
1462306a36Sopenharmony_ci#include <sys/mman.h>
1562306a36Sopenharmony_ci#include <sys/prctl.h>
1662306a36Sopenharmony_ci#include <signal.h>
1762306a36Sopenharmony_ci#include <stdio.h>
1862306a36Sopenharmony_ci#include <stdlib.h>
1962306a36Sopenharmony_ci#include <stdbool.h>
2062306a36Sopenharmony_ci#include <string.h>
2162306a36Sopenharmony_ci#include <syscall.h>
2262306a36Sopenharmony_ci#include <limits.h>
2362306a36Sopenharmony_ci#include <unistd.h>
2462306a36Sopenharmony_ci
2562306a36Sopenharmony_ci#include "../kselftest.h"
2662306a36Sopenharmony_ci#include "helpers.h"
2762306a36Sopenharmony_ci
2862306a36Sopenharmony_ci/* Construct a test directory with the following structure:
2962306a36Sopenharmony_ci *
3062306a36Sopenharmony_ci * root/
3162306a36Sopenharmony_ci * |-- a/
3262306a36Sopenharmony_ci * |   `-- c/
3362306a36Sopenharmony_ci * `-- b/
3462306a36Sopenharmony_ci */
3562306a36Sopenharmony_ciint setup_testdir(void)
3662306a36Sopenharmony_ci{
3762306a36Sopenharmony_ci	int dfd;
3862306a36Sopenharmony_ci	char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
3962306a36Sopenharmony_ci
4062306a36Sopenharmony_ci	/* Make the top-level directory. */
4162306a36Sopenharmony_ci	if (!mkdtemp(dirname))
4262306a36Sopenharmony_ci		ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
4362306a36Sopenharmony_ci	dfd = open(dirname, O_PATH | O_DIRECTORY);
4462306a36Sopenharmony_ci	if (dfd < 0)
4562306a36Sopenharmony_ci		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
4662306a36Sopenharmony_ci
4762306a36Sopenharmony_ci	E_mkdirat(dfd, "a", 0755);
4862306a36Sopenharmony_ci	E_mkdirat(dfd, "b", 0755);
4962306a36Sopenharmony_ci	E_mkdirat(dfd, "a/c", 0755);
5062306a36Sopenharmony_ci
5162306a36Sopenharmony_ci	return dfd;
5262306a36Sopenharmony_ci}
5362306a36Sopenharmony_ci
5462306a36Sopenharmony_ci/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
5562306a36Sopenharmony_cipid_t spawn_attack(int dirfd, char *a, char *b)
5662306a36Sopenharmony_ci{
5762306a36Sopenharmony_ci	pid_t child = fork();
5862306a36Sopenharmony_ci	if (child != 0)
5962306a36Sopenharmony_ci		return child;
6062306a36Sopenharmony_ci
6162306a36Sopenharmony_ci	/* If the parent (the test process) dies, kill ourselves too. */
6262306a36Sopenharmony_ci	E_prctl(PR_SET_PDEATHSIG, SIGKILL);
6362306a36Sopenharmony_ci
6462306a36Sopenharmony_ci	/* Swap @a and @b. */
6562306a36Sopenharmony_ci	for (;;)
6662306a36Sopenharmony_ci		renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
6762306a36Sopenharmony_ci	exit(1);
6862306a36Sopenharmony_ci}
6962306a36Sopenharmony_ci
7062306a36Sopenharmony_ci#define NUM_RENAME_TESTS 2
7162306a36Sopenharmony_ci#define ROUNDS 400000
7262306a36Sopenharmony_ci
7362306a36Sopenharmony_ciconst char *flagname(int resolve)
7462306a36Sopenharmony_ci{
7562306a36Sopenharmony_ci	switch (resolve) {
7662306a36Sopenharmony_ci	case RESOLVE_IN_ROOT:
7762306a36Sopenharmony_ci		return "RESOLVE_IN_ROOT";
7862306a36Sopenharmony_ci	case RESOLVE_BENEATH:
7962306a36Sopenharmony_ci		return "RESOLVE_BENEATH";
8062306a36Sopenharmony_ci	}
8162306a36Sopenharmony_ci	return "(unknown)";
8262306a36Sopenharmony_ci}
8362306a36Sopenharmony_ci
8462306a36Sopenharmony_civoid test_rename_attack(int resolve)
8562306a36Sopenharmony_ci{
8662306a36Sopenharmony_ci	int dfd, afd;
8762306a36Sopenharmony_ci	pid_t child;
8862306a36Sopenharmony_ci	void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
8962306a36Sopenharmony_ci	int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0;
9062306a36Sopenharmony_ci
9162306a36Sopenharmony_ci	struct open_how how = {
9262306a36Sopenharmony_ci		.flags = O_PATH,
9362306a36Sopenharmony_ci		.resolve = resolve,
9462306a36Sopenharmony_ci	};
9562306a36Sopenharmony_ci
9662306a36Sopenharmony_ci	if (!openat2_supported) {
9762306a36Sopenharmony_ci		how.resolve = 0;
9862306a36Sopenharmony_ci		ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n");
9962306a36Sopenharmony_ci	}
10062306a36Sopenharmony_ci
10162306a36Sopenharmony_ci	dfd = setup_testdir();
10262306a36Sopenharmony_ci	afd = openat(dfd, "a", O_PATH);
10362306a36Sopenharmony_ci	if (afd < 0)
10462306a36Sopenharmony_ci		ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n");
10562306a36Sopenharmony_ci
10662306a36Sopenharmony_ci	child = spawn_attack(dfd, "a/c", "b");
10762306a36Sopenharmony_ci
10862306a36Sopenharmony_ci	for (int i = 0; i < ROUNDS; i++) {
10962306a36Sopenharmony_ci		int fd;
11062306a36Sopenharmony_ci		char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
11162306a36Sopenharmony_ci
11262306a36Sopenharmony_ci		if (openat2_supported)
11362306a36Sopenharmony_ci			fd = sys_openat2(afd, victim_path, &how);
11462306a36Sopenharmony_ci		else
11562306a36Sopenharmony_ci			fd = sys_openat(afd, victim_path, &how);
11662306a36Sopenharmony_ci
11762306a36Sopenharmony_ci		if (fd < 0) {
11862306a36Sopenharmony_ci			if (fd == -EAGAIN)
11962306a36Sopenharmony_ci				eagains++;
12062306a36Sopenharmony_ci			else if (fd == -EXDEV)
12162306a36Sopenharmony_ci				exdevs++;
12262306a36Sopenharmony_ci			else if (fd == -ENOENT)
12362306a36Sopenharmony_ci				escapes++; /* escaped outside and got ENOENT... */
12462306a36Sopenharmony_ci			else
12562306a36Sopenharmony_ci				other_errs++; /* unexpected error */
12662306a36Sopenharmony_ci		} else {
12762306a36Sopenharmony_ci			if (fdequal(fd, afd, NULL))
12862306a36Sopenharmony_ci				successes++;
12962306a36Sopenharmony_ci			else
13062306a36Sopenharmony_ci				escapes++; /* we got an unexpected fd */
13162306a36Sopenharmony_ci		}
13262306a36Sopenharmony_ci		close(fd);
13362306a36Sopenharmony_ci	}
13462306a36Sopenharmony_ci
13562306a36Sopenharmony_ci	if (escapes > 0)
13662306a36Sopenharmony_ci		resultfn = ksft_test_result_fail;
13762306a36Sopenharmony_ci	ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d\n",
13862306a36Sopenharmony_ci		       eagains, exdevs, other_errs, successes);
13962306a36Sopenharmony_ci	resultfn("rename attack with %s (%d runs, got %d escapes)\n",
14062306a36Sopenharmony_ci		 flagname(resolve), ROUNDS, escapes);
14162306a36Sopenharmony_ci
14262306a36Sopenharmony_ci	/* Should be killed anyway, but might as well make sure. */
14362306a36Sopenharmony_ci	E_kill(child, SIGKILL);
14462306a36Sopenharmony_ci}
14562306a36Sopenharmony_ci
14662306a36Sopenharmony_ci#define NUM_TESTS NUM_RENAME_TESTS
14762306a36Sopenharmony_ci
14862306a36Sopenharmony_ciint main(int argc, char **argv)
14962306a36Sopenharmony_ci{
15062306a36Sopenharmony_ci	ksft_print_header();
15162306a36Sopenharmony_ci	ksft_set_plan(NUM_TESTS);
15262306a36Sopenharmony_ci
15362306a36Sopenharmony_ci	test_rename_attack(RESOLVE_BENEATH);
15462306a36Sopenharmony_ci	test_rename_attack(RESOLVE_IN_ROOT);
15562306a36Sopenharmony_ci
15662306a36Sopenharmony_ci	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
15762306a36Sopenharmony_ci		ksft_exit_fail();
15862306a36Sopenharmony_ci	else
15962306a36Sopenharmony_ci		ksft_exit_pass();
16062306a36Sopenharmony_ci}
161