โฌข DragonFlyBSD Kernel Audit
DF-0746 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0746 โ€” Deterministic transcription of the use-after-free READ in
 * l2cap_rtx(): req->lr_id is read AFTER zfree(l2cap_req_pool, req).
 *
 * USERSPACE HARNESS (not a kernel PoC). The runtime netbt L2CAP path
 * (BTPROTO_L2CAP socket + RTX timeout) is unreachable on this KVM guest:
 *   - There is no Bluetooth radio.
 *   - `BLUETOOTH` is `optional bluetooth` (sys/conf/files:1614) and is NOT
 *     listed in sys/config/X86_64_GENERIC, so l2cap_misc.c is not compiled
 *     into the default kernel (only into the unloadable /boot/kernel/netbt.ko).
 *   - In the shipped module, `BLUETOOTH_DEBUG` is NOT defined
 *     (sys/netbt/Makefile has no `-DBLUETOOTH_DEBUG`), so DPRINTF expands to
 *     ((void)0) (sys/netbt/bluetooth.h:145) and req->lr_id is never actually
 *     evaluated at runtime on a production box.
 * The bug is real at the source level (the DPRINTF dereferences req->lr_id
 * after zfree) and WOULD fire in any BLUETOOTH_DEBUG build. The harness
 * transcribes the exact l2cap_rtx body and a faithful vm_zone zalloc/zfree
 * model so the UAF READ is observable deterministically, the same precedent
 * as DF-0745 (the sibling double-free finding on the same function) and the
 * DF-0393/0594/0616/0732/0733 wifi/netgraph/bt-unreachable cluster.
 *
 * ---------------------------------------------------------------------------
 * THE BUG (transcribed exactly from sys/netbt/l2cap_misc.c):
 *
 *   163  void
 *   164  l2cap_request_free(struct l2cap_req *req)
 *   165  {
 *   166    struct hci_link *link = req->lr_link;
 *   167
 *   168    callout_stop(&req->lr_rtx);
 *   169    if (callout_active(&req->lr_rtx))   // dead guard (DF-0745); always falls through
 *   170      return;
 *   171
 *   172    TAILQ_REMOVE(&link->hl_reqs, req, lr_next);
 *   173    zfree(l2cap_req_pool, req);         // <--- req is freed HERE
 *   174  }
 *
 *   183  void
 *   184  l2cap_rtx(void *arg)
 *   185  {
 *   186    struct l2cap_req *req = arg;
 *   187    struct l2cap_channel *chan;
 *   188
 *   189    chan = req->lr_chan;                // chan saved BEFORE free (safe)
 *   190    l2cap_request_free(req);           // <--- calls zfree above
 *   191
 *   192    DPRINTF("cid %d, ident %d\n", (chan ? chan->lc_lcid : 0), req->lr_id);
 *                                       // ^^^^^^^^ USE-AFTER-FREE READ of req->lr_id
 *   193
 *   194    if (chan && chan->lc_state != L2CAP_CLOSED)
 *   195      l2cap_close(chan, ETIMEDOUT);
 *   196  }
 *
 * ---------------------------------------------------------------------------
 * WHAT THE HARNESS PROVES:
 *
 *  (1) zfree(l2cap_req_pool, req) writes ONLY offset 0 (freelist link,
 *      vm_zone.c:233 `((void **)item)[0] = zpcpu->zitems`) and, under
 *      INVARIANTS, offset 8 (ZENTRY_FREE magic, vm_zone.c:237). It does NOT
 *      touch offset 16 (lr_code) or offset 17 (lr_id). So the immediate
 *      stale read of req->lr_id returns the ORIGINAL id โ€” a benign value
 *      the caller itself set in l2cap_request_alloc (l2cap_misc.c:129).
 *
 *  (2) HOWEVER the access is unambiguously against FREED memory: if any
 *      same-CPU zalloc from l2cap_req_pool happens between the zfree and the
 *      DPRINTF (in this single-threaded callout path that cannot happen,
 *      but it is the latent risk the bug creates), the read returns
 *      whatever bytes the new owner wrote at offset 17. The harness models
 *      that reuse step explicitly to demonstrate the read is of freed
 *      memory that may be (re)shaped by an attacker-influenced allocation.
 *
 *  (3) The fix is to capture lr_id BEFORE the free and use the local in the
 *      DPRINTF. harness_fixed.c transcribes the fix and proves the read no
 *      longer touches req after free.
 *
 * ---------------------------------------------------------------------------
 * MODEL FIDELITY:
 *  - struct l2cap_req layout from sys/netbt/l2cap.h:423-430 (lr_link, lr_chan,
 *    lr_code, lr_id, lr_rtx, lr_next). On amd64 the offsets are
 *    0=lr_link, 8=lr_chan, 16=lr_code, 17=lr_id.
 *  - vm_zone zalloc/zfree transcribed from sys/vm/vm_zone.c:87-120 / :211-244
 *    (LIFO per-zone freelist; zfree writes item[0]=link, item[1]=ZENTRY_FREE
 *    under INVARIANTS; zalloc pops item[0]).
 *  - DPRINTF is modeled as an unconditional kprintf (the BLUETOOTH_DEBUG form,
 *    sys/netbt/bluetooth.h:132-135) so the read is actually performed; on a
 *    production kernel DPRINTF is ((void)0) and the read is never emitted,
 *    which is why the runtime impact on the default kernel is "none".
 *
 * Build:  cc -O2 -Wall -Wextra -o harness harness.c
 * Run:    ./harness
 * Expected (BUG PRESENT):
 *     "UAF READ CONFIRMED: req->lr_id dereferenced after zfree"
 *     "  stale-or-reused value at offset 17: 0xNN"
 *     exit 0
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>

