โฌข DragonFlyBSD Kernel Audit
DF-0808 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0808 โ€” dirfs_nrename: missing NULL check on absolute-path return
 *
 * Bug location: sys/vfs/dirfs/dirfs_vnops.c:977-981 (dirfs_nrename)
 *
 *   977:  tpath = dirfs_node_absolute_path_plus(dmp, tdnp,
 *   978:                                tncp->nc_name, &tpathfree);
 *   979:  fpath = dirfs_node_absolute_path_plus(dmp, fdnp,
 *   980:                                fncp->nc_name, &fpathfree);
 *   981:  error = rename(fpath, tpath);          // NO NULL CHECK
 *
 * dirfs_node_absolute_path_plus (dirfs_subr.c:377-443) returns NULL when:
 *   - cur == NULL              (line 390)
 *   - assembled path > MAXPATHLEN  (line 433 condition fails)
 *   - parent chain broken / unlinked (dnp1==NULL, line 423 break)
 *
 * The finding CLAIMS "rename(NULL,...) derefs NULL in kernel = panic".
 * This harness PROVES the claim is FALSE: rename(NULL,...) returns -1/EFAULT,
 * not a segfault or panic.  The kernel's copyinstr(NULL) path catches the
 * bad address and returns EFAULT cleanly.
 *
 * WHY A HARNESS / WHY NOT A LIVE KERNEL TRIGGER:
 *   dirfs is vkernel64-only โ€” listed in sys/platform/vkernel64/conf/files as
 *   "optional dirfs" but NOT in sys/conf/files.  It is absent from the running
 *   X86_64_GENERIC kernel, and there is no dirfs.ko in /boot/kernel.  It
 *   cannot be mounted or triggered on this guest.  The harness transcribes
 *   the exact buggy function and exercises the actual rename(NULL,...) call
 *   via libc (the same call dirfs makes โ€” dirfs_vnops.c includes <unistd.h>).
 *
 * The harness:
 *   1. Transcribes dirfs_node_absolute_path_plus faithfully and shows it
 *      returns NULL for over-length paths.
 *   2. Calls rename(NULL,...) live and shows it returns EFAULT (no crash).
 *   3. Shows the FIXED path (NULL check -> ENAMETOOLONG) returns the
 *      correct POSIX error.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */

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

#define MAXPATHLEN 1024

/* ---- Faithful transcription of dirfs_node_absolute_path_plus ---- */
/*   sys/vfs/dirfs/dirfs_subr.c:377-443                            */

struct dirfs_node {
    char *dn_name;
    int   dn_namelen;
    int   dn_isroot;
    struct dirfs_node *dn_parent;
};

/*
 * Returns a malloc'd path string, or NULL when the assembled path exceeds
 * MAXPATHLEN or the parent chain is broken.  Sets *pathfree to the malloc'd
 * buffer (or NULL on failure) exactly as the kernel version does.
 */
static char *
transcribed_absolute_path_plus(const char *dm_path,
                               struct dirfs_node *cur,
                               const char *last,
                               char **pathfreep)
{
    int len;
    struct dirfs_node *dnp1;
    char *buf;
    int count;

    *pathfreep = NULL;
    if (cur == NULL)
        return NULL;

    buf = malloc(MAXPATHLEN + 1);

    count = 0;
    buf[MAXPATHLEN] = 0;
    if (last) {
        len = strlen(last);
        count += len;
        if (count <= MAXPATHLEN)
            memcpy(&buf[MAXPATHLEN - count], last, len);
        ++count;
        if (count <= MAXPATHLEN)
            buf[MAXPATHLEN - count] = '/';
    }

    dnp1 = cur;
    while (dnp1->dn_isroot == 0) {
        count += dnp1->dn_namelen;
        if (count <= MAXPATHLEN)
            memcpy(&buf[MAXPATHLEN - count], dnp1->dn_name, dnp1->dn_namelen);
        ++count;
        if (count <= MAXPATHLEN)
            buf[MAXPATHLEN - count] = '/';
        dnp1 = dnp1->dn_parent;
        if (dnp1 == NULL)
            break;
    }

    len = strlen(dm_path);
    count += len;
    if (dnp1 && count <= MAXPATHLEN) {
        memcpy(&buf[MAXPATHLEN - count], dm_path, len);
        *pathfreep = buf;
        return &buf[MAXPATHLEN - count];
    } else {
        free(buf);
        *pathfreep = NULL;
        return NULL;
    }
}

