DragonFlyBSD Kernel Audit
DF-0285 / layout_proof.c
← back to finding ↓ download raw
/*
 * layout_proof.c - DF-0285 structural / overflow-reach proof
 *
 * This is a *userspace layout replica* of the relevant slice of
 * `struct ieee80211_node` (the "11s state" block from
 * sys/netproto/802_11/ieee80211_node.h:199-208) together with an exact
 * reproduction of `struct callout` (sys/sys/callout.h:77-82) and the
 * separately-allocated `struct _callout` (sys/sys/callout.h:54-75) it points at.
 *
 * The real header cannot be included verbatim in userspace (it depends on a
 * large forest of kernel-only macros / locks), so we replicate ONLY the field
 * types and order, which is what governs the C ABI layout. All types are
 * stdint / pointer types with identical size/alignment on x86_64 to the kernel
 * originals. We assert this at runtime with sizeof/alignof checks.
 *
 * Purpose: prove, with offsetof() arithmetic, that
 *
 *   memcpy(ni->ni_meshid, ie+2, ie[1])      [ieee80211_mesh.c:3460]
 *
 * with an attacker-controlled ie[1] in (32 .. 255] overflows the fixed 32-byte
 * ni_meshid array into the subsequent fields, AND in particular into
 * ni_mltimer (a struct callout whose first member is a pointer, `toc`).
 *
 * IMPORTANT CORRECTION TO THE FINDING CLAIM:
 *   The finding says ni_mltimer "is a callout containing a function pointer".
 *   That is imprecise. `struct callout` (the field ni_mltimer) does NOT itself
 *   contain a function pointer; it contains a POINTER `toc` to a
 *   separately-allocated `struct _callout`, and the function pointers
 *   (`rfunc`/`qfunc`) live inside that `_callout`. So the overflow corrupts the
 *   `toc` pointer, giving an indirect / type-confused function-pointer-control
 *   primitive (controlled pointer deref -> attacker-chosen qfunc), not a
 *   direct function-pointer overwrite. The program below prints the exact byte
 *   index at which each field (and ni_mltimer.toc specifically) is clobbered.
 *
 * Build: see build.sh
 * Run:   see run.sh   (prints the layout table + overflow-reach analysis)
 */
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#define IEEE80211_MESHID_LEN 32   /* sys/netproto/802_11/ieee80211.h:200 */

/* ---- exact replica of sys/sys/callout.h:54-75 ------------------------------ */
struct _callout {
    /* spinlock, exis, tailq_entry are kernel-internal; their sizes are not
     * relevant to THIS proof because _callout is the *pointed-to* object, not
     * part of the contiguous ieee80211_node overflow region. We only need its
     * members that matter for the function-pointer-control claim. */
    void   *spin_skip[3];     /* spinlock + exis + TAILQ_ENTRY (placeholder) */
    void   *verifier;
    uint32_t flags;
    uint32_t lineno;
    void   *lk;
    const char *ident;
    void   *rsc;
    void   *rarg;
    void   (*rfunc)(void *);   /* <--- function pointer #1 */
    int     rtick;
    uint32_t unused01;
    void   *qsc;
    void   *qarg;
    void   (*qfunc)(void *);   /* <--- function pointer #2 (the one fired) */
    int     qtick;
    int     waiters;
};

/* ---- exact replica of sys/sys/callout.h:77-82 ------------------------------ */
struct callout {
    struct _callout *toc;      /* opaque internal pointer  <-- FIRST FIELD */
    struct lock_stub *lk;      /* callout_init() copy data */
    uint32_t flags;            /* callout_init() copy data */
    uint32_t unused01;
};
struct lock_stub;  /* opaque ptr type, size = 8 */

/* ---- exact replica of the "11s state" block: ieee80211_node.h:199-208 ------ */
enum ieee80211_mesh_mlstate { ML_DUMMY = 4 };  /* plain enum -> int (4 bytes) */

struct node_11s_block {
    uint8_t             ni_meshidlen;                         /* :199 */
    uint8_t             ni_meshid[IEEE80211_MESHID_LEN];      /* :200 */
    enum ieee80211_mesh_mlstate ni_mlstate;                   /* :201 */
    uint16_t            ni_mllid;                             /* :202 */
    uint16_t            ni_mlpid;                             /* :203 */
    struct callout      ni_mltimer;                           /* :204 */
    uint8_t             ni_mlrcnt;                            /* :205 */
    uint8_t             ni_mltval;                            /* :206 */
    struct callout      ni_mlhtimer;                          /* :207 */
    uint8_t             ni_mlhcnt;                            /* :208 */
};

static void check(const char *what, int ok)
{
    printf("  %-42s %s\n", what, ok ? "OK" : "*** MISMATCH ***");
}

