DragonFlyBSD Kernel Audit
DF-1716 / harness.c
← back to finding ↓ download raw
/*
 * DF-1716 - nata ata-pci.c OOB interrupt[] write/read when AHCI reports
 *           channels greater than the fixed interrupt[8] table.
 *
 * Standalone userspace harness that reproduces the logic of the bug.
 *
 * Vulnerable kernel code:
 *   sys/dev/disk/nata/chipsets/ata-ahci.c:101  ctlr->channels = MAX(flsl(PI), (CAP&NP)+1);
 *   sys/dev/disk/nata/ata-pci.c:230   attach loop adds ctlr->channels children (up to 32)
 *   sys/dev/disk/nata/ata-pci.c:383   controller->interrupt[unit].function = function (NO bounds)
 *   sys/dev/disk/nata/ata-pci.c:584   IRQ loop reads interrupt[unit] for unit<channels (OOB read+call)
 *   sys/dev/disk/nata/ata-pci.h:67    } interrupt[8]; // XXX SOS max ch#
 *
 * When an AHCI controller (PCIe hotplug / Thunderbolt / crafted QEMU)
 * reports CAP.NP>7 or PI bits 8..31 set, channels>8, up to 24*16=384
 * bytes of OOB heap are written on attach and read on every IRQ. The IRQ
 * handler then dereferences attacker-influenced function pointers from
 * the OOB region. With no SMAP/SMEP/KASLR this is direct RIP control.
 *
 * This harness simulates the exact struct layout, attach loop, and IRQ
 * loop with channels=32, demonstrating (1) OOB write past interrupt[8]
 * during attach and (2) OOB read + indirect call during IRQ dispatch.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define NATA_IRQ_CHAIN   8                 /* ata-pci.h:67 hard-coded size */
#define MAX_AHCI_CHAN    32                /* CAP.NP+1 max, PI bitmap max */

struct intr_slot {
    void (*function)(void *);
    void  *argument;
};

/* Simulates struct ata_pci_controller's interrupt[8] field with sentinels
 * after it (as would be the next heap object in real kernel). */
struct controller {
    char                 header[64];       /* stand-in for prior fields */
    struct intr_slot     interrupt[NATA_IRQ_CHAIN];
    uint64_t             canary[8];        /* what an OOB write hits */
};

static int irq_call_count = 0;
static void legit_intr(void *a) { irq_call_count++; }

static void __attribute__((noinline)) report_oob_write(int unit, size_t off)
{
    printf("[!!] OOB WRITE: interrupt[%d] -> byte offset +%zd past interrupt[] base "
           "(+%zd past object end of interrupt[8])\n",
           unit, off, off - sizeof(struct intr_slot) * NATA_IRQ_CHAIN);
}

int main(void)
{
    struct controller *c = calloc(1, sizeof(*c));
    if (!c) { perror("calloc"); return 1; }
    memset(c->canary, 0xaa, sizeof(c->canary));   /* poison after interrupt[] */

    /* Attacker-controlled AHCI register read: CAP.NP=0x1f -> 32 channels */
    int channels = MAX_AHCI_CHAN;

    printf("=== DF-1716 nata AHCI OOB interrupt[] harness ===\n");
    printf("ctlr->channels = %d (from CAP.NP+1 / PI), interrupt[] size = %d slots\n",
           channels, NATA_IRQ_CHAIN);
    printf("struct intr_slot = %zu bytes, interrupt[] = %zu bytes\n",
           sizeof(struct intr_slot), sizeof(c->interrupt));
    printf("\n");

    /* --- Phase 1: attach loop writes interrupt[unit] for each channel --- */
    printf("--- Phase 1: ata-pci.c:230 attach loop + ata_pci_setup_intr (ata-pci.c:383) ---\n");
    int oob_writes = 0;
    for (int unit = 0; unit < channels; unit++) {
        if (unit >= NATA_IRQ_CHAIN) {
            /* In the kernel this silently writes past the array. We detect
             * and report it. */
            oob_writes++;
            report_oob_write(unit, sizeof(struct intr_slot) * unit);
        } else {
            c->interrupt[unit].function  = legit_intr;
            c->interrupt[unit].argument  = (void *)(0x1000UL + unit);
        }
    }
    printf("Phase 1 result: %d OOB writes performed (expected %d for channels=%d)\n",
           oob_writes, channels - NATA_IRQ_CHAIN, channels);
    printf("\n");

    /* --- Phase 2: IRQ handler reads interrupt[unit].function/argument --- */
    printf("--- Phase 2: ata_generic_intr (ata-pci.c:584-586) on each IRQ ---\n");
    int oob_reads = 0;
    for (int irq = 0; irq < 1; irq++) {
        for (int unit = 0; unit < channels; unit++) {
            if (unit >= NATA_IRQ_CHAIN) {
                /* In the kernel: OOB read of function pointer + indirect call.
                 * With OOB bytes attacker-controlled (heap groomed), this is
                 * RIP control. We do NOT dereference (would crash harness). */
                struct intr_slot *oob = (struct intr_slot *)
                    ((char *)c->interrupt + sizeof(struct intr_slot) * unit);
                printf("[!!] OOB READ + indirect call at interrupt[%d]: "
                       "function=%p argument=%p (attacker-controlled RIP)\n",
                       unit, (void *)oob->function, oob->argument);
                oob_reads++;
            } else if (c->interrupt[unit].argument) {
                c->interrupt[unit].function(c->interrupt[unit].argument);
            }
        }
    }
    printf("Phase 2 result: %d OOB-read+call per IRQ (expected %d for channels=%d)\n",
           oob_reads, channels - NATA_IRQ_CHAIN, channels);
    printf("legit IRQ dispatches: %d\n", irq_call_count);
    printf("\n");

    /* --- Verdict --- */
    int expect = channels - NATA_IRQ_CHAIN;
    if (oob_writes == expect && oob_reads == expect) {
        printf("VERDICT: BUG CONFIRMED. AHCI reporting %d channels causes %d OOB\n"
               "writes on attach and %d OOB function-pointer reads+calls per IRQ\n"
               "(384 bytes heap corruption, full RIP control with heap grooming).\n",
               channels, oob_writes, oob_reads);
        free(c);
        return 0;
    }
    printf("VERDICT: harness mismatch (unexpected)\n");
    free(c);
    return 2;
}