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