int main(void)
{
    struct node_11s_block n;

    printf("== ABI sanity (must all be OK) ==\n");
    check("sizeof(void*) == 8",                sizeof(void*) == 8);
    check("sizeof(struct callout) == 24",      sizeof(struct callout) == 24);
    check("sizeof(enum) == 4",                 sizeof(enum ieee80211_mesh_mlstate) == 4);
    check("IEEE80211_MESHID_LEN == 32",        IEEE80211_MESHID_LEN == 32);

    printf("\n== Field layout (offsets relative to start of ni_meshid) ==\n");
    /* base address of ni_meshid in this replica object */
    char *base = (char *)&n.ni_meshid;
#define REL(f) ((long)((char *)&(n.f) - base))
    printf("  ni_meshid[0]   rel %+4ld  (memcpy dst, attacker data starts here)\n", REL(ni_meshid[0]));
    printf("  ni_meshid[31]  rel %+4ld  (last legal byte)\n",                 REL(ni_meshid[31]));
    printf("  ni_mlstate     rel %+4ld  enum (peering FSM state)\n",          REL(ni_mlstate));
    printf("  ni_mllid       rel %+4ld  uint16 link-local id\n",              REL(ni_mllid));
    printf("  ni_mlpid       rel %+4ld  uint16 link peer id\n",               REL(ni_mlpid));
    printf("  ni_mltimer     rel %+4ld  struct callout\n",                    REL(ni_mltimer));
    printf("  ni_mltimer.toc rel %+4ld  *** POINTER to _callout (qfunc) ***\n",REL(ni_mltimer.toc));
    printf("  ni_mltimer.lk  rel %+4ld  pointer\n",                           REL(ni_mltimer.lk));
    printf("  ni_mltimer.flags rel %+4ld\n",                                   REL(ni_mltimer.flags));
    printf("  ni_mlrcnt      rel %+4ld\n",                                     REL(ni_mlrcnt));
    printf("  ni_mltval      rel %+4ld\n",                                     REL(ni_mltval));
    printf("  ni_mlhtimer    rel %+4ld  struct callout (2nd callout clobbered)\n", REL(ni_mlhtimer));
    printf("  ni_mlhtimer.toc rel %+4ld  *** 2nd POINTER clobbered ***\n",     REL(ni_mlhtimer.toc));
    printf("  ni_mlhcnt      rel %+4ld\n",                                     REL(ni_mlhcnt));

    /* end of the clobberable region we model (next field would be 11n state) */
    long region_end = REL(ni_mlhcnt) + 1;

    printf("\n== Overflow-reach analysis for ie[1]=N (attacker length byte) ==\n");
    printf("  memcpy copies N bytes starting at ni_meshid[0] (rel 0).\n");
    printf("  Bytes at rel >= 32 are PAST the legal ni_meshid[] array -> heap OOB.\n");
    printf("\n  %-10s %-12s %-12s %-12s\n", "ie[1]", "OOB_bytes", "reaches_toc?", "reaches_2nd_toc?");
    int sizes[] = { 33, 48, 64, 128, 200, 255 };
    for (size_t i = 0; i < sizeof(sizes)/sizeof(sizes[0]); i++) {
        int N = sizes[i];
        int oob = N - IEEE80211_MESHID_LEN;            /* bytes past ni_meshid */
        int last_rel = N - 1;                            /* last byte written, rel */
        int reach_toc  = (last_rel >= REL(ni_mltimer.toc));
        int reach_toc2 = (last_rel >= REL(ni_mlhtimer.toc));
        printf("  %-10d %-12d %-12s %-12s\n",
               N, oob, reach_toc ? "YES" : "no", reach_toc2 ? "YES" : "no");
    }

    printf("\n== Worst case ie[1]=255 ==\n");
    {
        int N = 255;
        int oob = N - IEEE80211_MESHID_LEN;       /* 223 */
        int last_rel = N - 1;                       /* 254 */
        printf("  Overflow past ni_meshid: %d bytes\n", oob);
        printf("  Last byte written at rel %d (region we model ends at rel %ld)\n",
               last_rel, region_end);
        printf("  -> clobbers ni_mlstate, ni_mllid, ni_mlpid, ni_mltimer (incl .toc ptr),\n");
        printf("     ni_mlrcnt, ni_mltval, ni_mlhtimer (incl .toc ptr), ni_mlhcnt, AND\n");
        printf("     continues %ld bytes past ni_mlhcnt into the 11n HT-state fields.\n",
               (long)(last_rel - region_end + 1));
    }

    printf("\n== Verdict ==\n");
    printf("  The memcpy at ieee80211_mesh.c:3460 with attacker ie[1] in (32..255]\n");
    printf("  performs an up-to-223-byte heap OOB write into ni_mlstate/ni_mllid/\n");
    printf("  ni_mlpid/ni_mltimer(+.toc ptr)/ni_mlrcnt/ni_mltval/ni_mlhtimer(+.toc ptr)/\n");
    printf("  ni_mlhcnt and on into HT state. Two struct callout `toc` pointers are\n");
    printf("  attacker-controlled -> indirect function-pointer-control primitive\n");
    printf("  (forge toc -> forged _callout -> chosen qfunc). Real bug, real RCE\n");
    printf("  surface; NOT directly a function-pointer field overwrite (correction\n");
    printf("  to the finding's wording).\n");
    return 0;
}