linux/tools/testing/selftests/openat2/rename_attack_test.c

// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Author: Aleksa Sarai <[email protected]>
 * 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 "../kselftest.h"
#include "helpers.h"

/* Construct a test directory with the following structure:
 *
 * root/
 * |-- a/
 * |   `-- c/
 * `-- b/
 */
int setup_testdir(void)
{
	int dfd;
	char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";

	/* Make the top-level directory. */
	if (!mkdtemp(dirname))
		ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
	dfd = open(dirname, O_PATH | O_DIRECTORY);
	if (dfd < 0)
		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");

	E_mkdirat(dfd, "a", 0755);
	E_mkdirat(dfd, "b", 0755);
	E_mkdirat(dfd, "a/c", 0755);

	return dfd;
}

/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
pid_t spawn_attack(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. */
	E_prctl(PR_SET_PDEATHSIG, SIGKILL);

	/* Swap @a and @b. */
	for (;;)
		renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
	exit(1);
}

#define NUM_RENAME_TESTS 2
#define ROUNDS 400000

const char *flagname(int resolve)
{
	switch (resolve) {
	case RESOLVE_IN_ROOT:
		return "RESOLVE_IN_ROOT";
	case RESOLVE_BENEATH:
		return "RESOLVE_BENEATH";
	}
	return "(unknown)";
}

void test_rename_attack(int resolve)
{
	int dfd, afd;
	pid_t child;
	void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
	int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0;

	struct open_how how = {
		.flags = O_PATH,
		.resolve = resolve,
	};

	if (!openat2_supported) {
		how.resolve = 0;
		ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n");
	}

	dfd = setup_testdir();
	afd = openat(dfd, "a", O_PATH);
	if (afd < 0)
		ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n");

	child = spawn_attack(dfd, "a/c", "b");

	for (int i = 0; i < ROUNDS; i++) {
		int fd;
		char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";

		if (openat2_supported)
			fd = sys_openat2(afd, victim_path, &how);
		else
			fd = sys_openat(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(fd, afd, NULL))
				successes++;
			else
				escapes++; /* we got an unexpected fd */
		}
		close(fd);
	}

	if (escapes > 0)
		resultfn = ksft_test_result_fail;
	ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d\n",
		       eagains, exdevs, other_errs, successes);
	resultfn("rename attack with %s (%d runs, got %d escapes)\n",
		 flagname(resolve), ROUNDS, escapes);

	/* Should be killed anyway, but might as well make sure. */
	E_kill(child, SIGKILL);
}

#define NUM_TESTS NUM_RENAME_TESTS

int main(int argc, char **argv)
{
	ksft_print_header();
	ksft_set_plan(NUM_TESTS);

	test_rename_attack(RESOLVE_BENEATH);
	test_rename_attack(RESOLVE_IN_ROOT);

	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
		ksft_exit_fail();
	else
		ksft_exit_pass();
}