DragonFlyBSD Kernel Audit
DF-0771 / df0771_harness.c
← back to finding ↓ download raw
/*
 * DF-0771 — deterministic code-level harness reproducing the
 * unvalidated-data_len logic in hammer_get_inode().
 *
 * Cited bug: sys/vfs/hammer/hammer_inode.c:524-525
 *     ip->ino_leaf = cursor.node->ondisk->elms[cursor.index].leaf;
 *     ip->ino_data = cursor.data->inode;          <-- 128-byte struct copy
 *
 * cursor.data is set by hammer_btree_extract() (hammer_btree.c:737) to
 *     cursor->data = hammer_bread_ext(hmp, data_off, data_len, ...);
 * which returns (char *)buffer->ondisk + xoff  where xoff = data_off & 0x3FFF.
 * The ONLY length validation anywhere on the path is (hammer_btree.c:734)
 *     KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE);   // <= 65536
 * i.e. data_len is NEVER checked against sizeof(struct hammer_inode_data).
 *
 * The CRC gate that *should* catch a wrong-sized inode record is bypassable:
 *   hammer_crc.h:267-271  hammer_crc_get_leaf():
 *       case HAMMER_RECTYPE_INODE:
 *           if (leaf->data_len != sizeof(struct hammer_inode_data))
 *               return(0);   // <-- returns 0 for a wrong-sized inode
 *   hammer_crc.h:294-296  hammer_crc_test_leaf():
 *       if (leaf->data_crc == hammer_crc_get_leaf(...))   // on-disk 0 == 0
 *           return(1);      // <-- CRC PASSES
 *
 * So a crafted image whose root-inode B-Tree leaf has
 *     data_len = 1          (anything != 128)
 *     data_crc = 0
 * sails through the CRC test, and the 128-byte struct copy at line 525 then
 * reads 128 bytes starting at cursor.data — which, if the on-disk
 * data_offset's within-buffer xoff is > (HAMMER_BUFSIZE - 128) = 16256,
 * reads PAST the 16 KiB buffer into adjacent kernel heap. The leaked bytes
 * populate ip->ino_data.{mode, uid, gid, size, ext.symlink[24], mtime, atime},
 * which are returned to userspace via stat()/readlink()  =>  kernel heap
 * info leak (KASLR-defeat class). Requires mount capability
 * (root/operator/removable media).
 *
 * This harness reproduces BOTH halves of the bug with the real kernel
 * structs and the verbatim CRC inline logic, without needing a crafted
 * HAMMER image. crc32/iscsi_crc32 are stubbed because the bypass path
 * returns 0 before ever calling them (that is precisely the defect).
 */

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

typedef uint32_t hammer_crc_t;
typedef uint64_t hammer_off_t;
typedef uint64_t hammer_tid_t;

/* ---- copied verbatim from sys/vfs/hammer/hammer_disk.h ---- */
#define HAMMER_RECTYPE_INODE 0x0001

typedef char hammer_uuid_t[16];

typedef struct hammer_inode_data {
    uint16_t version;
    uint16_t mode;
    uint32_t uflags;
    uint32_t rmajor;
    uint32_t rminor;
    uint64_t ctime;
    int64_t parent_obj_id;
    hammer_uuid_t uid;
    hammer_uuid_t gid;
    uint8_t obj_type;
    uint8_t cap_flags;
    uint16_t reserved01;
    uint32_t reserved02;
    uint64_t nlinks;
    uint64_t size;
    union { char symlink[24]; } ext;
    uint64_t mtime;
    uint64_t atime;
} *hammer_inode_data_t;

#define HAMMER_INODE_CRCSIZE (offsetof(struct hammer_inode_data, mtime))

/* ---- copied verbatim from sys/vfs/hammer/hammer_btree.h ---- */
typedef struct hammer_base_elm {
    int64_t obj_id;
    int64_t key;
    hammer_tid_t create_tid;
    hammer_tid_t delete_tid;
    uint16_t rec_type;
    uint8_t obj_type;
    uint8_t btype;
    uint32_t localization;
} *hammer_base_elm_t;

typedef struct hammer_btree_leaf_elm {
    struct hammer_base_elm base;
    uint32_t created_ts;
    uint32_t deleted_ts;
    hammer_off_t data_offset;
    int32_t data_len;
    hammer_crc_t data_crc;
} *hammer_btree_leaf_elm_t;

