/*
 * DF-0884 - Use-after-free in smbfs_readvdir via dead-code vnode lock
 *           upgrade on read(2) of a directory.
 *
 * This is a DETERMINISTIC HARNESS that transcribes the kernel race from
 * sys/vfs/smbfs/smbfs_io.c and sys/vfs/smbfs/smbfs_smb.c into userspace.
 * The live trigger needs a mounted SMB share (an SMB server reachable from
 * the kernel client), which is not present on this isolated KVM guest -
 * there is no network beyond QEMU user-mode NAT to the host and no SMB
 * server.  Per the harness precedent (DF-0598 / DF-0599, also smbfs), we
 * prove the bug deterministically by transcribing the exact code path and
 * racing the two threads across a controllable interleaving point that
 * models the SMB network round-trip inside smbfs_findnext().
 *
 * ---------------------------------------------------------------------------
 * The bug (every hop cited against sys/vfs/smbfs/smbfs_io.c):
 *
 *   vfs_vnops.c:751   vn_read() does  vn_lock(vp, LK_SHARED | LK_RETRY)
 *                     so the directory vnode is held SHARED on read(2).
 *   smbfs_io.c:202    lks = LK_EXCLUSIVE; with the lockstatus() call that
 *                     would detect the real shared mode COMMENTED OUT; lks
 *                     is hardcoded to LK_EXCLUSIVE.
 *   smbfs_io.c:203    if (lks == LK_SHARED)   -- ALWAYS FALSE (dead code)
 *   smbfs_io.c:204        vn_lock(vp, LK_UPGRADE ...)  -- DEAD CODE
 *   smbfs_io.c:205    error = smbfs_readvdir(vp, ...)  -- runs SHARED
 *
 *   smbfs_readvdir() (smbfs_io.c:78-174) MUTATES per-vnode directory
 *   iteration state: np->n_dirseq and np->n_dirofs (lines 118-141).  The
 *   dead-code upgrade means two read(2) callers can run smbfs_readvdir()
 *   CONCURRENTLY under the shared lock.
 *
 *   The race to UAF:
 *     Thread A:  smbfs_readvdir -> smbfs_findnext(ctx, ...)  (smbfs_io.c:137
 *                or :151).  smbfs_findnext() (smbfs_smb.c:1196) blocks on a
 *                full SMB network round-trip inside smbfs_findnextLM1/LM2
 *                (smbfs_smb.c:854/1049) while still holding the ctx pointer
 *                (== the pointer stored in np->n_dirseq).
 *     Thread B:  smbfs_readvdir, offset != np->n_dirofs (A advanced it), so
 *                takes the REOPEN branch (smbfs_io.c:118-135) and calls
 *                smbfs_findclose(np->n_dirseq, ...) at smbfs_io.c:121.
 *                smbfs_findclose() (smbfs_smb.c:1224-1236) does
 *                kfree(ctx, M_SMBFSDATA) at smbfs_smb.c:1234.
 *     Thread A:  resumes from the blocked smbfs_findnext() and WRITES
 *                ctx->f_attr.fa_ino at smbfs_smb.c:1220 (and reads it back
 *                at smbfs_io.c:157) -> WRITE/READ UAF on freed smbfs_fctx.
 *
 *   The getdents(2) path is NOT affected: smbfs_readdir() unconditionally
 *   takes LK_EXCLUSIVE (smbfs_vnops.c:725).
 *
 * ---------------------------------------------------------------------------
 * Harness model:
 *   - The vnode lock is a pthread rwlock; LK_SHARED  == read-lock,
 *     LK_EXCLUSIVE == write-lock, LK_UPGRADE == upgrade (write-lock).
 *   - The "SMB network round-trip" inside smbfs_findnext is a controllable
 *     barrier: Thread A blocks on a pthread cond var; the test thread
 *     releases it ONLY AFTER Thread B has run smbfs_findclose() on A ctx,
 *     modelling the full round-trip race window deterministically.
 *   - The allocator is POISONED: every free() overwrites the object with
 *     0xDD bytes and marks it freed, so a write through the dangling ctx
 *     is detected unambiguously (the poison signature is destroyed /
 *     observed-over-freed).
 *
 *   Two modes (argv[1]):
 *     "buggy"  -- transcribes smbfs_io.c:202-203 as-shipped (dead upgrade).
 *     "fixed"  -- transcribes the fix.diff: UNCONDITIONAL LK_UPGRADE before
 *                 smbfs_readvdir() and LK_DOWNGRADE after, so the two
 *                 readers serialize and the race cannot occur.
 *
 * Build:  cc -O2 -pthread -o harness harness.c
 * Run:    ./harness buggy    (expect: UAF CONFIRMED)
 *         ./harness fixed    (expect: FIXED no UAF)
 */

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>

