โฌข DragonFlyBSD Kernel Audit
DF-0598 / leak.c
โ† back to finding โ†“ download raw
/*
 * DF-0598 โ€” Code-level proof: smb_sm_lookupint leaks a VC ref on every miss.
 *
 * The netsmb in-kernel SMB client (`sys/netproto/smb/smb_conn.c`) is
 * `optional netsmb` and is NOT compiled into the audited X86_64_GENERIC
 * kernel; the loadable KLD (`sys/vfs/smbfs/smbfs.ko`) builds & loads cleanly
 * on this guest (verified separately โ€” see env.txt) and creates /dev/nsmb
 * (mode 0700).  Driving the bug end-to-end in-kernel additionally requires a
 * VC already in the vclist, which only appears after a real SMB server
 * connection โ€” none is reachable on this isolated guest.  This harness
 * instead reproduces the *exact control flow* of smb_sm_lookupint in
 * userspace, verbatim line-for-line against the audited source, proving the
 * stale-`vcp` leak deterministically.  (Mirrors the DF-0265 precedent.)
 *
 * Build: cc -O2 -Wall -o leak leak.c
 * Run:   ./leak [N]
 *        N = number of failing lookups (default 10)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>

/* --- constants verbatim from sys/netproto/smb/smb_conn.h / smb.h ------------ */
#define SMBL_VC            1
#define SMBM_MASK       0777
#define SMBM_EXACT     010000
#define SMBM_ANY_OWNER  ((uid_t)-1)
#define SMBM_ANY_GROUP  ((gid_t)-1)
#define SMBV_PRIVATE   0x0020
#define SMBV_CREATE    0x0100

/* --- SLIST (verbatim semantics from sys/sys/queue.h) ----------------------- *
 * The kernel's queue.h `SLIST_HEAD(name,type)` with empty `name` produces an
 * anonymous struct used as a member type; we just write that struct out
 * directly to stay portable across userspace <sys/queue.h> variants. */
struct smb_co_listhead { struct smb_connobj *slh_first; };
#define SLIST_FIRST(head)   ((head)->slh_first)
#define SLIST_NEXT(elem, field)  ((elem)->field.sle_next)
#define SLIST_FOREACH(var, head, field)            \
    for ((var) = SLIST_FIRST(head);                \
         (var);                                    \
         (var) = SLIST_NEXT(var, field))
#define SLIST_INSERT_HEAD(head, elem, field) do {            \
    (elem)->field.sle_next = SLIST_FIRST(head);              \
    SLIST_FIRST(head) = (elem);                              \
} while (0)
#define SLIST_INIT(head) do { SLIST_FIRST(head) = NULL; } while (0)

/* --- minimal in-kernel types, only the fields smb_sm_lookupint touches ----- */

struct smb_connobj {
    int                       co_level;     /* SMBL_* */
    int                       co_flags;
    int                       co_usecount;
    struct smb_co_listhead    co_children;  /* mirrors SLIST_HEAD(,smb_connobj) */
    struct { struct smb_connobj *sle_next; } co_next;  /* SLIST_ENTRY(smb_connobj) */
};

/* SMBCO_FOREACH is defined in sys/netproto/smb/smb_conn.h:223 as
 *   #define SMBCO_FOREACH(var, cp)  SLIST_FOREACH((var), &(cp)->co_children, co_next)
 * โ€” a 2-arg macro that bakes in `co_next`.  Replicate verbatim so the harness
 * body cites the kernel source line-for-line. */
#define SMBCO_FOREACH(var, cp)  SLIST_FOREACH((var), &(cp)->co_children, co_next)

/* VC "derives" from connobj by embedding `obj` as the first member, exactly
 * like sys/netproto/smb/smb_conn.h:243-244.  This is what makes the
 * `(struct smb_vc *)scp` cast on line 136 valid and is the crux of the bug:
 * `scp` walks the children list (type smb_connobj*), while `vcp` is the
 * derived VC pointer; assigning one to the other preserves identity. */
struct smb_vc {
    struct smb_connobj obj;        /* MUST be first */
    const char *vc_username;
    uid_t       vc_uid;
    gid_t       vc_grp;
    int         vc_mode;
    /* paddr/sap compare: we model with a single int "port" to drive a miss */
    int         vc_port;
};

/* smb_vcspec โ€” fields the comparisons at smb_conn.c:142-163 touch */
struct smb_vcspec {
    const char *username;
    uid_t       owner;
    gid_t       group;
    int         mode;
    int         sap_port;       /* models vcspec->sap (sockaddr*) */
    struct smb_vc *shspec;      /* unused here; mirrors real struct */
};

