1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Author: Aleksa Sarai <cyphar@cyphar.com>
4 * Copyright (C) 2018-2019 SUSE LLC.
5 */
6
7#define _GNU_SOURCE
8#include <errno.h>
9#include <fcntl.h>
10#include <sched.h>
11#include <sys/stat.h>
12#include <sys/types.h>
13#include <sys/mount.h>
14#include <sys/mman.h>
15#include <sys/prctl.h>
16#include <signal.h>
17#include <stdio.h>
18#include <stdlib.h>
19#include <stdbool.h>
20#include <string.h>
21#include <syscall.h>
22#include <limits.h>
23#include <unistd.h>
24
25#include "../kselftest.h"
26#include "helpers.h"
27
28/* Construct a test directory with the following structure:
29 *
30 * root/
31 * |-- a/
32 * |   `-- c/
33 * `-- b/
34 */
35int setup_testdir(void)
36{
37	int dfd;
38	char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
39
40	/* Make the top-level directory. */
41	if (!mkdtemp(dirname))
42		ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
43	dfd = open(dirname, O_PATH | O_DIRECTORY);
44	if (dfd < 0)
45		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
46
47	E_mkdirat(dfd, "a", 0755);
48	E_mkdirat(dfd, "b", 0755);
49	E_mkdirat(dfd, "a/c", 0755);
50
51	return dfd;
52}
53
54/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
55pid_t spawn_attack(int dirfd, char *a, char *b)
56{
57	pid_t child = fork();
58	if (child != 0)
59		return child;
60
61	/* If the parent (the test process) dies, kill ourselves too. */
62	E_prctl(PR_SET_PDEATHSIG, SIGKILL);
63
64	/* Swap @a and @b. */
65	for (;;)
66		renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
67	exit(1);
68}
69
70#define NUM_RENAME_TESTS 2
71#define ROUNDS 400000
72
73const char *flagname(int resolve)
74{
75	switch (resolve) {
76	case RESOLVE_IN_ROOT:
77		return "RESOLVE_IN_ROOT";
78	case RESOLVE_BENEATH:
79		return "RESOLVE_BENEATH";
80	}
81	return "(unknown)";
82}
83
84void test_rename_attack(int resolve)
85{
86	int dfd, afd;
87	pid_t child;
88	void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
89	int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0;
90
91	struct open_how how = {
92		.flags = O_PATH,
93		.resolve = resolve,
94	};
95
96	if (!openat2_supported) {
97		how.resolve = 0;
98		ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n");
99	}
100
101	dfd = setup_testdir();
102	afd = openat(dfd, "a", O_PATH);
103	if (afd < 0)
104		ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n");
105
106	child = spawn_attack(dfd, "a/c", "b");
107
108	for (int i = 0; i < ROUNDS; i++) {
109		int fd;
110		char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
111
112		if (openat2_supported)
113			fd = sys_openat2(afd, victim_path, &how);
114		else
115			fd = sys_openat(afd, victim_path, &how);
116
117		if (fd < 0) {
118			if (fd == -EAGAIN)
119				eagains++;
120			else if (fd == -EXDEV)
121				exdevs++;
122			else if (fd == -ENOENT)
123				escapes++; /* escaped outside and got ENOENT... */
124			else
125				other_errs++; /* unexpected error */
126		} else {
127			if (fdequal(fd, afd, NULL))
128				successes++;
129			else
130				escapes++; /* we got an unexpected fd */
131		}
132		close(fd);
133	}
134
135	if (escapes > 0)
136		resultfn = ksft_test_result_fail;
137	ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d\n",
138		       eagains, exdevs, other_errs, successes);
139	resultfn("rename attack with %s (%d runs, got %d escapes)\n",
140		 flagname(resolve), ROUNDS, escapes);
141
142	/* Should be killed anyway, but might as well make sure. */
143	E_kill(child, SIGKILL);
144}
145
146#define NUM_TESTS NUM_RENAME_TESTS
147
148int main(int argc, char **argv)
149{
150	ksft_print_header();
151	ksft_set_plan(NUM_TESTS);
152
153	test_rename_attack(RESOLVE_BENEATH);
154	test_rename_attack(RESOLVE_IN_ROOT);
155
156	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
157		ksft_exit_fail();
158	else
159		ksft_exit_pass();
160}
161