/* ---- poisoned allocator ------------------------------------------------ */
#define POISON_BYTE 0xDD
struct alloc_rec {
    void   *ptr;
    size_t  sz;
    int     freed;
};
static struct alloc_rec g_allocs[64];
static int              g_nallocs = 0;

static void *
poison_malloc(size_t sz)
{
    void *p = calloc(1, sz ? sz : 1);
    if (!p) { perror("calloc"); exit(2); }
    if (g_nallocs < 64) {
        g_allocs[g_nallocs].ptr = p;
        g_allocs[g_nallocs].sz  = sz;
        g_allocs[g_nallocs].freed = 0;
        g_nallocs++;
    }
    return p;
}

static void
poison_free(void *p)
{
    int i;
    for (i = 0; i < g_nallocs; i++) {
        if (g_allocs[i].ptr == p) {
            if (g_allocs[i].freed) {
                fprintf(stderr, "[alloc] DOUBLE FREE detected on %p\n", p);
                exit(3);
            }
            /* model kfree(): don't actually return the page to the pool
             * yet; poison it so a write through the dangling pointer is
             * observable. */
            memset(p, POISON_BYTE, g_allocs[i].sz);
            g_allocs[i].freed = 1;
            return;
        }
    }
    fprintf(stderr, "[alloc] free of untracked %p\n", p);
    exit(4);
}

static int
poison_is_freed(void *p)
{
    int i;
    for (i = 0; i < g_nallocs; i++)
        if (g_allocs[i].ptr == p)
            return g_allocs[i].freed;
    return -1;
}

/* ---- transcribed kernel structures (only fields the race touches) ----- */
struct smbfattr {                 /* smbfs_subr.h:65 */
    int      fa_attr;
    int64_t  fa_size;
    long     fa_ino;
    /* fa_mtime etc. elided — irrelevant to the race */
};

struct smbnode {                  /* smbfs_node.h:69-70 */
    struct smbfs_fctx *n_dirseq;  /* ff context (the raced pointer) */
    long               n_dirofs;  /* last ff offset */
    long               n_ino;
};

struct smbfs_fctx {              /* smbfs_subr.h:90 */
    int             f_flags;
    struct smbfattr f_attr;
    char           *f_name;
    int             f_nmlen;
    long            f_dirofs_marker; /* unused in race, kept for size */
    /* remaining fields (f_ssp, f_rq, f_t2, f_skey, ...) elided */
};

/* ---- vnode lock model -------------------------------------------------- */
static pthread_rwlock_t g_vnlock;     /* models vp->v_lock */

/* ---- network-round-trip interleaving point ----------------------------- */
/* Thread A blocks in smbfs_findnext() on g_netio_cond until the orchestrator
 * (main) signals it AFTER Thread B has freed A's ctx.  This deterministically
 * models the full SMB network round-trip during which the race window is
 * open. */
static pthread_mutex_t g_netio_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  g_netio_cond = PTHREAD_COND_INITIALIZER;
static int             g_netio_thread_a_blocked = 0;   /* A set then waits */
static int             g_netio_release_a        = 0;   /* orchestrator set */
static volatile int    g_b_freed_a_ctx         = 0;   /* B did findclose */
static int             g_a_write_saw_freed     = -1;  /* A's snapshot at write */

/* ---- transcribed smbfs_smb.c ------------------------------------------- */
static int
model_smbfs_findopen(struct smbnode *np, struct smbfs_fctx **ctxpp)
{                              /* smbfs_smb.c:1170 */
    struct smbfs_fctx *ctx = poison_malloc(sizeof(*ctx)); /* :1177 */
    ctx->f_flags = 0;
    ctx->f_attr.fa_ino = 0;
    ctx->f_name = poison_malloc(16);
    *ctxpp = ctx;
    (void)np;
    return 0;
}

