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