DragonFlyBSD Kernel Audit
DF-0149 / tlv_truncation.c
← back to finding ↓ download raw
/*
 * DF-0149 - Signed-integer truncation in preload TLV walk size math.
 *
 * sys/kern/subr_module.c, the per-record advance used by every
 * preload_search_*() / preload_delete_name() / preload_bootstrap_relocate():
 *
 *   int  next;                                   // SIGNED int
 *   ...
 *   next = sizeof(u_int32_t) * 2 + hdr[1];       // hdr[1] is u_int32_t
 *   next = roundup(next, sizeof(u_long));        // roundup is division-based
 *   curp += next;                                // caddr_t += int
 *
 * roundup(x,y) = ((((x)+((y)-1))/(y))*(y))  (sys/sys/param.h:402) is
 * sign-preserving, so a negative `next` stays negative and `curp` walks
 * BACKWARD past preload_metadata, reading/writing memory that precedes the
 * metadata blob.  hdr[1]==0xFFFFFFF8 makes next==0 -> infinite loop / boot hang.
 *
 * This harness reproduces the exact C arithmetic for a range of crafted
 * hdr[1] values and prints whether next goes negative / zero / OOB.
 *
 * Boot-time only: preload_metadata is supplied by the boot loader from the
 * kernel + preloaded-module image.  Triggering requires a tampered/crafted
 * preloaded module or bootloader compromise (high privilege).  Not reachable
 * from unprivileged userspace at runtime.
 */
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

/* Exact kernel definitions. */
typedef uint32_t u_int32_t;
typedef unsigned long u_long;

#define roundup(x, y)   ((((x)+((y)-1))/(y))*(y))   /* sys/sys/param.h:402 */

#define MODINFO_END 0

static void walk_emulation(u_int32_t *fake_meta, size_t words, const char *label)
{
    unsigned char *curp = (unsigned char *)fake_meta;
    unsigned char *base = curp;
    unsigned char *blob_end = base + words * sizeof(u_int32_t);
    int step = 0;

    printf("=== %s (metadata @ %p, %zu words) ===\n", label, (void *)base, words);
    for (;;) {
        u_int32_t *hdr = (u_int32_t *)curp;
        int next;

        /* Bounds-checked read: never dereference outside the blob (the real
         * kernel does NOT do this check -- that is precisely the bug). */
        if (curp < base || curp + 2*sizeof(u_int32_t) > blob_end) {
            long off = (long)(curp - base);
            printf("  step %d: WOULD READ at offset %+ld -- %s the %zu-byte blob\n",
                   step, off,
                   off < 0 ? "*** BEFORE ***" : "*** PAST ***",
                   words * sizeof(u_int32_t));
            if (off < 0)
                printf("    => kernel reads memory that PRECEES preload_metadata "
                       "(OOB read), or loops here forever (next==0)\n");
            break;
        }

        if (hdr[0] == MODINFO_END && hdr[1] == MODINFO_END) {
            printf("  step %d: terminator reached normally at offset %+ld\n",
                   step, (long)(curp - base));
            break;
        }

        next = sizeof(u_int32_t) * 2 + hdr[1];
        next = roundup(next, sizeof(u_long));

        printf("  step %d: type=0x%08x len(hdr[1])=0x%08x -> next(int)=%d (0x%x) "
               "[offset %+ld]\n",
               step, hdr[0], hdr[1], next, (unsigned)next, (long)(curp - base));

        if (next <= 0) {
            if (next == 0)
                printf("    !!! next==0 => curp never advances => INFINITE LOOP "
                       "(boot hang)\n");
            else
                printf("    !!! next<0 => curp walks BACKWARD past preload_metadata "
                       "=> OOB read before blob\n");
        }
        curp += next;
        step++;
        if (step > 8) { printf("  ... (stopping after 8 steps)\n"); break; }
    }
    printf("\n");
}

int
main(void)
{
    /*
     * Case A: a benign metadata blob. type=1(MODINFO_NAME), len=5 ("hello"+nul).
     * Walks forward correctly, hits terminator.
     */
    u_int32_t good[6];
    good[0] = 1;            /* MODINFO_NAME */
    good[1] = 6;            /* len bytes */
    good[2] = 0x00006c6c;   /* "ll" + nul + pad */
    good[3] = 0;
    good[4] = 0;            /* terminator */
    good[5] = 0;
    walk_emulation(good, 6, "A: benign metadata (forward walk OK)");

    /*
     * Case B: tampered len = 0x80000000. 8 + 0x80000000 (as size_t) truncates
     * to int 0x80000008 = negative.  roundup keeps it negative.  curp goes
     * BACKWARD.
     */
    u_int32_t bad1[4];
    bad1[0] = 1;
    bad1[1] = 0x80000000;
    bad1[2] = 0;
    bad1[3] = 0;
    walk_emulation(bad1, 4, "B: hdr[1]=0x80000000 -> next NEGATIVE (backward walk)");

    /*
     * Case C: tampered len = 0xFFFFFFF8. 8 + 0xFFFFFFF8 = 0x100000000 (size_t),
     * truncated to int = 0 -> roundup(0,8)=0 -> next==0 -> infinite loop.
     */
    u_int32_t bad2[4];
    bad2[0] = 1;
    bad2[1] = 0xFFFFFFF8;
    bad2[2] = 0;
    bad2[3] = 0;
    walk_emulation(bad2, 4, "C: hdr[1]=0xFFFFFFF8 -> next==0 (boot hang)");

    return 0;
}