/* ----------------------------------------------------------------------
 * Faithful vm_zone model (sys/vm/vm_zone.c:87-120 / :211-244)
 * LIFO freelist, item[0]=next, item[1]=ZENTRY_FREE under INVARIANTS.
 * -------------------------------------------------------------------- */
#define ZENTRY_FREE       0x12342378u          /* vm_zone.c:76 */
#define INVARIANTS                                  /* model a GENERIC build */

struct zone {
    void      *zitems;       /* LIFO head */
    size_t     zsize;
    const char *zname;
};

static inline void *
zalloc(struct zone *z)
{
    void *item = z->zitems;
    if (item) {
        z->zitems = *(void **)item;                 /* vm_zone.c:104 pop */
        return item;
    }
    return malloc(z->zsize);                        /* back the zone with malloc */
}

static inline void
zfree(struct zone *z, void *item)
{
    *(void **)item = z->zitems;                     /* vm_zone.c:233 item[0]=link */
#ifdef INVARIANTS
    if (((void **)item)[1] == (void *)(uintptr_t)ZENTRY_FREE)
        fprintf(stderr, "  [INVARIANTS] zerror(ZONE_ERROR_ALREADYFREE)\n");
    ((void **)item)[1] = (void *)(uintptr_t)ZENTRY_FREE; /* vm_zone.c:237 */
#endif
    z->zitems = item;                               /* vm_zone.c:239 push */
}

/* ----------------------------------------------------------------------
 * struct l2cap_req  (sys/netbt/l2cap.h:423-430)
 * On amd64: 0=lr_link(8) 8=lr_chan(8) 16=lr_code(1) 17=lr_id(1)
 *           18..23 pad, then lr_rtx (struct callout), then lr_next.
 * For the harness we only need the head layout (offsets 0..17); the rest is
 * modeled as opaque bytes so the struct is the same size class as in the
 * kernel.
 * -------------------------------------------------------------------- */
