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