DragonFlyBSD Kernel Audit
DF-1375 / harness.c
← back to finding ↓ download raw
/*
 * DF-1375 harness: OOB heap write via unsigned slot underflow in
 * _mapping_add_new_device (mps_mapping.c).
 *
 * This is the MPS driver's twin of DF-1283 (the mpr driver's identical bug).
 * OBJECT-LEVEL proof of the primitive: the mps driver cannot attach on the
 * audit QEMU guest (no LSI/Avago SAS HBA present), so the vulnerable path is
 * not runtime-reachable here. This harness replicates the exact kernel struct
 * (dev_mapping_table, enc_mapping_table) and the exact unguarded arithmetic
 * from _mapping_add_new_device with a firmware-controlled Slot < start_slot
 * to PROVE the OOB write primitive is real.
 *
 * Cited path: sys/dev/raid/mps/mps_mapping.c:1165
 *               map_idx = et_entry->start_index + phy_change->slot -
 *                         et_entry->start_slot;      // u32, slot<start_slot -> wrap
 *             sys/dev/raid/mps/mps_mapping.c:1167
 *               mt_entry = &sc->mapping_table[map_idx];   // OOB
 *             sys/dev/raid/mps/mps_mapping.c:992-998
 *               reservation loop trusts firmware num_slots without bounds check
 *
 * phy_change->slot comes from le16toh(sas_device_pg0.Slot) (firmware). When
 * slot < start_slot, the u32 subtraction wraps to ~0 and map_idx becomes a
 * huge value -> mapping_table[map_idx] is a massive OOB write.
 *
 * Build: cc -O2 -o harness harness.c
 * Run:   ./harness
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

struct dev_mapping_table {
    uint64_t physical_id;
    uint32_t device_info;
    uint32_t phy_bits;
    uint16_t dpm_entry_num;
    uint16_t dev_handle;
    uint16_t reserved1;
    uint16_t id;
    uint8_t  missing_count;
    uint8_t  init_complete;
    uint8_t  TLR_bits;
    uint8_t  reserved2;
};

/* Mirror enc_mapping_table (the fields used by the buggy arithmetic). */
struct enc_mapping_table {
    uint64_t enclosure_id;
    uint64_t phys_id;
    uint32_t device_info;
    uint32_t phy_bits;
    uint16_t num_slots;
    uint16_t slot;
    uint16_t enclosure_id_short;
    uint16_t start_index;
    uint16_t start_slot;
    uint16_t dpm_entry_num;
    uint8_t  missing_count;
    uint8_t  init_complete;
    uint16_t reserved;
};

#define MAX_DEVICES   264u

int main(void)
{
    /* We allocate one big table so we can show the wrapping arithmetic in a
     * controlled way. A real underflow (slot=0, start_slot=2) yields a wrapped
     * map_idx of ~0xFFFFFFFE which is unrepresentable as an in-process index;
     * instead we demonstrate (a) the exact buggy expression and the wrap, and
     * (b) a "small underflow" (start_index=10, slot=0, start_slot=4 => map_idx
     * =6 which IS in-range but WRONG, plus a negative-wrap case showing the
     * OOB pointer lands before the table). */
    struct dev_mapping_table *table = calloc(MAX_DEVICES + 8, sizeof(*table));
    if (!table) { perror("calloc"); return 1; }

    struct enc_mapping_table et;
    memset(&et, 0, sizeof(et));
    et.start_index = 10;
    et.start_slot  = 4;

    printf("[DF-1375] mapping_table max_devices=%u; start_index=%u start_slot=%u\n",
           MAX_DEVICES, et.start_index, et.start_slot);

    /* Case A: slot < start_slot with start_index large enough that the u32
     * arithmetic wraps to a huge value (the real bug). */
    uint16_t slot_a = 0;     /* firmware sas_device_pg0.Slot */
    uint32_t map_idx = (uint32_t)et.start_index + (uint32_t)slot_a -
                       (uint32_t)et.start_slot;   /* mps_mapping.c:1165 */
    printf("[DF-1375] Case A: slot=%u < start_slot=%u -> map_idx = %u (0x%08x)\n",
           slot_a, et.start_slot, map_idx, map_idx);

    /* If start_index is 0 and slot<start_slot, the wrap is catastrophic: */
    et.start_index = 0;
    et.start_slot  = 2;
    slot_a = 0;
    map_idx = (uint32_t)et.start_index + (uint32_t)slot_a - (uint32_t)et.start_slot;
    printf("[DF-1375] Case A2: start_index=0, slot=0, start_slot=2 -> "
           "map_idx = %u (0x%08x) -- wraps to ~0xFFFFFFFE -> &mapping_table[~0xFFFFFFFE]\n",
           map_idx, map_idx);

    if ((int32_t)((int32_t)et.start_index + (int16_t)slot_a - (int16_t)et.start_slot) < 0 ||
        map_idx >= MAX_DEVICES) {
        printf("[DF-1375] BUG CONFIRMED: slot < start_slot makes map_idx wrap/underflow "
               "-> mapping_table[%u] is a %s OOB write\n",
               map_idx, map_idx >= MAX_DEVICES ? "massive" : "before-table");
    }

    /* Case B: demonstrate the in-table WRONG write (slot<start_slot but
     * start_index keeps it in-range): the wrong entry gets overwritten. */
    et.start_index = 10;
    et.start_slot  = 4;
    slot_a = 1;   /* < start_slot */
    map_idx = (uint32_t)et.start_index + (uint32_t)slot_a - (uint32_t)et.start_slot;
    printf("[DF-1375] Case B: slot=%u -> map_idx=%u (expected >= start_index=%u); "
           "writes attacker physical_id/device_info into WRONG entry\n",
           slot_a, map_idx, et.start_index);
    if (map_idx < et.start_index) {
        struct dev_mapping_table *mt = &table[map_idx];   /* :1167 */
        mt->physical_id = 0x4141414141414141ULL;
        mt->device_info = 0x43434343;
        printf("[DF-1375] wrote attacker data into mapping_table[%u] "
               "(below the enclosure's start_index=%u) -> confirmed wrong-target "
               "corruption\n", map_idx, et.start_index);
    }

    /* The num_slots reservation loop (mps_mapping.c:992-998) has the same
     * class of issue: trusts firmware num_slots without an upper bound. */
    uint16_t num_slots_fw = 600;   /* firmware NumSlots, > max_devices */
    uint32_t base = 250;
    uint32_t end = base + num_slots_fw;
    printf("[DF-1375] reservation loop: base=%u + num_slots(fw)=%u = %u %s "
           "max_devices=%u -> %s\n",
           base, num_slots_fw, end,
           end > MAX_DEVICES ? ">" : "<=",
           MAX_DEVICES,
           end > MAX_DEVICES ? "OOB write past mapping_table" : "ok");

    printf("[DF-1375] FIX: validate slot >= start_slot AND map_idx < max_devices; "
           "validate map_idx + num_slots <= max_devices before the loops\n");

    free(table);
    return 0;
}