/* smb_vc_ref = smb_co_ref (smb_conn.c:270-276) โ€” atomic usecount++ */
static void smb_co_ref(struct smb_connobj *cp) { cp->co_usecount++; }
#define smb_vc_ref(vcp)  smb_co_ref(&(vcp)->obj)

/* smb_vc_lock/unlock โ€” no-op in this single-threaded harness; the bug is
 * pure control-flow, independent of locking. */
static int  smb_vc_lock(struct smb_vc *vcp, int flags) { (void)vcp; (void)flags; return 0; }
static void smb_vc_unlock(struct smb_vc *vcp, int flags) { (void)vcp; (void)flags; }

#define CONNADDREQ_PORT(a_port, b_port)  ((a_port) == (b_port))

/* smb_vc_access โ€” model the access check; return nonzero => miss */
static int smb_vc_access(struct smb_vc *vcp, void *scred, int mode) {
    (void)vcp; (void)scred; (void)mode; return 0;
}

/* The kernel's smb_vclist (smb_conn.c:106) is `static struct smb_connobj
 * smb_vclist;` โ€” a connobj whose co_children is the list head.  We mirror
 * that exactly so SMBCO_FOREACH(scp, &smb_vclist) works verbatim. */
static struct smb_connobj smb_vclist;

/* ===========================================================================
 * VULNERABLE smb_sm_lookupint โ€” VERBATIM copy of the audited source,
 * sys/netproto/smb/smb_conn.c:123-180.  Only edits: drop the smb_sharespec
 * second-lookup half (not on the failing path), replace CONNADDREQ/strcmp
 * with the model macros above, drop the shspec/ssp block (not exercised on
 * the username-miss path the harness drives).  Line numbers preserved as
 * comments so this file is auditable side-by-side with the kernel source.
 * =========================================================================== */
static int
smb_sm_lookupint_VULN(struct smb_vcspec *vcspec,
                      struct smb_vc **vcpp)
{
    struct smb_connobj *scp;
    struct smb_vc *vcp;
    int exact = 1;
    int error;

    /* smb_conn.c:132-134 */
    vcspec->shspec = NULL;
    error = ENOENT;
    vcp = NULL;

    /* smb_conn.c:135  SMBCO_FOREACH(scp, &smb_vclist) */
    SMBCO_FOREACH(scp, &smb_vclist) {
        /* smb_conn.c:136  vcp = (struct smb_vc *)scp;   <-- STALE WRITE */
        vcp = (struct smb_vc *)scp;

        /* smb_conn.c:137-139 */
        error = smb_vc_lock(vcp, 0 /* LK_EXCLUSIVE */);
        if (error)
            continue;

        /* smb_conn.c:141-145  -- the username-miss the harness drives */
        error = 1;
        if ((vcp->obj.co_flags & SMBV_PRIVATE) ||
            !CONNADDREQ_PORT(vcp->vc_port, vcspec->sap_port) ||
            strcmp(vcp->vc_username, vcspec->username) != 0)
            goto unlock;

        /* smb_conn.c:146-160  owner/group/exact checks (we use ANY -> exact=0) */
        if (vcspec->owner != SMBM_ANY_OWNER) {
            if (vcp->vc_uid != vcspec->owner)
                goto unlock;
        } else
            exact = 0;
        if (vcspec->group != SMBM_ANY_GROUP) {
            if (vcp->vc_grp != vcspec->group)
                goto unlock;
        } else
            exact = 0;
        if (vcspec->mode & SMBM_EXACT) {
            if (!exact || (vcspec->mode & SMBM_MASK) != vcp->vc_mode)
                goto unlock;
        }

        /* smb_conn.c:162-163 */
        if (smb_vc_access(vcp, NULL, vcspec->mode) != 0)
            goto unlock;

        /* smb_conn.c:170-171  success */
        error = 0;
        break;

    unlock:
        /* smb_conn.c:172-173  -- NOTE: no `vcp = NULL;` here! */
        smb_vc_unlock(vcp, 0);
    }

    /* smb_conn.c:175-178  -- fires on STALE vcp after a list-exhausting miss */
    if (vcp) {
        smb_vc_ref(vcp);
        *vcpp = vcp;
    }
    return error;
}

/* ===========================================================================
 * FIXED smb_sm_lookupint โ€” applies fix.diff: clear vcp on every unlock path
 * (so the post-loop block only fires on a genuine `break`), plus an
 * `error == 0` belt-and-suspenders guard on the ref block.
 * =========================================================================== */