static int
model_smbfs_findnext(struct smbfs_fctx *ctx, int limit)
{                              /* smbfs_smb.c:1196 */
    /* smbfs_findnextLM1/LM2 perform an SMB network round-trip here
     * (smbfs_smb.c:868 smbfs_smb_search / :1099 smb_t2_request).  The race
     * window is exactly this blocking I/O.  Model it: block on the cond var
     * the FIRST time (Thread A) so B can run findclose() underneath us. */
    (void)limit;
    pthread_mutex_lock(&g_netio_mtx);
    if (!g_netio_thread_a_blocked) {
        /* This is Thread A's call.  Announce we are "in" the network I/O
         * holding ctx (== np->n_dirseq), then block until released. */
        g_netio_thread_a_blocked = 1;
        pthread_cond_signal(&g_netio_cond);   /* wake orchestrator */
        while (!g_netio_release_a)
            pthread_cond_wait(&g_netio_cond, &g_netio_mtx);
        pthread_mutex_unlock(&g_netio_mtx);

        /* *** RACE WINDOW IS NOW OPEN *** ctx may have been kfree()'d by
         * Thread B's smbfs_findclose() while we were blocked.  Snapshot the
         * freed state BEFORE the write so the verdict can tell the UAF case
         * (B freed first) from the safe case (A still owns ctx).  Then
         * resume the transcribed post-I/O write: smbfs_smb.c:1220. */
        g_a_write_saw_freed = poison_is_freed(ctx);
        ctx->f_attr.fa_ino = 0xCAFEBABE;                 /* THE UAF WRITE */
        return 0;
    }
    /* Subsequent calls (Thread B) model instant local completion. */
    pthread_mutex_unlock(&g_netio_mtx);
    ctx->f_attr.fa_ino = 0xBEEF;
    return 0;
}

static void
model_smbfs_findclose(struct smbfs_fctx *ctx)
{                              /* smbfs_smb.c:1224 */
    if (ctx->f_name)
        poison_free(ctx->f_name);                   /* :1233 (f_rname) */
    poison_free(ctx);                               /* :1234 kfree(ctx) */
}

/* ---- transcribed smbfs_io.c smbfs_readvdir (only the raced path) ------ */
struct uio { long uio_offset; long uio_resid; };

static int
model_smbfs_readvdir(struct smbnode *np, struct uio *uio)
{                              /* smbfs_io.c:78 */
    struct smbfs_fctx *ctx;
    int error, offset;

    if (uio->uio_offset < 0)
        return 1;               /* smbfs_io.c:91 EINVAL */
    offset = uio->uio_offset;   /* :95 */

    /* skip the "." / ".." synthesis (lines 97-113): we model offset==0 as
     * a fresh reader so the reopen branch fires. */

    if (offset != np->n_dirofs || np->n_dirseq == NULL) {   /* :118 */
        if (np->n_dirseq) {
            /* *** smbfs_io.c:121 *** -- this is the kfree() that races
             * with another thread's in-flight smbfs_findnext(ctx). */
            model_smbfs_findclose(np->n_dirseq);
            np->n_dirseq = NULL;                            /* :122 */
        }
        np->n_dirofs = 2;                                   /* :124 */
        error = model_smbfs_findopen(np, &ctx);             /* :125 */
        if (error) return error;
        np->n_dirseq = ctx;                                 /* :132 */
    } else {
        ctx = np->n_dirseq;                                 /* :134 */
    }
    /* skip-ahead loop (136-144) elided; offset model jumps to inner loop */
    /* inner loop (146-168): smbfs_findnext then use ctx->f_attr */
    if (uio->uio_resid > 0) {
        error = model_smbfs_findnext(ctx, 1);               /* :151 */
        if (error) return error;
        np->n_dirofs++;                                     /* :154 */
        /* smbfs_io.c:157 reads ctx->f_attr.fa_ino back: another UAF read
         * on the freed ctx.  Touch it to make the read observable. */
        long ino = ctx->f_attr.fa_ino;                      /* UAF READ */
        (void)ino;
    }
    return 0;
}

/* ---- transcribed smbfs_io.c smbfs_readvnode ---------------------------- */
static int g_fixed_mode = 0;   /* 0 = as-shipped (dead upgrade), 1 = fixed */