/* ---- dirfs_dropfd transcription (cleanup) ---- */
static void
transcribed_dropfd(char *pathfree)
{
    if (pathfree)
        free(pathfree);
}

/* ---- VULNERABLE dirfs_nrename (lines 977-1020) ---- */
static int
vulnerable_nrename(const char *dm_path,
                   struct dirfs_node *fdnp, const char *fname,
                   struct dirfs_node *tdnp, const char *tname)
{
    char *fpath, *fpathfree;
    char *tpath, *tpathfree;
    int error;

    tpath = transcribed_absolute_path_plus(dm_path, tdnp, tname, &tpathfree);
    fpath = transcribed_absolute_path_plus(dm_path, fdnp, fname, &fpathfree);

    /* line 981: NO NULL CHECK โ€” calls rename with potentially-NULL args */
    error = rename(fpath, tpath);
    if (error < 0)
        error = errno;

    if (error == 0) {
        printf("    rename succeeded (unexpected for NULL test)\n");
    }

    transcribed_dropfd(fpathfree);
    transcribed_dropfd(tpathfree);

    return error;
}

/* ---- FIXED dirfs_nrename (with NULL check) ---- */
static int
fixed_nrename(const char *dm_path,
              struct dirfs_node *fdnp, const char *fname,
              struct dirfs_node *tdnp, const char *tname)
{
    char *fpath, *fpathfree;
    char *tpath, *tpathfree;
    int error;

    tpath = transcribed_absolute_path_plus(dm_path, tdnp, tname, &tpathfree);
    fpath = transcribed_absolute_path_plus(dm_path, fdnp, fname, &fpathfree);

    /* FIX: check for NULL before calling rename */
    if (fpath == NULL || tpath == NULL) {
        error = ENAMETOOLONG;
    } else {
        error = rename(fpath, tpath);
        if (error < 0)
            error = errno;
    }

    transcribed_dropfd(fpathfree);
    transcribed_dropfd(tpathfree);

    return error;
}

/* Build a simple node hierarchy: root -> dir1 -> dir2 -> ...
 * Each node is individually malloc'd so cleanup can free them one by one.
 * Caller frees: each node->dn_name (if dynamic), then each node struct.
 */
static struct dirfs_node *
make_chain(const char *names[], int depth)
{
    struct dirfs_node **nodes = calloc(depth, sizeof(struct dirfs_node *));
    /* Pass 1: allocate all nodes */
    for (int i = 0; i < depth; i++)
        nodes[i] = calloc(1, sizeof(struct dirfs_node));
    /* Pass 2: set fields (parent pointers reference already-allocated nodes) */
    for (int i = 0; i < depth; i++) {
        nodes[i]->dn_name = (char *)names[i];
        nodes[i]->dn_namelen = strlen(names[i]);
        nodes[i]->dn_isroot = (i == depth - 1) ? 1 : 0;
        nodes[i]->dn_parent = (i < depth - 1) ? nodes[i + 1] : NULL;
    }
    struct dirfs_node *head = nodes[0];
    free(nodes);
    return head;
}

static struct dirfs_node *
make_chain_dynamic(const char *basename, int comp_len, int depth)
{
    struct dirfs_node **nodes = calloc(depth, sizeof(struct dirfs_node *));
    /* Pass 1: allocate all nodes + names */
    for (int i = 0; i < depth; i++) {
        char *n = malloc(comp_len + 1);
        memset(n, 'a', comp_len);
        n[comp_len] = 0;
        nodes[i] = calloc(1, sizeof(struct dirfs_node));
        nodes[i]->dn_name = n;
        nodes[i]->dn_namelen = comp_len;
    }
    /* Pass 2: set hierarchy */
    for (int i = 0; i < depth; i++) {
        nodes[i]->dn_isroot = (i == depth - 1) ? 1 : 0;
        nodes[i]->dn_parent = (i < depth - 1) ? nodes[i + 1] : NULL;
    }
    struct dirfs_node *head = nodes[0];
    free(nodes);
    return head;
}