static int
smb_sm_lookupint_FIXED(struct smb_vcspec *vcspec,
                       struct smb_vc **vcpp)
{
    struct smb_connobj *scp;
    struct smb_vc *vcp;
    int exact = 1;
    int error;

    vcspec->shspec = NULL;
    error = ENOENT;
    vcp = NULL;

    SMBCO_FOREACH(scp, &smb_vclist) {
        vcp = (struct smb_vc *)scp;

        error = smb_vc_lock(vcp, 0);
        if (error) {
            vcp = NULL;                      /* FIX: lock-fail path */
            continue;
        }

        error = 1;
        if ((vcp->obj.co_flags & SMBV_PRIVATE) ||
            !CONNADDREQ_PORT(vcp->vc_port, vcspec->sap_port) ||
            strcmp(vcp->vc_username, vcspec->username) != 0)
            goto unlock;

        if (vcspec->owner != SMBM_ANY_OWNER) {
            if (vcp->vc_uid != vcspec->owner)
                goto unlock;
        } else
            exact = 0;
        if (vcspec->group != SMBM_ANY_GROUP) {
            if (vcp->vc_grp != vcspec->group)
                goto unlock;
        } else
            exact = 0;
        if (vcspec->mode & SMBM_EXACT) {
            if (!exact || (vcspec->mode & SMBM_MASK) != vcp->vc_mode)
                goto unlock;
        }
        if (smb_vc_access(vcp, NULL, vcspec->mode) != 0)
            goto unlock;

        error = 0;
        break;

    unlock:
        smb_vc_unlock(vcp, 0);
        vcp = NULL;                          /* FIX: goto-unlock paths */
    }

    if (error == 0 && vcp) {                 /* FIX: belt-and-suspenders */
        smb_vc_ref(vcp);
        *vcpp = vcp;
    }
    return error;
}

