DragonFlyBSD Kernel Audit
DF-0832 / udf_oob_kmod.c
← back to finding ↓ download raw
/*
 * DF-0832 — In-kernel deterministic harness for the off-by-one OOB read
 * in udf_bmap_internal (sys/vfs/udf/udf_vnops.c:1104 / :1128).
 *
 * This kernel module replicates the EXACT buggy do/while loop from
 * udf_bmap_internal and runs it against a controlled struct file_entry
 * allocation with a guard sentinel placed right past the live AD area.
 * It detects whether the loop dereferences past l_ad (the off-by-one)
 * and prints the result.
 *
 * Build (in-guest, as root):
 *   cd /usr/src/sys && make -V SYSDIR|xargs -I{} cc -DKERNEL_TEST \
 *     -c /root/udf_oob_kmod.c -I{} -o /tmp/udf_oob_kmod.o   # (fiddly)
 *
 * Simpler: build as a loadable kld module.  Save this file as
 * /root/df0832_oob/udf_oob.c and the Makefile below as
 * /root/df0832_oob/Makefile, then `make && kldload ./udf_oob.ko`.
 *
 * Makefile:
 *   .PATH:  /root/df0832_oob
 *   KMOD=   udf_oob
 *   SRCS=   udf_oob.c
 *   .include <bsd.kmod.mk>
 *
 * Expected output (bug present): dmesg shows
 *   "DF-0832: OOB READ DETECTED at ad_offset=8 l_ad=8 ..."
 * Expected (bug fixed, >= instead of >): dmesg shows
 *   "DF-0832: no OOB (clean EINVAL at ad_offset=8)"
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <sys/sysctl.h>

MALLOC_DECLARE(M_UDFFENTRY);
MALLOC_DEFINE(M_UDFFENTRY, "UDF fentry harness", "DF-0832 harness");

#include <vfs/udf/ecma167-udf.h>

#define SHORT_AD_SIZE (sizeof(struct short_ad))   /* 8 */

static int
run_bmap_short(struct file_entry *fe, uint32_t offset,
               uint32_t *out_sector, uint32_t *out_max, int *out_oob)
{
    void *icb;
    uint32_t icblen = 0;
    int ad_offset, ad_num = 0;

    *out_oob = 0;

    /* ---- VERBATIM kernel loop: sys/vfs/udf/udf_vnops.c:1101-1111 ---- */
    do {
        offset -= icblen;
        ad_offset = SHORT_AD_SIZE * ad_num;
        if (ad_offset > (int)fe->l_ad) {        /* line 1104 — THE BUG: > should be >= */
            return (EINVAL);
        }
        /* detect OOB: are we about to read past the live AD area? */
        if (ad_offset + SHORT_AD_SIZE > (int)fe->l_ad)
            *out_oob = 1;    /* GETICB below dereferences past l_ad */
        icb = GETICB(long_ad, fe, fe->l_ea + ad_offset);
        icblen = GETICBLEN(short_ad, icb);
        ad_num++;
    } while (offset >= icblen);
    /* ---- end verbatim ---- */

    *out_sector = ((struct short_ad *)icb)->pos;
    *out_max = icblen;
    return (0);
}

static int
udf_oob_modevent(module_t mod, int type, void *data)
{
    struct file_entry *fe;
    uint32_t sector = 0, max_size = 0;
    uint32_t offset = 2048;
    int rc, oob = 0;
    int ret = 0;

    switch (type) {
    case MOD_LOAD:
        /*
         * Allocate exactly one short_ad worth of AD area, plus a guard
         * short_ad filled with a sentinel.  l_ea=0 so data[0] is the
         * start of the AD area.
         */
        fe = kmalloc(UDF_FENTRY_SIZE + SHORT_AD_SIZE + SHORT_AD_SIZE,
                     M_UDFFENTRY, M_WAITOK | M_ZERO);
        fe->l_ea = 0;
        fe->l_ad = SHORT_AD_SIZE;    /* 8 bytes = one short_ad */

        /* the one valid short_ad: covers 2048 bytes at sector 5 */
        ((struct short_ad *)&fe->data[0])->len = 2048;
        ((struct short_ad *)&fe->data[0])->pos = 5;

        /* guard: the next 8 bytes (which the buggy loop reads OOB) */
        ((struct short_ad *)&fe->data[SHORT_AD_SIZE])->len = 0xDEADBEEFu;
        ((struct short_ad *)&fe->data[SHORT_AD_SIZE])->pos = 0xCAFEBABEu;

        rc = run_bmap_short(fe, offset, &sector, &max_size, &oob);

        kprintf("DF-0832: udf_bmap_internal off-by-one in-kernel harness\n");
        kprintf("  fentry l_ea=%u l_ad=%u (1 short_ad), offset=%u\n",
                fe->l_ea, fe->l_ad, offset);
        kprintf("  loop rc=%d sector=0x%x max_size=0x%x oob_read=%d\n",
                rc, sector, max_size, oob);
        if (oob) {
            kprintf("DF-0832: OOB READ DETECTED at ad_offset==l_ad==%u:\n"
                    "  loop dereferenced &fentry->data[l_ea+l_ad] = ONE PAST the\n"
                    "  allocation-descriptor area.  On a real fentry this is an 8-byte\n"
                    "  (short_ad) / 16-byte (long_ad) heap OOB read.\n",
                    fe->l_ad);
            if (sector == 0xCAFEBABE && max_size == 0xDEADBEEF)
                kprintf("  CONFIRMED: returned values are the guard sentinel\n"
                        "  (0x%08x / 0x%08x).\n", sector, max_size);
        } else {
            kprintf("DF-0832: no OOB (bound is tight: >= fired at ad_offset==l_ad).\n");
        }
        kfree(fe, M_UDFFENTRY);
        break;
    case MOD_UNLOAD:
        break;
    default:
        ret = EOPNOTSUPP;
        break;
    }
    return (ret);
}

static moduledata_t udf_oob_mod = {
    "udf_oob",
    udf_oob_modevent,
    NULL
};
DECLARE_MODULE(udf_oob, udf_oob_mod, SI_SUB_EXEC, SI_ORDER_ANY);
MODULE_VERSION(udf_oob, 1);