Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/exec: Add 32 tests for AT_EXECVE_CHECK and exec securebits

Test that checks performed by execveat(..., AT_EXECVE_CHECK) are
consistent with noexec mount points and file execute permissions.

Test that SECBIT_EXEC_RESTRICT_FILE and SECBIT_EXEC_DENY_INTERACTIVE are
inherited by child processes and that they can be pinned with the
appropriate SECBIT_EXEC_RESTRICT_FILE_LOCKED and
SECBIT_EXEC_DENY_INTERACTIVE_LOCKED bits.

Cc: Al Viro <viro@zeniv.linux.org.uk>
Cc: Christian Brauner <brauner@kernel.org>
Cc: Kees Cook <keescook@chromium.org>
Cc: Paul Moore <paul@paul-moore.com>
Cc: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Link: https://lore.kernel.org/r/20241212174223.389435-4-mic@digikod.net
Signed-off-by: Kees Cook <kees@kernel.org>

authored by

Mickaël Salaün and committed by
Kees Cook
b083cc81 a0623b2a

+472
+2
tools/testing/selftests/exec/.gitignore
··· 9 9 execveat.denatured 10 10 non-regular 11 11 null-argv 12 + /check-exec 13 + /false 12 14 /load_address.* 13 15 !load_address.c 14 16 /recursion-depth
+7
tools/testing/selftests/exec/Makefile
··· 1 1 # SPDX-License-Identifier: GPL-2.0 2 2 CFLAGS = -Wall 3 3 CFLAGS += -Wno-nonnull 4 + CFLAGS += $(KHDR_INCLUDES) 5 + 6 + LDLIBS += -lcap 4 7 5 8 ALIGNS := 0x1000 0x200000 0x1000000 6 9 ALIGN_PIES := $(patsubst %,load_address.%,$(ALIGNS)) ··· 12 9 13 10 TEST_PROGS := binfmt_script.py 14 11 TEST_GEN_PROGS := execveat non-regular $(ALIGNMENT_TESTS) 12 + TEST_GEN_PROGS_EXTENDED := false 15 13 TEST_GEN_FILES := execveat.symlink execveat.denatured script subdir 16 14 # Makefile is a run-time dependency, since it's accessed by the execveat test 17 15 TEST_FILES := Makefile 18 16 19 17 TEST_GEN_PROGS += recursion-depth 20 18 TEST_GEN_PROGS += null-argv 19 + TEST_GEN_PROGS += check-exec 21 20 22 21 EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx* \ 23 22 $(OUTPUT)/S_I*.test ··· 43 38 $(OUTPUT)/load_address.static.0x%: load_address.c 44 39 $(CC) $(CFLAGS) $(LDFLAGS) -Wl,-z,max-page-size=$(lastword $(subst ., ,$@)) \ 45 40 -fPIE -static-pie $< -o $@ 41 + $(OUTPUT)/false: false.c 42 + $(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@
+456
tools/testing/selftests/exec/check-exec.c
··· 1 + // SPDX-License-Identifier: GPL-2.0 2 + /* 3 + * Test execveat(2) with AT_EXECVE_CHECK, and prctl(2) with 4 + * SECBIT_EXEC_RESTRICT_FILE, SECBIT_EXEC_DENY_INTERACTIVE, and their locked 5 + * counterparts. 6 + * 7 + * Copyright © 2018-2020 ANSSI 8 + * Copyright © 2024 Microsoft Corporation 9 + * 10 + * Author: Mickaël Salaün <mic@digikod.net> 11 + */ 12 + 13 + #include <asm-generic/unistd.h> 14 + #include <errno.h> 15 + #include <fcntl.h> 16 + #include <linux/prctl.h> 17 + #include <linux/securebits.h> 18 + #include <stdio.h> 19 + #include <stdlib.h> 20 + #include <sys/capability.h> 21 + #include <sys/mount.h> 22 + #include <sys/prctl.h> 23 + #include <sys/socket.h> 24 + #include <sys/stat.h> 25 + #include <sys/sysmacros.h> 26 + #include <unistd.h> 27 + 28 + /* Defines AT_EXECVE_CHECK without type conflicts. */ 29 + #define _ASM_GENERIC_FCNTL_H 30 + #include <linux/fcntl.h> 31 + 32 + #include "../kselftest_harness.h" 33 + 34 + static void drop_privileges(struct __test_metadata *const _metadata) 35 + { 36 + const unsigned int noroot = SECBIT_NOROOT | SECBIT_NOROOT_LOCKED; 37 + cap_t cap_p; 38 + 39 + if ((cap_get_secbits() & noroot) != noroot) 40 + EXPECT_EQ(0, cap_set_secbits(noroot)); 41 + 42 + cap_p = cap_get_proc(); 43 + EXPECT_NE(NULL, cap_p); 44 + EXPECT_NE(-1, cap_clear(cap_p)); 45 + 46 + /* 47 + * Drops everything, especially CAP_SETPCAP, CAP_DAC_OVERRIDE, and 48 + * CAP_DAC_READ_SEARCH. 49 + */ 50 + EXPECT_NE(-1, cap_set_proc(cap_p)); 51 + EXPECT_NE(-1, cap_free(cap_p)); 52 + } 53 + 54 + static int test_secbits_set(const unsigned int secbits) 55 + { 56 + int err; 57 + 58 + err = prctl(PR_SET_SECUREBITS, secbits); 59 + if (err) 60 + return errno; 61 + return 0; 62 + } 63 + 64 + FIXTURE(access) 65 + { 66 + int memfd, pipefd; 67 + int pipe_fds[2], socket_fds[2]; 68 + }; 69 + 70 + FIXTURE_VARIANT(access) 71 + { 72 + const bool mount_exec; 73 + const bool file_exec; 74 + }; 75 + 76 + /* clang-format off */ 77 + FIXTURE_VARIANT_ADD(access, mount_exec_file_exec) { 78 + /* clang-format on */ 79 + .mount_exec = true, 80 + .file_exec = true, 81 + }; 82 + 83 + /* clang-format off */ 84 + FIXTURE_VARIANT_ADD(access, mount_exec_file_noexec) { 85 + /* clang-format on */ 86 + .mount_exec = true, 87 + .file_exec = false, 88 + }; 89 + 90 + /* clang-format off */ 91 + FIXTURE_VARIANT_ADD(access, mount_noexec_file_exec) { 92 + /* clang-format on */ 93 + .mount_exec = false, 94 + .file_exec = true, 95 + }; 96 + 97 + /* clang-format off */ 98 + FIXTURE_VARIANT_ADD(access, mount_noexec_file_noexec) { 99 + /* clang-format on */ 100 + .mount_exec = false, 101 + .file_exec = false, 102 + }; 103 + 104 + static const char binary_path[] = "./false"; 105 + static const char workdir_path[] = "./test-mount"; 106 + static const char reg_file_path[] = "./test-mount/regular_file"; 107 + static const char dir_path[] = "./test-mount/directory"; 108 + static const char block_dev_path[] = "./test-mount/block_device"; 109 + static const char char_dev_path[] = "./test-mount/character_device"; 110 + static const char fifo_path[] = "./test-mount/fifo"; 111 + 112 + FIXTURE_SETUP(access) 113 + { 114 + int procfd_path_size; 115 + static const char path_template[] = "/proc/self/fd/%d"; 116 + char procfd_path[sizeof(path_template) + 10]; 117 + 118 + /* Makes sure we are not already restricted nor locked. */ 119 + EXPECT_EQ(0, test_secbits_set(0)); 120 + 121 + /* 122 + * Cleans previous workspace if any error previously happened (don't 123 + * check errors). 124 + */ 125 + umount(workdir_path); 126 + rmdir(workdir_path); 127 + 128 + /* Creates a clean mount point. */ 129 + ASSERT_EQ(0, mkdir(workdir_path, 00700)); 130 + ASSERT_EQ(0, mount("test", workdir_path, "tmpfs", 131 + MS_MGC_VAL | (variant->mount_exec ? 0 : MS_NOEXEC), 132 + "mode=0700,size=9m")); 133 + 134 + /* Creates a regular file. */ 135 + ASSERT_EQ(0, mknod(reg_file_path, 136 + S_IFREG | (variant->file_exec ? 0700 : 0600), 0)); 137 + /* Creates a directory. */ 138 + ASSERT_EQ(0, mkdir(dir_path, variant->file_exec ? 0700 : 0600)); 139 + /* Creates a character device: /dev/null. */ 140 + ASSERT_EQ(0, mknod(char_dev_path, S_IFCHR | 0400, makedev(1, 3))); 141 + /* Creates a block device: /dev/loop0 */ 142 + ASSERT_EQ(0, mknod(block_dev_path, S_IFBLK | 0400, makedev(7, 0))); 143 + /* Creates a fifo. */ 144 + ASSERT_EQ(0, mknod(fifo_path, S_IFIFO | 0600, 0)); 145 + 146 + /* Creates a regular file without user mount point. */ 147 + self->memfd = memfd_create("test-exec-probe", MFD_CLOEXEC); 148 + ASSERT_LE(0, self->memfd); 149 + /* Sets mode, which must be ignored by the exec check. */ 150 + ASSERT_EQ(0, fchmod(self->memfd, variant->file_exec ? 0700 : 0600)); 151 + 152 + /* Creates a pipefs file descriptor. */ 153 + ASSERT_EQ(0, pipe(self->pipe_fds)); 154 + procfd_path_size = snprintf(procfd_path, sizeof(procfd_path), 155 + path_template, self->pipe_fds[0]); 156 + ASSERT_LT(procfd_path_size, sizeof(procfd_path)); 157 + self->pipefd = open(procfd_path, O_RDWR | O_CLOEXEC); 158 + ASSERT_LE(0, self->pipefd); 159 + ASSERT_EQ(0, fchmod(self->pipefd, variant->file_exec ? 0700 : 0600)); 160 + 161 + /* Creates a socket file descriptor. */ 162 + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, 163 + self->socket_fds)); 164 + } 165 + 166 + FIXTURE_TEARDOWN_PARENT(access) 167 + { 168 + /* There is no need to unlink the test files. */ 169 + EXPECT_EQ(0, umount(workdir_path)); 170 + EXPECT_EQ(0, rmdir(workdir_path)); 171 + } 172 + 173 + static void fill_exec_fd(struct __test_metadata *_metadata, const int fd_out) 174 + { 175 + char buf[1024]; 176 + size_t len; 177 + int fd_in; 178 + 179 + fd_in = open(binary_path, O_CLOEXEC | O_RDONLY); 180 + ASSERT_LE(0, fd_in); 181 + /* Cannot use copy_file_range(2) because of EXDEV. */ 182 + len = read(fd_in, buf, sizeof(buf)); 183 + EXPECT_LE(0, len); 184 + while (len > 0) { 185 + EXPECT_EQ(len, write(fd_out, buf, len)) 186 + { 187 + TH_LOG("Failed to write: %s (%d)", strerror(errno), 188 + errno); 189 + } 190 + len = read(fd_in, buf, sizeof(buf)); 191 + EXPECT_LE(0, len); 192 + } 193 + EXPECT_EQ(0, close(fd_in)); 194 + } 195 + 196 + static void fill_exec_path(struct __test_metadata *_metadata, 197 + const char *const path) 198 + { 199 + int fd_out; 200 + 201 + fd_out = open(path, O_CLOEXEC | O_WRONLY); 202 + ASSERT_LE(0, fd_out) 203 + { 204 + TH_LOG("Failed to open %s: %s", path, strerror(errno)); 205 + } 206 + fill_exec_fd(_metadata, fd_out); 207 + EXPECT_EQ(0, close(fd_out)); 208 + } 209 + 210 + static void test_exec_fd(struct __test_metadata *_metadata, const int fd, 211 + const int err_code) 212 + { 213 + char *const argv[] = { "", NULL }; 214 + int access_ret, access_errno; 215 + 216 + /* 217 + * If we really execute fd, filled with the "false" binary, the current 218 + * thread will exits with an error, which will be interpreted by the 219 + * test framework as an error. With AT_EXECVE_CHECK, we only check a 220 + * potential successful execution. 221 + */ 222 + access_ret = 223 + execveat(fd, "", argv, NULL, AT_EMPTY_PATH | AT_EXECVE_CHECK); 224 + access_errno = errno; 225 + if (err_code) { 226 + EXPECT_EQ(-1, access_ret); 227 + EXPECT_EQ(err_code, access_errno) 228 + { 229 + TH_LOG("Wrong error for execveat(2): %s (%d)", 230 + strerror(access_errno), errno); 231 + } 232 + } else { 233 + EXPECT_EQ(0, access_ret) 234 + { 235 + TH_LOG("Access denied: %s", strerror(access_errno)); 236 + } 237 + } 238 + } 239 + 240 + static void test_exec_path(struct __test_metadata *_metadata, 241 + const char *const path, const int err_code) 242 + { 243 + int flags = O_CLOEXEC; 244 + int fd; 245 + 246 + /* Do not block on pipes. */ 247 + if (path == fifo_path) 248 + flags |= O_NONBLOCK; 249 + 250 + fd = open(path, flags | O_RDONLY); 251 + ASSERT_LE(0, fd) 252 + { 253 + TH_LOG("Failed to open %s: %s", path, strerror(errno)); 254 + } 255 + test_exec_fd(_metadata, fd, err_code); 256 + EXPECT_EQ(0, close(fd)); 257 + } 258 + 259 + /* Tests that we don't get ENOEXEC. */ 260 + TEST_F(access, regular_file_empty) 261 + { 262 + const int exec = variant->mount_exec && variant->file_exec; 263 + 264 + test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES); 265 + 266 + drop_privileges(_metadata); 267 + test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES); 268 + } 269 + 270 + TEST_F(access, regular_file_elf) 271 + { 272 + const int exec = variant->mount_exec && variant->file_exec; 273 + 274 + fill_exec_path(_metadata, reg_file_path); 275 + 276 + test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES); 277 + 278 + drop_privileges(_metadata); 279 + test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES); 280 + } 281 + 282 + /* Tests that we don't get ENOEXEC. */ 283 + TEST_F(access, memfd_empty) 284 + { 285 + const int exec = variant->file_exec; 286 + 287 + test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES); 288 + 289 + drop_privileges(_metadata); 290 + test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES); 291 + } 292 + 293 + TEST_F(access, memfd_elf) 294 + { 295 + const int exec = variant->file_exec; 296 + 297 + fill_exec_fd(_metadata, self->memfd); 298 + 299 + test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES); 300 + 301 + drop_privileges(_metadata); 302 + test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES); 303 + } 304 + 305 + TEST_F(access, non_regular_files) 306 + { 307 + test_exec_path(_metadata, dir_path, EACCES); 308 + test_exec_path(_metadata, block_dev_path, EACCES); 309 + test_exec_path(_metadata, char_dev_path, EACCES); 310 + test_exec_path(_metadata, fifo_path, EACCES); 311 + test_exec_fd(_metadata, self->socket_fds[0], EACCES); 312 + test_exec_fd(_metadata, self->pipefd, EACCES); 313 + } 314 + 315 + /* clang-format off */ 316 + FIXTURE(secbits) {}; 317 + /* clang-format on */ 318 + 319 + FIXTURE_VARIANT(secbits) 320 + { 321 + const bool is_privileged; 322 + const int error; 323 + }; 324 + 325 + /* clang-format off */ 326 + FIXTURE_VARIANT_ADD(secbits, priv) { 327 + /* clang-format on */ 328 + .is_privileged = true, 329 + .error = 0, 330 + }; 331 + 332 + /* clang-format off */ 333 + FIXTURE_VARIANT_ADD(secbits, unpriv) { 334 + /* clang-format on */ 335 + .is_privileged = false, 336 + .error = EPERM, 337 + }; 338 + 339 + FIXTURE_SETUP(secbits) 340 + { 341 + /* Makes sure no exec bits are set. */ 342 + EXPECT_EQ(0, test_secbits_set(0)); 343 + EXPECT_EQ(0, prctl(PR_GET_SECUREBITS)); 344 + 345 + if (!variant->is_privileged) 346 + drop_privileges(_metadata); 347 + } 348 + 349 + FIXTURE_TEARDOWN(secbits) 350 + { 351 + } 352 + 353 + TEST_F(secbits, legacy) 354 + { 355 + EXPECT_EQ(variant->error, test_secbits_set(0)); 356 + } 357 + 358 + #define CHILD(...) \ 359 + do { \ 360 + pid_t child = vfork(); \ 361 + EXPECT_LE(0, child); \ 362 + if (child == 0) { \ 363 + __VA_ARGS__; \ 364 + _exit(0); \ 365 + } \ 366 + } while (0) 367 + 368 + TEST_F(secbits, exec) 369 + { 370 + unsigned int secbits = prctl(PR_GET_SECUREBITS); 371 + 372 + secbits |= SECBIT_EXEC_RESTRICT_FILE; 373 + EXPECT_EQ(0, test_secbits_set(secbits)); 374 + EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)); 375 + CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS))); 376 + 377 + secbits |= SECBIT_EXEC_DENY_INTERACTIVE; 378 + EXPECT_EQ(0, test_secbits_set(secbits)); 379 + EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)); 380 + CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS))); 381 + 382 + secbits &= ~(SECBIT_EXEC_RESTRICT_FILE | SECBIT_EXEC_DENY_INTERACTIVE); 383 + EXPECT_EQ(0, test_secbits_set(secbits)); 384 + EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)); 385 + CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS))); 386 + } 387 + 388 + TEST_F(secbits, check_locked_set) 389 + { 390 + unsigned int secbits = prctl(PR_GET_SECUREBITS); 391 + 392 + secbits |= SECBIT_EXEC_RESTRICT_FILE; 393 + EXPECT_EQ(0, test_secbits_set(secbits)); 394 + secbits |= SECBIT_EXEC_RESTRICT_FILE_LOCKED; 395 + EXPECT_EQ(0, test_secbits_set(secbits)); 396 + 397 + /* Checks lock set but unchanged. */ 398 + EXPECT_EQ(variant->error, test_secbits_set(secbits)); 399 + CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 400 + 401 + secbits &= ~SECBIT_EXEC_RESTRICT_FILE; 402 + EXPECT_EQ(EPERM, test_secbits_set(0)); 403 + CHILD(EXPECT_EQ(EPERM, test_secbits_set(0))); 404 + } 405 + 406 + TEST_F(secbits, check_locked_unset) 407 + { 408 + unsigned int secbits = prctl(PR_GET_SECUREBITS); 409 + 410 + secbits |= SECBIT_EXEC_RESTRICT_FILE_LOCKED; 411 + EXPECT_EQ(0, test_secbits_set(secbits)); 412 + 413 + /* Checks lock unset but unchanged. */ 414 + EXPECT_EQ(variant->error, test_secbits_set(secbits)); 415 + CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 416 + 417 + secbits &= ~SECBIT_EXEC_RESTRICT_FILE; 418 + EXPECT_EQ(EPERM, test_secbits_set(0)); 419 + CHILD(EXPECT_EQ(EPERM, test_secbits_set(0))); 420 + } 421 + 422 + TEST_F(secbits, restrict_locked_set) 423 + { 424 + unsigned int secbits = prctl(PR_GET_SECUREBITS); 425 + 426 + secbits |= SECBIT_EXEC_DENY_INTERACTIVE; 427 + EXPECT_EQ(0, test_secbits_set(secbits)); 428 + secbits |= SECBIT_EXEC_DENY_INTERACTIVE_LOCKED; 429 + EXPECT_EQ(0, test_secbits_set(secbits)); 430 + 431 + /* Checks lock set but unchanged. */ 432 + EXPECT_EQ(variant->error, test_secbits_set(secbits)); 433 + CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 434 + 435 + secbits &= ~SECBIT_EXEC_DENY_INTERACTIVE; 436 + EXPECT_EQ(EPERM, test_secbits_set(0)); 437 + CHILD(EXPECT_EQ(EPERM, test_secbits_set(0))); 438 + } 439 + 440 + TEST_F(secbits, restrict_locked_unset) 441 + { 442 + unsigned int secbits = prctl(PR_GET_SECUREBITS); 443 + 444 + secbits |= SECBIT_EXEC_DENY_INTERACTIVE_LOCKED; 445 + EXPECT_EQ(0, test_secbits_set(secbits)); 446 + 447 + /* Checks lock unset but unchanged. */ 448 + EXPECT_EQ(variant->error, test_secbits_set(secbits)); 449 + CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 450 + 451 + secbits &= ~SECBIT_EXEC_DENY_INTERACTIVE; 452 + EXPECT_EQ(EPERM, test_secbits_set(0)); 453 + CHILD(EXPECT_EQ(EPERM, test_secbits_set(0))); 454 + } 455 + 456 + TEST_HARNESS_MAIN
+2
tools/testing/selftests/exec/config
··· 1 + CONFIG_BLK_DEV=y 2 + CONFIG_BLK_DEV_LOOP=y
+5
tools/testing/selftests/exec/false.c
··· 1 + // SPDX-License-Identifier: GPL-2.0 2 + int main(void) 3 + { 4 + return 1; 5 + }