/* --- driver --------------------------------------------------------------- */
int main(int argc, char **argv)
{
    int N = (argc > 1) ? atoi(argv[1]) : 10;
    if (N <= 0) N = 10;

    /* Build a vclist with 3 VCs (mirrors a system with one user logged into
     * three SMB servers).  Each VC starts life at usecount=1 (smb_co_init,
     * smb_conn.c:238). */
    SLIST_INIT(&smb_vclist.co_children);

    static struct smb_vc vca, vcb, vcc;   /* static -> stable addresses */
    struct smb_vc *vcs[] = { &vca, &vcb, &vcc };
    const char *names[] = { "alice@a", "bob@b", "carol@c" };
    for (int i = 0; i < 3; i++) {
        vcs[i]->obj.co_level   = SMBL_VC;
        vcs[i]->obj.co_flags   = 0;
        vcs[i]->obj.co_usecount = 1;            /* smb_co_init baseline */
        vcs[i]->vc_username    = names[i];
        vcs[i]->vc_uid         = 1001;
        vcs[i]->vc_grp         = 1001;
        vcs[i]->vc_mode        = 0700;
        vcs[i]->vc_port        = 139 + i;
        SLIST_INSERT_HEAD(&smb_vclist.co_children, &vcs[i]->obj, co_next);
    }
    /* SLIST_INSERT_HEAD reverses insertion order: head -> vcc -> vcb -> vca.
     * The LAST VC visited by SMBCO_FOREACH is therefore vca ("alice@a"). */

    printf("[harness] vclist head-order: ");
    for (struct smb_connobj *s = SLIST_FIRST(&smb_vclist.co_children); s; s = SLIST_NEXT(s, co_next))
        printf("%s ", ((struct smb_vc *)s)->vc_username);
    printf("\n");
    printf("[harness] %d failing lookups (username 'nobody@nowhere' matches none)\n\n", N);

    /* ---------------- VULNERABLE ---------------- */
    printf("=== VULNERABLE smb_sm_lookupint (sys/netproto/smb/smb_conn.c:123-180) ===\n");
    int base_use_a = vca.obj.co_usecount;
    int base_use_b = vcb.obj.co_usecount;
    int base_use_c = vcc.obj.co_usecount;
    struct smb_vc *leaked_last = NULL;

    for (int i = 0; i < N; i++) {
        struct smb_vcspec spec = {
            .username  = "nobody@nowhere",
            .owner     = SMBM_ANY_OWNER,
            .group     = SMBM_ANY_GROUP,
            .mode      = SMBM_MASK,
            .sap_port  = 9999,             /* matches no VC's port */
        };
        /* The kernel caller (smb_sm_lookup, smb_conn.c:190) does
         * `*vcpp = vcp = NULL;` BEFORE calling smb_sm_lookupint.  Replicate
         * that contract: out enters NULL, and we observe whether the callee
         * (mis)writes it. */
        struct smb_vc *out = NULL;
        int rc = smb_sm_lookupint_VULN(&spec, &out);
        if (i == 0) {
            printf("  call #0: rc=%d (nonzero => lookup FAILED), but *vcpp=%s (NOT NULL!)\n",
                   rc, out ? out->vc_username : "(null)");
            leaked_last = out;
        }
    }
    printf("  after %d failed lookups:\n", N);
    printf("    usecount[alice@a] = %d  (baseline %d, delta %+d)\n",
           vca.obj.co_usecount, base_use_a, vca.obj.co_usecount - base_use_a);
    printf("    usecount[bob@b]   = %d  (baseline %d, delta %+d)\n",
           vcb.obj.co_usecount, base_use_b, vcb.obj.co_usecount - base_use_b);
    printf("    usecount[carol@c] = %d  (baseline %d, delta %+d)\n",
           vcc.obj.co_usecount, base_use_c, vcc.obj.co_usecount - base_use_c);
    int leaked = (vca.obj.co_usecount - base_use_a) +
                 (vcb.obj.co_usecount - base_use_b) +
                 (vcc.obj.co_usecount - base_use_c);
    printf("    TOTAL leaked refs across all VCs = %d  (expected %d)\n", leaked, N);
    printf("    every miss returned *vcpp=%s (WRONG VC: caller gets a ref to a VC\n"
           "        that does NOT match the lookup, and never releases it)\n",
           leaked_last ? leaked_last->vc_username : "(null)");

    int vuln_leaked = leaked;

    /* ---------------- FIXED ---------------- */
    printf("\n=== FIXED smb_sm_lookupint (fix.diff applied) ===\n");
    /* reset usecounts to baseline */
    vca.obj.co_usecount = vcb.obj.co_usecount = vcc.obj.co_usecount = 1;
    base_use_a = base_use_b = base_use_c = 1;
    leaked_last = NULL;

    for (int i = 0; i < N; i++) {
        struct smb_vcspec spec = {
            .username  = "nobody@nowhere",
            .owner     = SMBM_ANY_OWNER,
            .group     = SMBM_ANY_GROUP,
            .mode      = SMBM_MASK,
            .sap_port  = 9999,
        };
        struct smb_vc *out = NULL;   /* caller pre-NULLs (smb_conn.c:190) */
        int rc = smb_sm_lookupint_FIXED(&spec, &out);
        if (i == 0) {
            printf("  call #0: rc=%d (nonzero => lookup FAILED), *vcpp=%s\n",
                   rc, out == NULL ? "(null) โ€” correct: miss leaves caller's NULL intact"
                                  : "(NON-NULL โ€” BUG)");
            leaked_last = out;
        }
    }
    printf("  after %d failed lookups:\n", N);
    printf("    usecount[alice@a] = %d  (baseline %d, delta %+d)\n",
           vca.obj.co_usecount, base_use_a, vca.obj.co_usecount - base_use_a);
    printf("    usecount[bob@b]   = %d  (baseline %d, delta %+d)\n",
           vcb.obj.co_usecount, base_use_b, vcb.obj.co_usecount - base_use_b);
    printf("    usecount[carol@c] = %d  (baseline %d, delta %+d)\n",
           vcc.obj.co_usecount, base_use_c, vcc.obj.co_usecount - base_use_c);
    leaked = (vca.obj.co_usecount - base_use_a) +
             (vcb.obj.co_usecount - base_use_b) +
             (vcc.obj.co_usecount - base_use_c);
    printf("    TOTAL leaked refs across all VCs = %d  (expected 0)\n", leaked);
    printf("    every miss returned *vcpp=%s\n",
           leaked_last == NULL ? "(null) โ€” caller cannot confuse a NULL VC for a real one" : "(NON-NULL โ€” BUG)");

    /* ---------------- VERDICT ---------------- */
    printf("\n=== RESULT ===\n");
    if (vuln_leaked == N && leaked == 0) {
        printf("REPRODUCED: vulnerable lookupint leaked %d VC ref(s) across %d miss(es);\n"
               "FIXED:      patched lookupint leaked 0 ref(s) across %d miss(es).\n"
               "The stale `vcp` post-loop in sys/netproto/smb/smb_conn.c:175-178 is\n"
               "the root cause; fix.diff closes it.\n",
               vuln_leaked, N, N);
        return 0;
    } else {
        printf("UNEXPECTED: vuln_leaked=%d (want %d), fixed_leaked=%d (want 0)\n",
               vuln_leaked, N, leaked);
        return 1;
    }
}