/* ---- crc32 / iscsi_crc32 stubs: NOT reached on the bypass path ---- */
static hammer_crc_t crc32(const void *buf, size_t n) { (void)buf; (void)n; return 0xDEAD; }
static hammer_crc_t iscsi_crc32(const void *buf, size_t n) { (void)buf; (void)n; return 0xBEEF; }

#define HAMMER_VOL_VERSION_SEVEN 7
#define HAMMER_VOL_VERSION_SIX   6

static hammer_crc_t hammer_datacrc(uint32_t vol_version, const void *buf, size_t size) {
    return (vol_version >= HAMMER_VOL_VERSION_SEVEN) ?
        iscsi_crc32(buf, size) : crc32(buf, size);
}

/* ---- copied verbatim from sys/vfs/hammer/hammer_crc.h:263-274 ---- */
static hammer_crc_t
hammer_crc_get_leaf(uint32_t vol_version, const void *data, hammer_btree_leaf_elm_t leaf)
{
    hammer_crc_t crc;
    if (leaf->data_len == 0)
        return(0);
    switch(leaf->base.rec_type) {
    case HAMMER_RECTYPE_INODE:
        if (leaf->data_len != sizeof(struct hammer_inode_data))
            return(0);  /* "This shouldn't happen" -- but it does for a crafted image */
        crc = hammer_datacrc(vol_version, data, HAMMER_INODE_CRCSIZE);
        break;
    default:
        crc = hammer_datacrc(vol_version, data, leaf->data_len);
        break;
    }
    return(crc);
}

/* ---- copied verbatim from sys/vfs/hammer/hammer_crc.h:293-302 ---- */
static int
hammer_crc_test_leaf(uint32_t vol_version, const void *data, hammer_btree_leaf_elm_t leaf)
{
    if (leaf->data_crc == hammer_crc_get_leaf(vol_version, data, leaf))
        return(1);
    if (vol_version >= HAMMER_VOL_VERSION_SEVEN) {
        if (leaf->data_crc == hammer_crc_get_leaf(HAMMER_VOL_VERSION_SIX, data, leaf))
            return(1);
    }
    return(0);
}

/* ---- simulate hammer_get_inode lines 523-525 ---- */
struct hammer_inode_mem {
    struct hammer_btree_leaf_elm ino_leaf;
    struct hammer_inode_data ino_data;
};

static void load_inode_like_kernel(struct hammer_inode_mem *ip,
                                   hammer_btree_leaf_elm_t leaf,
                                   const void *cursor_data)
{
    /* hammer_inode.c:524-525 */
    ip->ino_leaf = *leaf;                 /* struct copy of leaf */
    ip->ino_data = *(hammer_inode_data_t)cursor_data;  /* 128-byte struct copy */
}