struct l2cap_req {
    void         *lr_link;        /* offset 0 */
    void         *lr_chan;        /* offset 8 */
    uint8_t       lr_code;        /* offset 16 */
    uint8_t       lr_id;          /* offset 17  <-- the field read after free */
    uint8_t       pad[6];         /* 18..23 */
    uint8_t       lr_rtx_dummy[64]; /* stand-in for struct callout */
    /* TAILQ_ENTRY follows in the kernel; size-only here */
    uint8_t       lr_tail[16];
};

static struct zone l2cap_req_pool = { .zitems = NULL,
                                      .zsize = sizeof(struct l2cap_req),
                                      .zname = "l2cap_req" };

/* ----------------------------------------------------------------------
 * DPRINTF in BLUETOOTH_DEBUG form (sys/netbt/bluetooth.h:132-135).
 * In production this is ((void)0) and req->lr_id is never read; we model the
 * debug form so the UAF is observable.
 * -------------------------------------------------------------------- */
#define DPRINTF(fmt, ...) \
    do { if (bt_debug) printf("%s: " fmt, "l2cap_rtx", ##__VA_ARGS__); } while (0)
static int bt_debug = 1;

/* Global used to detect that req->lr_id was actually dereferenced after free */
static int      g_uaf_read_observed = 0;
static uint8_t  g_uaf_read_value    = 0;

/* ----------------------------------------------------------------------
 * l2cap_request_free  (sys/netbt/l2cap_misc.c:163-174) โ€” transcribed verbatim
 * -------------------------------------------------------------------- */
struct hci_link { int dummy; } fake_link;

static void
l2cap_request_free(struct l2cap_req *req)
{
    /* callout_stop + dead callout_active guard elided (see DF-0745): always
     * falls through to TAILQ_REMOVE + zfree. */
    (void)fake_link;
    zfree(&l2cap_req_pool, req);            /* l2cap_misc.c:173 */
}

/* ----------------------------------------------------------------------
 * l2cap_rtx  (sys/netbt/l2cap_misc.c:183-197) โ€” transcribed verbatim
 *
 * NB: in the kernel, req is freed at line 190 then read at line 192.
 * We instrument the read so it is unambiguously observable.
 * -------------------------------------------------------------------- */
static void
l2cap_rtx(void *arg)
{
    struct l2cap_req *req = arg;
    struct l2cap_channel { uint16_t lc_lcid; int lc_state; } *chan; /* min model */

    chan = (void *)req->lr_chan;                       /* l2cap_misc.c:189 (safe) */
    l2cap_request_free(req);                           /* l2cap_misc.c:190 (FREES) */

    /* l2cap_misc.c:192 โ€” DPRINTF reads req->lr_id AFTER free.
     * We mark the address as freed first, then perform the exact deref. */
    {
        volatile uint8_t *p = &req->lr_id;
        g_uaf_read_observed = 1;
        g_uaf_read_value    = *p;                      /* THE UAF READ */
        DPRINTF("cid %d, ident %d\n",
                (chan ? (int)chan->lc_lcid : 0),
                (int)g_uaf_read_value);
    }

    if (chan && ((int *)(void *)chan)[1] != 0)
        (void)0; /* l2cap_close stub */
}

/* ----------------------------------------------------------------------
 * Test 1: stale-but-original read (no intervening reuse).
 * zfree leaves offset 17 untouched, so the value read is the original id.
 * -------------------------------------------------------------------- */
static void test_stale_read(void)
{
    printf("== Test 1: stale read of req->lr_id immediately after zfree ==\n");
    struct l2cap_req *req = zalloc(&l2cap_req_pool);
    memset(req, 0, sizeof(*req));
    req->lr_link = &fake_link;
    req->lr_code = 0x0a;                 /* L2CAP_CONNECT_REQ etc. */
    req->lr_id   = 0x42;                 /* the "original" id */
    g_uaf_read_observed = 0;
    g_uaf_read_value    = 0;
    l2cap_rtx(req);
    printf("  g_uaf_read_observed = %d\n", g_uaf_read_observed);
    printf("  g_uaf_read_value    = 0x%02x (original was 0x42)\n",
           (unsigned)g_uaf_read_value);
    if (g_uaf_read_observed &&
        g_uaf_read_value == 0x42) {
        printf("UAF READ CONFIRMED: req->lr_id dereferenced after zfree\n");
        printf("  stale value at offset 17 == original id (no slab reuse yet)\n");
    } else {
        printf("UNEXPECTED: uaf_observed=%d value=0x%02x\n",
               g_uaf_read_observed, (unsigned)g_uaf_read_value);
        exit(2);
    }
    printf("\n");
}

/* ----------------------------------------------------------------------
 * Test 2: same-CPU slab reuse between zfree and DPRINTF.
 * Model an intervening zalloc (which hands back the just-freed slot),
 * let the new owner scribble an attacker-chosen byte at offset 17,
 * then perform the DPRINTF read. This proves the read is of memory that
 * an attacker-influenced allocation can reshape โ€” the latent risk.
 * (In the actual l2cap_rtx path no allocation sits between zfree and DPRINTF,
 *  but the bug is that the deref is of freed memory that ANY later allocator
 *  action can repurpose.)
 * -------------------------------------------------------------------- */
static void test_reuse_read(void)
{
    printf("== Test 2: read after same-CPU slab reuse (latent risk) ==\n");
    /* prime the pool so zfree pushes one entry, then a new zalloc pops it */
    struct l2cap_req *victim = zalloc(&l2cap_req_pool);
    memset(victim, 0, sizeof(*victim));
    victim->lr_link = &fake_link;
    victim->lr_id   = 0x11;

    /* free it back into the LIFO */
    zfree(&l2cap_req_pool, victim);

    /* a different consumer grabs the same slot and writes attacker bytes */
    struct l2cap_req *reused = zalloc(&l2cap_req_pool);
    if (reused != victim) {
        printf("UNEXPECTED: pool did not hand back the same slot\n");
        exit(2);
    }
    memset(reused, 0xCC, sizeof(*reused));   /* attacker-shaped */
    reused->lr_id = 0xDD;

    /* now model the DPRINTF read that l2cap_rtx performs on the ORIGINAL
     * pointer after its zfree โ€” the original pointer aliases the reused slot */
    volatile uint8_t *p = &victim->lr_id;
    uint8_t got = *p;
    printf("  read of freed req->lr_id returned 0x%02x (reused slot wrote 0xDD)\n",
           (unsigned)got);
    if (got == 0xDD) {
        printf("UAF READ CONFIRMED: freed req->lr_id returns attacker-shaped byte\n");
    } else {
        printf("UNEXPECTED: got 0x%02x\n", (unsigned)got);
        exit(2);
    }
    printf("\n");
}

int main(void)
{
    printf("DF-0746 harness: UAF read of req->lr_id in l2cap_rtx after zfree\n");
    printf("transcribed from sys/netbt/l2cap_misc.c:163-197\n");
    printf("vm_zone model from sys/vm/vm_zone.c:87-244\n\n");

    test_stale_read();
    test_reuse_read();

    printf("== Summary ==\n");
    printf("BUG: l2cap_rtx reads req->lr_id AFTER zfree(l2cap_req_pool, req)\n");
    printf("     at sys/netbt/l2cap_misc.c:192 (free at :173 via :190).\n");
    printf("RUNTIME IMPACT on default X86_64_GENERIC kernel: NONE โ€”\n");
    printf("     netbt is `optional bluetooth` (not in GENERIC), and even in\n");
    printf("     the netbt.ko module DPRINTF is ((void)0) without -DBLUETOOTH_DEBUG,\n");
    printf("     so the deref is never emitted in production. Latent hardening\n");
    printf("     bug for BLUETOOTH_DEBUG builds.\n");
    return 0;
}