/* Free a chain of individually-allocated nodes (with dynamic names). */
static void
free_chain_dynamic(struct dirfs_node *head)
{
    struct dirfs_node *cur = head;
    while (cur) {
        struct dirfs_node *parent = cur->dn_parent;
        free(cur->dn_name);
        free(cur);
        cur = parent;
    }
}

/* Free a chain with static names (only free node structs). */
static void
free_chain_static(struct dirfs_node *head)
{
    struct dirfs_node *cur = head;
    while (cur) {
        struct dirfs_node *parent = cur->dn_parent;
        free(cur);
        cur = parent;
    }
}

int main(void)
{
    printf("=== DF-0808 dirfs_nrename missing-NULL-check analysis ===\n\n");
    printf("Bug: sys/vfs/dirfs/dirfs_vnops.c:977-981 โ€” tpath/fpath from\n");
    printf("     dirfs_node_absolute_path_plus() passed to rename() with NO\n");
    printf("     NULL check. Finding claims: 'rename(NULL,...) derefs NULL = panic'\n\n");

    /* ---- Part 1: show dirfs_node_absolute_path_plus returns NULL ---- */
    printf("--- Part 1: dirfs_node_absolute_path_plus returns NULL for over-length paths ---\n\n");

    /* Short path: should succeed */
    {
        const char *names[] = {"subdir", "rootdir"};
        struct dirfs_node *fdnp = make_chain(names, 2); /* subdir -> root */
        struct dirfs_node *tdnp = fdnp;
        char *path, *pathfree;
        path = transcribed_absolute_path_plus("/mnt/dirfs", tdnp, "myfile.txt", &pathfree);
        printf("  Short path test:\n");
        printf("    dm_path=\"/mnt/dirfs\", dir=\"subdir\", name=\"myfile.txt\"\n");
        printf("    result: %s (pathfree=%p)\n\n", path ? path : "NULL", (void *)pathfree);
        if (pathfree) free(pathfree);
        free_chain_static(fdnp);
    }

    /* Over-length path: dm_path + many dirs + long name > MAXPATHLEN */
    {
        /* 5 levels of 200-char components = 5*201 = 1005, + dm_path(50) = 1055 > 1024 */
        struct dirfs_node *fdnp = make_chain_dynamic("comp", 200, 6); /* 5 dirs + root */
        struct dirfs_node *tdnp = fdnp;
        char *tpath, *tpathfree;
        char *fpath, *fpathfree;
        tpath = transcribed_absolute_path_plus("/a/reasonably/long/host/mount/path/here",
                                                tdnp, "target_name_here", &tpathfree);
        fpath = transcribed_absolute_path_plus("/a/reasonably/long/host/mount/path/here",
                                                fdnp, "source_name_here", &fpathfree);
        printf("  Over-length path test (5 dirs * 200 chars + mount path > MAXPATHLEN):\n");
        printf("    tpath = %s\n", tpath ? "NON-NULL" : "NULL  <-- over-length, returns NULL");
        printf("    fpath = %s\n", fpath ? "NON-NULL" : "NULL  <-- over-length, returns NULL");
        printf("    => dirfs_node_absolute_path_plus CAN return NULL (CONFIRMED)\n\n");
        if (tpathfree) free(tpathfree);
        if (fpathfree) free(fpathfree);
        free_chain_dynamic(fdnp);
    }

    /* ---- Part 2: live rename(NULL,...) โ€” does it crash? ---- */
    printf("--- Part 2: live rename(NULL, ...) call โ€” crash or EFAULT? ---\n\n");

    /* Create a dummy source so the valid path exists */
    FILE *f = fopen("/tmp/df0808_testfile", "w");
    if (f) fclose(f);

    int r, err;
    errno = 0;
    r = rename(NULL, "/tmp/df0808_dst");
    err = errno;
    printf("  rename(NULL, \"/tmp/df0808_dst\"):\n");
    printf("    return value: %d\n", r);
    printf("    errno:        %d (%s)\n", err, strerror(err));
    printf("    => NO CRASH.  Returns EFAULT (kernel copyinstr catches NULL).\n\n");

    errno = 0;
    r = rename("/tmp/df0808_testfile", NULL);
    err = errno;
    printf("  rename(\"/tmp/df0808_testfile\", NULL):\n");
    printf("    return value: %d, errno: %d (%s)\n", r, err, strerror(err));
    printf("    => NO CRASH.  Returns EFAULT.\n\n");

    errno = 0;
    r = rename(NULL, NULL);
    err = errno;
    printf("  rename(NULL, NULL):\n");
    printf("    return value: %d, errno: %d (%s)\n", r, err, strerror(err));
    printf("    => NO CRASH.  Returns EFAULT.\n\n");

    /* ---- Part 3: VULNERABLE vs FIXED dirfs_nrename transcription ---- */
    printf("--- Part 3: dirfs_nrename transcription (over-length path scenario) ---\n\n");

    {
        struct dirfs_node *fdnp = make_chain_dynamic("comp", 200, 6);
        struct dirfs_node *tdnp = fdnp;
        const char *dm_path = "/a/reasonably/long/host/mount/path/here";

        printf("  VULNERABLE dirfs_nrename (no NULL check, lines 977-981):\n");
        int v_err = vulnerable_nrename(dm_path, fdnp, "src.txt", tdnp, "dst.txt");
        printf("    returned error: %d (%s)\n", v_err, strerror(v_err));
        printf("    => EFAULT (wrong error; should be ENAMETOOLONG), but NO PANIC\n\n");

        /* Re-create the chain (vulnerable_nrename freed the pathfree bufs,
           but they were NULL so nothing was freed; nodes are still valid) */

        printf("  FIXED dirfs_nrename (NULL check -> ENAMETOOLONG):\n");
        int f_err = fixed_nrename(dm_path, fdnp, "src.txt", tdnp, "dst.txt");
        printf("    returned error: %d (%s)\n", f_err, strerror(f_err));
        printf("    => ENAMETOOLONG (correct POSIX error)\n\n");

        free_chain_dynamic(fdnp);
    }

    /* ---- Part 4: unlinked-parent scenario ---- */
    printf("--- Part 4: unlinked-parent scenario (dnp1==NULL after loop) ---\n\n");
    {
        /* A single node with no parent and not root โ€” simulates unlinked dir */
        struct dirfs_node unlinked;
        unlinked.dn_name = "orphan";
        unlinked.dn_namelen = 6;
        unlinked.dn_isroot = 0;
        unlinked.dn_parent = NULL;  /* parent was unlinked */

        char *path, *pathfree;
        path = transcribed_absolute_path_plus("/mnt/dirfs", &unlinked, "file.txt", &pathfree);
        printf("  Unlinked parent test:\n");
        printf("    dirfs_node_absolute_path_plus returns: %s\n",
               path ? path : "NULL  <-- parent chain broken, returns NULL");
        printf("    => rename(NULL,...) would return EFAULT (not panic)\n\n");
        if (pathfree) free(pathfree);
    }

    printf("=== SUMMARY ===\n");
    printf("1. dirfs_node_absolute_path_plus CAN return NULL (over-length path or\n");
    printf("   unlinked parent) โ€” CONFIRMED by faithful code transcription.\n");
    printf("2. dirfs_nrename does NOT check for NULL before calling rename() โ€” CONFIRMED.\n");
    printf("3. rename(NULL,...) returns EFAULT (errno 14), NOT a segfault/panic โ€”\n");
    printf("   PROVEN by live call on the guest kernel.\n");
    printf("4. The finding's claim of 'NULL deref panic' is a FALSE POSITIVE.\n");
    printf("   Actual impact: rename returns EFAULT instead of ENAMETOOLONG\n");
    printf("   (POSIX correctness bug, not a security panic/DoS).\n");
    printf("5. The fix (NULL check -> ENAMETOOLONG) is still correct for robustness.\n");

    return 0;
}