static int
model_smbfs_readvnode(struct smbnode *np, struct uio *uio)
{                              /* smbfs_io.c:176 */
    int lks, error;

    if (uio->uio_resid == 0) return 0;                      /* :196 */

    if (1 /* vp->v_type == VDIR, :201 */) {
        /* smbfs_io.c:202-204 (AS-SHIPPED -- dead upgrade): */
        lks = 2 /* LK_EXCLUSIVE */;  /* lockstatus() commented out */
        if (lks == 1 /* LK_SHARED */)            /* :203 ALWAYS FALSE */
            pthread_rwlock_wrlock(&g_vnlock);    /* :204 dead */

        if (g_fixed_mode) {
            /* fix.diff: UNCONDITIONAL shared->exclusive upgrade before the
             * directory iteration that mutates per-vnode state.
             *
             * DragonFly lockmgr LK_UPGRADE (kern_lock.c:576) has an explicit
             * anti-deadlock rule: if another upgrade is already pending, the
             * caller RELEASES its shared lock and acquires exclusive
             * normally (kern_lock.c:616-625).  pthread rwlocks have no
             * atomic upgrade and would deadlock if a read-holder calls
             * wrlock, so we model the upgrade as that same safe sequence:
             * drop shared, take exclusive.  After readvdir we reverse it
             * (drop exclusive, take shared) so vn_read's caller still
             * observes a shared hold on return (model of LK_DOWNGRADE). */
            pthread_rwlock_unlock(&g_vnlock);               /* drop shared */
            if (pthread_rwlock_wrlock(&g_vnlock) != 0) {    /* take excl */
                fprintf(stderr, "FIXED: upgrade failed\n");
                return 1;
            }
        }

        error = model_smbfs_readvdir(np, uio);              /* :205 */

        if (g_fixed_mode) {
            pthread_rwlock_unlock(&g_vnlock);               /* drop excl */
            pthread_rwlock_rdlock(&g_vnlock);               /* take shared */
        }
        return error;
    }
    return 0;
}

/* ---- vnode lock acquire/release (model of vfs_vnops.c:751) ------------- */
static void
vn_read_enter(void) {           /* vn_read: LK_SHARED */
    pthread_rwlock_rdlock(&g_vnlock);
}
static void
vn_read_leave(void) {
    pthread_rwlock_unlock(&g_vnlock);
}

/* ---- worker threads ---------------------------------------------------- */
struct job_arg { struct smbnode *np; long offset; long resid; int rc; };

static void *
thread_read(void *v)
{
    struct job_arg *a = v;
    struct uio uio = { a->offset, a->resid };

    vn_read_enter();                       /* vfs_vnops.c:751 LK_SHARED */
    a->rc = model_smbfs_readvnode(a->np, &uio);
    vn_read_leave();
    return NULL;
}

