1 // SPDX-License-Identifier: GPL-2.0
2 /*
3 * Landlock tests - Ptrace
4 *
5 * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
6 * Copyright © 2019-2020 ANSSI
7 */
8
9 #define _GNU_SOURCE
10 #include <errno.h>
11 #include <fcntl.h>
12 #include <linux/landlock.h>
13 #include <signal.h>
14 #include <sys/prctl.h>
15 #include <sys/ptrace.h>
16 #include <sys/types.h>
17 #include <sys/wait.h>
18 #include <unistd.h>
19
20 #include "common.h"
21
create_domain(struct __test_metadata * const _metadata)22 static void create_domain(struct __test_metadata *const _metadata)
23 {
24 int ruleset_fd;
25 struct landlock_ruleset_attr ruleset_attr = {
26 .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK,
27 };
28
29 ruleset_fd = landlock_create_ruleset(&ruleset_attr,
30 sizeof(ruleset_attr), 0);
31 EXPECT_LE(0, ruleset_fd) {
32 TH_LOG("Failed to create a ruleset: %s", strerror(errno));
33 }
34 EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
35 EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
36 EXPECT_EQ(0, close(ruleset_fd));
37 }
38
test_ptrace_read(const pid_t pid)39 static int test_ptrace_read(const pid_t pid)
40 {
41 static const char path_template[] = "/proc/%d/environ";
42 char procenv_path[sizeof(path_template) + 10];
43 int procenv_path_size, fd;
44
45 procenv_path_size = snprintf(procenv_path, sizeof(procenv_path),
46 path_template, pid);
47 if (procenv_path_size >= sizeof(procenv_path))
48 return E2BIG;
49
50 fd = open(procenv_path, O_RDONLY | O_CLOEXEC);
51 if (fd < 0)
52 return errno;
53 /*
54 * Mixing error codes from close(2) and open(2) should not lead to any
55 * (access type) confusion for this test.
56 */
57 if (close(fd) != 0)
58 return errno;
59 return 0;
60 }
61
FIXTURE(hierarchy)62 FIXTURE(hierarchy) { };
63
FIXTURE_VARIANT(hierarchy)64 FIXTURE_VARIANT(hierarchy) {
65 const bool domain_both;
66 const bool domain_parent;
67 const bool domain_child;
68 };
69
70 /*
71 * Test multiple tracing combinations between a parent process P1 and a child
72 * process P2.
73 *
74 * Yama's scoped ptrace is presumed disabled. If enabled, this optional
75 * restriction is enforced in addition to any Landlock check, which means that
76 * all P2 requests to trace P1 would be denied.
77 */
78
79 /*
80 * No domain
81 *
82 * P1-. P1 -> P2 : allow
83 * \ P2 -> P1 : allow
84 * 'P2
85 */
FIXTURE_VARIANT_ADD(hierarchy,allow_without_domain)86 FIXTURE_VARIANT_ADD(hierarchy, allow_without_domain) {
87 .domain_both = false,
88 .domain_parent = false,
89 .domain_child = false,
90 };
91
92 /*
93 * Child domain
94 *
95 * P1--. P1 -> P2 : allow
96 * \ P2 -> P1 : deny
97 * .'-----.
98 * | P2 |
99 * '------'
100 */
FIXTURE_VARIANT_ADD(hierarchy,allow_with_one_domain)101 FIXTURE_VARIANT_ADD(hierarchy, allow_with_one_domain) {
102 .domain_both = false,
103 .domain_parent = false,
104 .domain_child = true,
105 };
106
107 /*
108 * Parent domain
109 * .------.
110 * | P1 --. P1 -> P2 : deny
111 * '------' \ P2 -> P1 : allow
112 * '
113 * P2
114 */
FIXTURE_VARIANT_ADD(hierarchy,deny_with_parent_domain)115 FIXTURE_VARIANT_ADD(hierarchy, deny_with_parent_domain) {
116 .domain_both = false,
117 .domain_parent = true,
118 .domain_child = false,
119 };
120
121 /*
122 * Parent + child domain (siblings)
123 * .------.
124 * | P1 ---. P1 -> P2 : deny
125 * '------' \ P2 -> P1 : deny
126 * .---'--.
127 * | P2 |
128 * '------'
129 */
FIXTURE_VARIANT_ADD(hierarchy,deny_with_sibling_domain)130 FIXTURE_VARIANT_ADD(hierarchy, deny_with_sibling_domain) {
131 .domain_both = false,
132 .domain_parent = true,
133 .domain_child = true,
134 };
135
136 /*
137 * Same domain (inherited)
138 * .-------------.
139 * | P1----. | P1 -> P2 : allow
140 * | \ | P2 -> P1 : allow
141 * | ' |
142 * | P2 |
143 * '-------------'
144 */
FIXTURE_VARIANT_ADD(hierarchy,allow_sibling_domain)145 FIXTURE_VARIANT_ADD(hierarchy, allow_sibling_domain) {
146 .domain_both = true,
147 .domain_parent = false,
148 .domain_child = false,
149 };
150
151 /*
152 * Inherited + child domain
153 * .-----------------.
154 * | P1----. | P1 -> P2 : allow
155 * | \ | P2 -> P1 : deny
156 * | .-'----. |
157 * | | P2 | |
158 * | '------' |
159 * '-----------------'
160 */
FIXTURE_VARIANT_ADD(hierarchy,allow_with_nested_domain)161 FIXTURE_VARIANT_ADD(hierarchy, allow_with_nested_domain) {
162 .domain_both = true,
163 .domain_parent = false,
164 .domain_child = true,
165 };
166
167 /*
168 * Inherited + parent domain
169 * .-----------------.
170 * |.------. | P1 -> P2 : deny
171 * || P1 ----. | P2 -> P1 : allow
172 * |'------' \ |
173 * | ' |
174 * | P2 |
175 * '-----------------'
176 */
FIXTURE_VARIANT_ADD(hierarchy,deny_with_nested_and_parent_domain)177 FIXTURE_VARIANT_ADD(hierarchy, deny_with_nested_and_parent_domain) {
178 .domain_both = true,
179 .domain_parent = true,
180 .domain_child = false,
181 };
182
183 /*
184 * Inherited + parent and child domain (siblings)
185 * .-----------------.
186 * | .------. | P1 -> P2 : deny
187 * | | P1 . | P2 -> P1 : deny
188 * | '------'\ |
189 * | \ |
190 * | .--'---. |
191 * | | P2 | |
192 * | '------' |
193 * '-----------------'
194 */
FIXTURE_VARIANT_ADD(hierarchy,deny_with_forked_domain)195 FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) {
196 .domain_both = true,
197 .domain_parent = true,
198 .domain_child = true,
199 };
200
FIXTURE_SETUP(hierarchy)201 FIXTURE_SETUP(hierarchy)
202 { }
203
FIXTURE_TEARDOWN(hierarchy)204 FIXTURE_TEARDOWN(hierarchy)
205 { }
206
207 /* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
TEST_F(hierarchy,trace)208 TEST_F(hierarchy, trace)
209 {
210 pid_t child, parent;
211 int status, err_proc_read;
212 int pipe_child[2], pipe_parent[2];
213 char buf_parent;
214 long ret;
215
216 /*
217 * Removes all effective and permitted capabilities to not interfere
218 * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
219 */
220 drop_caps(_metadata);
221
222 parent = getpid();
223 ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
224 ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
225 if (variant->domain_both) {
226 create_domain(_metadata);
227 if (!_metadata->passed)
228 /* Aborts before forking. */
229 return;
230 }
231
232 child = fork();
233 ASSERT_LE(0, child);
234 if (child == 0) {
235 char buf_child;
236
237 ASSERT_EQ(0, close(pipe_parent[1]));
238 ASSERT_EQ(0, close(pipe_child[0]));
239 if (variant->domain_child)
240 create_domain(_metadata);
241
242 /* Waits for the parent to be in a domain, if any. */
243 ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
244
245 /* Tests PTRACE_ATTACH and PTRACE_MODE_READ on the parent. */
246 err_proc_read = test_ptrace_read(parent);
247 ret = ptrace(PTRACE_ATTACH, parent, NULL, 0);
248 if (variant->domain_child) {
249 EXPECT_EQ(-1, ret);
250 EXPECT_EQ(EPERM, errno);
251 EXPECT_EQ(EACCES, err_proc_read);
252 } else {
253 EXPECT_EQ(0, ret);
254 EXPECT_EQ(0, err_proc_read);
255 }
256 if (ret == 0) {
257 ASSERT_EQ(parent, waitpid(parent, &status, 0));
258 ASSERT_EQ(1, WIFSTOPPED(status));
259 ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0));
260 }
261
262 /* Tests child PTRACE_TRACEME. */
263 ret = ptrace(PTRACE_TRACEME);
264 if (variant->domain_parent) {
265 EXPECT_EQ(-1, ret);
266 EXPECT_EQ(EPERM, errno);
267 } else {
268 EXPECT_EQ(0, ret);
269 }
270
271 /*
272 * Signals that the PTRACE_ATTACH test is done and the
273 * PTRACE_TRACEME test is ongoing.
274 */
275 ASSERT_EQ(1, write(pipe_child[1], ".", 1));
276
277 if (!variant->domain_parent) {
278 ASSERT_EQ(0, raise(SIGSTOP));
279 }
280
281 /* Waits for the parent PTRACE_ATTACH test. */
282 ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
283 _exit(_metadata->passed ? EXIT_SUCCESS : EXIT_FAILURE);
284 return;
285 }
286
287 ASSERT_EQ(0, close(pipe_child[1]));
288 ASSERT_EQ(0, close(pipe_parent[0]));
289 if (variant->domain_parent)
290 create_domain(_metadata);
291
292 /* Signals that the parent is in a domain, if any. */
293 ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
294
295 /*
296 * Waits for the child to test PTRACE_ATTACH on the parent and start
297 * testing PTRACE_TRACEME.
298 */
299 ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
300
301 /* Tests child PTRACE_TRACEME. */
302 if (!variant->domain_parent) {
303 ASSERT_EQ(child, waitpid(child, &status, 0));
304 ASSERT_EQ(1, WIFSTOPPED(status));
305 ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
306 } else {
307 /* The child should not be traced by the parent. */
308 EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
309 EXPECT_EQ(ESRCH, errno);
310 }
311
312 /* Tests PTRACE_ATTACH and PTRACE_MODE_READ on the child. */
313 err_proc_read = test_ptrace_read(child);
314 ret = ptrace(PTRACE_ATTACH, child, NULL, 0);
315 if (variant->domain_parent) {
316 EXPECT_EQ(-1, ret);
317 EXPECT_EQ(EPERM, errno);
318 EXPECT_EQ(EACCES, err_proc_read);
319 } else {
320 EXPECT_EQ(0, ret);
321 EXPECT_EQ(0, err_proc_read);
322 }
323 if (ret == 0) {
324 ASSERT_EQ(child, waitpid(child, &status, 0));
325 ASSERT_EQ(1, WIFSTOPPED(status));
326 ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
327 }
328
329 /* Signals that the parent PTRACE_ATTACH test is done. */
330 ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
331 ASSERT_EQ(child, waitpid(child, &status, 0));
332 if (WIFSIGNALED(status) || !WIFEXITED(status) ||
333 WEXITSTATUS(status) != EXIT_SUCCESS)
334 _metadata->passed = 0;
335 }
336
337 TEST_HARNESS_MAIN
338