int main(void)
{
    printf("DF-0771 harness: sizeof(struct hammer_inode_data) = %zu\n",
           sizeof(struct hammer_inode_data));

    /* ============================================================
     * TEST 1: CRC gate is bypassable for a wrong-sized INODE record.
     * ============================================================ */
    struct hammer_btree_leaf_elm bad_leaf;
    memset(&bad_leaf, 0, sizeof(bad_leaf));
    bad_leaf.base.rec_type = HAMMER_RECTYPE_INODE;
    bad_leaf.data_len   = 1;     /* NOT sizeof(inode_data) (128) */
    bad_leaf.data_crc   = 0;     /* crafted on-disk CRC = 0 */
    bad_leaf.data_offset= 0;

    int crc_ok_v6 = hammer_crc_test_leaf(HAMMER_VOL_VERSION_SIX,   (void*)"x", &bad_leaf);
    int crc_ok_v7 = hammer_crc_test_leaf(HAMMER_VOL_VERSION_SEVEN, (void*)"x", &bad_leaf);
    printf("CRC test for INODE leaf with data_len=1, data_crc=0:\n");
    printf("   vol_version=6 -> %s (1=CRC PASSES = inode loaded)\n", crc_ok_v6 ? "PASS" : "FAIL");
    printf("   vol_version=7 -> %s (1=CRC PASSES = inode loaded)\n", crc_ok_v7 ? "PASS" : "FAIL");

    if (crc_ok_v6 && crc_ok_v7) {
        printf("BUG PART 1 CONFIRMED: CRC gate bypassed -- wrong-sized INODE record\n"
               "   is accepted because hammer_crc_get_leaf() returns 0 for INODE with\n"
               "   data_len!=sizeof(inode_data), and on-disk data_crc=0 matches 0==0.\n");
    } else {
        printf("CRC gate NOT bypassed -- finding would be a false positive on CRC half.\n");
    }

    /* ============================================================
     * TEST 2: struct copy reads past the 16 KiB HAMMER data buffer
     * when data_offset's xoff > (16384 - 128) = 16256.
     *
     * hammer_bread_ext() returns buffer->ondisk + xoff. The struct
     * copy at hammer_inode.c:525 then reads 128 bytes unconditionally.
     * ============================================================ */
#define HAMMER_BUFSIZE 16384
    /* Allocate one big region, treat first 16 KiB as the "HAMMER data buffer"
       and fill the bytes just past it with a recognizable leak marker so we
       can see the OOB read picking them up. */
    size_t total = HAMMER_BUFSIZE + 256;            /* 16 KiB buffer + adjacent-heap stand-in */
    unsigned char *big = calloc(1, total);
    /* fill the "HAMMER buffer" region with 0x11, the "adjacent heap" with 0x77 */
    memset(big,                        0x11, HAMMER_BUFSIZE);
    memset(big + HAMMER_BUFSIZE,       0x77, 256);

    /* attacker-controlled data_offset with xoff = 16280 (within the 16 KiB
       buffer: only 16384-16280 = 104 bytes remain; struct copy needs 128) */
    int xoff = 16280;
    unsigned char *cursor_data = big + xoff;        /* == what hammer_bread_ext returns */

    struct hammer_inode_mem ip;
    load_inode_like_kernel(&ip, &bad_leaf, cursor_data);

    /* The struct copy read bytes [xoff .. xoff+128).
       Bytes [HAMMER_BUFSIZE .. xoff+128) are PAST the buffer. */
    int oob_start = HAMMER_BUFSIZE;                 /* first byte past buffer */
    int oob_end   = xoff + (int)sizeof(struct hammer_inode_data);
    int oob_bytes = (oob_end > oob_start) ? (oob_end - oob_start) : 0;

    printf("\nTEST 2: xoff=%d, struct copy reads 128 bytes at [%d..%d)\n",
           xoff, xoff, xoff + (int)sizeof(struct hammer_inode_data));
    printf("   HAMMER buffer occupies [0..%d). OOB region = [%d..%d) = %d bytes past buffer end.\n",
           HAMMER_BUFSIZE, oob_start, oob_end, oob_bytes);

    /* Show that the copied inode fields contain bytes from PAST the buffer
       (the 0x77 marker from the adjacent-heap stand-in). The tail of the
       inode struct (mtime/atime = last 16 bytes) is the part that lands in
       the OOB region for xoff=16280. */
    unsigned char *ino_bytes = (unsigned char *)&ip.ino_data;
    int leaked_77 = 0;
    for (int i = 0; i < (int)sizeof(struct hammer_inode_data); i++) {
        if (ino_bytes[i] == 0x77) leaked_77++;
    }
    printf("   inode_data bytes copied from PAST the 16 KiB buffer (leaked 0x77 marker): %d\n",
           leaked_77);
    printf("   inode_data.atime (last 8 bytes, all in OOB region) hex:");
    for (int i = sizeof(struct hammer_inode_data) - 8; i < (int)sizeof(struct hammer_inode_data); i++)
        printf(" %02x", ino_bytes[i]);
    printf("\n");

    if (oob_bytes > 0 && leaked_77 > 0) {
        printf("BUG PART 2 CONFIRMED: struct copy at hammer_inode.c:525 read %d bytes\n"
               "   past the 16 KiB HAMMER data buffer end into adjacent kernel heap.\n"
               "   Those leaked bytes populate inode_data fields (mode/uid/gid/size/\n"
               "   ext.symlink[24]/mtime/atime) returned to userspace via stat()/readlink().\n",
               oob_bytes);
    }

    /* ============================================================
     * VERDICT
     * ============================================================ */
    int bug_repro = (crc_ok_v6 && crc_ok_v7 && oob_bytes > 0 && leaked_77 > 0);
    printf("\n==== DF-0771 %s ====\n", bug_repro ? "REPRODUCED (info-leak / heap OOB read)"
                                                  : "NOT REPRODUCED");
    printf("Threat model: crafted HAMMER filesystem image mounted by root/operator/\n"
           "  removable media. Realistic impact ceiling: kernel heap info leak\n"
           "  (KASLR-defeat class); NOT a write primitive -> no privilege-escalation\n"
           "  chain derivable (read-only primitive = valid Phase-6 stop).\n");

    free(big);
    return bug_repro ? 0 : 1;
}