int
main(int argc, char **argv)
{
    struct smbnode node;
    pthread_t ta, tb;
    struct job_arg a_arg = { &node, 0, 64, -1 };
    struct job_arg b_arg = { &node, 0, 64, -1 };

    if (argc >= 2 && strcmp(argv[1], "fixed") == 0)
        g_fixed_mode = 1;

    pthread_rwlock_init(&g_vnlock, NULL);

    memset(&node, 0, sizeof(node));
    node.n_dirseq = NULL;
    node.n_dirofs = 0;
    node.n_ino    = 2;

    printf("[*] DF-0884 harness: mode=%s\n", g_fixed_mode ? "FIXED" : "BUGGY");
    printf("[*] Thread A: read(2) on VDIR at offset 0 (will block in smbfs_findnext)\n");
    printf("[*] Thread B: read(2) on VDIR at offset 0 (will reopen -> findclose A's ctx)\n");

    /* Start Thread A; it will enter smbfs_findnext, announce it is blocked,
     * and wait for the orchestrator release. */
    pthread_create(&ta, NULL, thread_read, &a_arg);

    /* Wait until Thread A is blocked "in network I/O" holding ctx. */
    pthread_mutex_lock(&g_netio_mtx);
    while (!g_netio_thread_a_blocked)
        pthread_cond_wait(&g_netio_cond, &g_netio_mtx);
    pthread_mutex_unlock(&g_netio_mtx);
    printf("[*] Thread A is now blocked inside smbfs_findnext() holding ctx (== n_dirseq)\n");
    printf("[*]   n_dirseq=%p n_dirofs=%ld\n", (void*)node.n_dirseq, node.n_dirofs);

    /* Snapshot the ctx pointer Thread A is using. */
    struct smbfs_fctx *a_ctx = node.n_dirseq;

    /* Start Thread B.  In BUGGY mode B's read holds the lock SHARED (A does
     * too, because the upgrade is dead code), so B enters smbfs_readvdir
     * concurrently, takes the REOPEN branch (B offset 0 != n_dirofs 2), and
     * calls smbfs_findclose(n_dirseq = A's ctx) -> kfree(A's ctx).  In FIXED
     * mode B blocks on the write-lock upgrade until A is done. */
    pthread_create(&tb, NULL, thread_read, &b_arg);

    if (!g_fixed_mode) {
        /* Give B time to run findclose() on A's ctx.  B will complete its
         * readvdir and release the shared lock. */
        usleep(100 * 1000);
        int fr = poison_is_freed(a_ctx);
        printf("[*] After Thread B ran: A's ctx=%p freed? %s (poison=0x%02X)\n",
               (void*)a_ctx, fr == 1 ? "YES" : (fr == 0 ? "no" : "?"),
               POISON_BYTE);
        if (fr != 1) {
            printf("[!] UNEXPECTED: B did not free A's ctx; harness mis-modelled\n");
            pthread_mutex_lock(&g_netio_mtx);
            g_netio_release_a = 1;
            pthread_cond_broadcast(&g_netio_cond);
            pthread_mutex_unlock(&g_netio_mtx);
            pthread_join(ta, NULL); pthread_join(tb, NULL);
            return 5;
        }
        printf("[*] Releasing Thread A from smbfs_findnext() -- it will now write ctx->f_attr.fa_ino THROUGH FREED MEMORY\n");
    } else {
        printf("[*] FIXED mode: Thread B is blocked on the exclusive upgrade "
               "(A still holds shared); A must finish readvdir first.\n");
        printf("[*] Releasing Thread A so it can complete smbfs_readvdir() "
               "and only THEN drop the shared lock for B.\n");
    }

    /* Release Thread A.  In BUGGY mode: A resumes its post-I/O write through
     * the already-freed ctx (UAF).  In FIXED mode: A completes readvdir
     * normally and releases the shared lock, after which B's upgrade wins
     * and B runs readvdir on its own ctx (no race). */
    pthread_mutex_lock(&g_netio_mtx);
    g_netio_release_a = 1;
    pthread_cond_broadcast(&g_netio_cond);
    pthread_mutex_unlock(&g_netio_mtx);

    pthread_join(ta, NULL);
    pthread_join(tb, NULL);

    printf("[*] Thread A rc=%d  Thread B rc=%d\n", a_arg.rc, b_arg.rc);

    /* ---- verdict ------------------------------------------------------- */
    if (!g_fixed_mode) {
        /* g_a_write_saw_freed captures whether A's ctx was already freed at
         * the instant A resumed and wrote fa_ino.  In buggy mode B's
         * findclose() ran while A was blocked, so it is 1 (freed) -> UAF. */
        if (g_a_write_saw_freed == 1) {
            printf("[+] A wrote ctx->f_attr.fa_ino (0x%lx) into an object "
                   "that was ALREADY freed by Thread B's smbfs_findclose()\n",
                   (unsigned long)a_ctx->f_attr.fa_ino);
            printf(">>> UAF CONFIRMED: smbfs_findnext wrote through freed "
                   "smbfs_fctx (dead-code lock upgrade lets two read(2) on a "
                   "VDIR run smbfs_readvdir concurrently)\n");
            return 0;
        }
        printf("[!] UAF NOT observed (g_a_write_saw_freed=%d, harness error)\n",
               g_a_write_saw_freed);
        return 6;
    } else {
        /* In fixed mode the upgrade serializes A and B; A completes its full
         * readvdir (findnext -> dirent) BEFORE B can enter, so B never frees
         * A's in-flight ctx.  g_a_write_saw_freed must be 0 (not freed at
         * A's write instant). */
        if (g_a_write_saw_freed == 0) {
            printf("[+] FIXED: at A's write instant, ctx was still owned by A "
                   "(g_a_write_saw_freed=0); the exclusive upgrade serialized "
                   "the two readers so B's findclose ran only after A finished\n");
            printf(">>> FIXED: no UAF -- unconditional LK_UPGRADE before "
                   "smbfs_readvdir closes the race\n");
            return 0;
        }
        printf("[!] FIXED mode but A's ctx was freed at write instant "
               "(g_a_write_saw_freed=%d) -- fix ineffective\n",
               g_a_write_saw_freed);
        return 7;
    }
}
