xref: /linux/tools/testing/selftests/filesystems/openat2/rename_attack_test.c (revision 056a5087d87ead77dedbe9cf5bde53b7cd4b4651)
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 "helpers.h"
26 #include "kselftest_harness.h"
27 
28 #define ROUNDS 400000
29 
30 /* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
31 pid_t spawn_attack(struct __test_metadata *_metadata,
32 		   int dirfd, char *a, char *b)
33 {
34 	pid_t child = fork();
35 	if (child != 0)
36 		return child;
37 
38 	/* If the parent (the test process) dies, kill ourselves too. */
39 	ASSERT_EQ(prctl(PR_SET_PDEATHSIG, SIGKILL), 0);
40 
41 	/* Swap @a and @b. */
42 	for (;;)
43 		renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
44 	exit(1);
45 }
46 
47 /*
48  * Construct a test directory with the following structure:
49  *
50  * root/
51  * |-- a/
52  * |   `-- c/
53  * `-- b/
54  */
55 FIXTURE(rename_attack) {
56 	int dfd;
57 	int afd;
58 	pid_t child;
59 };
60 
61 FIXTURE_SETUP(rename_attack)
62 {
63 	char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
64 
65 	self->dfd = -1;
66 	self->afd = -1;
67 	self->child = 0;
68 
69 	/* Make the top-level directory. */
70 	ASSERT_NE(mkdtemp(dirname), NULL);
71 	self->dfd = open(dirname, O_PATH | O_DIRECTORY);
72 	ASSERT_GE(self->dfd, 0);
73 
74 	ASSERT_EQ(mkdirat(self->dfd, "a", 0755), 0);
75 	ASSERT_EQ(mkdirat(self->dfd, "b", 0755), 0);
76 	ASSERT_EQ(mkdirat(self->dfd, "a/c", 0755), 0);
77 
78 	self->afd = openat(self->dfd, "a", O_PATH);
79 	ASSERT_GE(self->afd, 0);
80 
81 	self->child = spawn_attack(_metadata, self->dfd, "a/c", "b");
82 	ASSERT_GT(self->child, 0);
83 }
84 
85 FIXTURE_TEARDOWN(rename_attack)
86 {
87 	if (self->child > 0)
88 		kill(self->child, SIGKILL);
89 	if (self->afd >= 0)
90 		close(self->afd);
91 	if (self->dfd >= 0)
92 		close(self->dfd);
93 }
94 
95 FIXTURE_VARIANT(rename_attack) {
96 	int resolve;
97 	const char *name;
98 };
99 
100 FIXTURE_VARIANT_ADD(rename_attack, resolve_beneath) {
101 	.resolve = RESOLVE_BENEATH,
102 	.name = "RESOLVE_BENEATH",
103 };
104 
105 FIXTURE_VARIANT_ADD(rename_attack, resolve_in_root) {
106 	.resolve = RESOLVE_IN_ROOT,
107 	.name = "RESOLVE_IN_ROOT",
108 };
109 
110 TEST_F_TIMEOUT(rename_attack, test, 120)
111 {
112 	int escapes = 0, successes = 0, other_errs = 0, exdevs = 0, eagains = 0;
113 	char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
114 	struct open_how how = {
115 		.flags = O_PATH,
116 		.resolve = variant->resolve,
117 	};
118 
119 	if (!openat2_supported) {
120 		how.resolve = 0;
121 		TH_LOG("openat2(2) unsupported -- using openat(2) instead");
122 	}
123 
124 	for (int i = 0; i < ROUNDS; i++) {
125 		int fd;
126 
127 		if (openat2_supported)
128 			fd = sys_openat2(self->afd, victim_path, &how);
129 		else
130 			fd = sys_openat(self->afd, victim_path, &how);
131 
132 		if (fd < 0) {
133 			if (fd == -EAGAIN)
134 				eagains++;
135 			else if (fd == -EXDEV)
136 				exdevs++;
137 			else if (fd == -ENOENT)
138 				escapes++; /* escaped outside and got ENOENT... */
139 			else
140 				other_errs++; /* unexpected error */
141 		} else {
142 			if (fdequal(_metadata, fd, self->afd, NULL))
143 				successes++;
144 			else
145 				escapes++; /* we got an unexpected fd */
146 		}
147 		if (fd >= 0)
148 			close(fd);
149 	}
150 
151 	TH_LOG("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d",
152 	       eagains, exdevs, other_errs, successes);
153 	ASSERT_EQ(escapes, 0) {
154 		TH_LOG("rename attack with %s (%d runs, got %d escapes)",
155 		       variant->name, ROUNDS, escapes);
156 	}
157 }
158 
159 TEST_HARNESS_MAIN
160