DragonFlyBSD Kernel Audit
DF-0888 / harness.c
← back to finding ↓ download raw
/*
 * DF-0888 deterministic harness.
 *
 * This userspace program transcribes the EXACT arithmetic of
 *   sys/vfs/ext2fs/ext2_inode.c: ext2_ind_truncate()  (lines 226-445)
 *   sys/vfs/ext2fs/ext2_inode.c: ext2_indirtrunc()   (lines 118-219)
 * verbatim, using the real DragonFly constants, and shows — without ever
 * touching the kernel — that:
 *
 *   1. ext2_truncate() has NO structural-limit guard on `length`:
 *        sys/vfs/ext2fs/ext2_inode.c:465  only checks (length < 0)
 *      (the only maxfilesize check at :251 is on the LENGTHEN branch
 *       `if (osize < length)` and uses e2fs_maxfilesize which is
 *       INT64_MAX under HUGE_FILE -- sys/vfs/ext2fs/ext2_vfsops.c:703).
 *   2. For length > ~4.4 TB (triple-indirect capacity, 4 KiB block),
 *      lastiblock[TRIPLE] computed at :313 EXCEEDS NINDIR(fs)^3.
 *   3. ext2_indirtrunc(level=TRIPLE) at :138-140 computes
 *        last = lastbn / factor   (factor = NINDIR^2)
 *      which EXCEEDS NINDR(fs)-1.
 *   4. The bzero at :172-173
 *        bzero(&bap[last+1], (NINDIR(fs)-(last+1)) * sizeof(e2fs_daddr_t))
 *      underflows the (size_t) length to ~2^64 -- a heap OOB write of
 *      effectively-unbounded extent past bp->b_data (a 4096-byte buffer),
 *      AND the &bap[last+1] start address is already past the end of bap[].
 *
 * The harness mimics a poisoned 4096-byte "buffer head" and proves the
 * pointer & start/extent are out of bounds. It exits non-zero iff the bug
 * fires (so build.sh / run.sh can assert on rc).
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */

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

/* ---- DragonFly ext2 constants (sys/vfs/ext2fs/inode.h, fs.h) ---- */
#define EXT2_NDADDR    12     /* inode.h:55 */
#define EXT2_NIADDR    3      /* inode.h:56 */
#define SINGLE  0
#define DOUBLE  1
#define TRIPLE  2

struct m_ext2fs_sim {
    int   e2fs_bsize;          /* block size, bytes */
    int   addr_per_block;      /* EXT2_ADDR_PER_BLOCK = bsize/4 */
};
#define NINDIR(fs)  ((fs)->addr_per_block)
#define lblkno(fs, off)   ((off) / (fs)->e2fs_bsize)
#define blkoff(fs, off)   ((off) % (fs)->e2fs_bsize)

/*
 * Verbatim transcription of ext2_indirtrunc (lines 118-219), restricted to
 * the OOB-relevant computations. We pass the buffer head (b_data) and report
 * where the bzero would start + how many bytes it would touch.
 */
struct oob_report {
    int       level;
    long long lastbn;        /* lastiblock[TRIPLE] passed in */
    long long factor;
    long long last;          /* after lastbn/factor */
    long long nindir;
    long long bap_entries;   /* = NINDIR */
    long long bzero_idx;     /* last+1   -- index into bap[] */
    int       start_oob;     /* &bap[last+1] beyond bap[NINDIR-1]? */
    unsigned long long bzero_len;  /* (NINDIR-(last+1))*sizeof(daddr_t) */
    int       len_underflow; /* size_t underflow? */
};

static void indirtrunc_analyze(struct m_ext2fs_sim *fs, int level,
                               long long lastbn, struct oob_report *r)
{
    long long factor = 1;
    int i;
    /* lines 135-137: factor = NINDR^level */
    for (i = SINGLE; i < level; i++)
        factor *= NINDIR(fs);
    /* lines 138-140 */
    long long last = lastbn;
    if (lastbn > 0)
        last /= factor;

    r->level       = level;
    r->lastbn      = lastbn;
    r->factor      = factor;
    r->last        = last;
    r->nindir      = NINDIR(fs);
    r->bap_entries = NINDIR(fs);
    r->bzero_idx   = last + 1;
    /* &bap[last+1] is OOB if (last+1) >= NINDR, i.e. last > NINDR-1 */
    r->start_oob   = (last + 1) > NINDIR(fs);   /* also covers last < -1 */
    /* size_t arithmetic (64-bit). (NINDR - (last+1)) * 4 */
    long long raw = NINDIR(fs) - (last + 1);
    unsigned long long ulen;
    if (raw < 0) {
        /* signed negative reinterpreted as unsigned -- underflow */
        ulen = (unsigned long long)(long long)raw * sizeof(uint32_t);
        r->len_underflow = 1;
    } else {
        ulen = (unsigned long long)raw * sizeof(uint32_t);
        r->len_underflow = 0;
    }
    r->bzero_len = ulen;
}

int main(int argc, char **argv)
{
    /* 4 KiB blocks: NINDR = 1024 -- the DragonFly default for bs=4096. */
    struct m_ext2fs_sim fs;
    fs.e2fs_bsize = 4096;
    fs.addr_per_block = fs.e2fs_bsize / 4;   /* 1024 */

    /* The on-disk inode claims i_size = 6 TiB; we ftruncate to 5 TiB.
     * Both are in the triple-indirect range, so osize > length -> SHORTEN
     * path of ext2_ind_truncate (no maxfilesize guard there). */
    long long bsize = fs.e2fs_bsize;
    long long TB = 1LL << 40;
    long long osize = 6 * TB;     /* crafted on-disk i_size */
    long long length = 5 * TB;    /* ftruncate target */

    printf("== DF-0888 deterministic arithmetic harness ==\n");
    printf("block_size      = %lld bytes\n", bsize);
    printf("NINDR(fs)       = %lld  (max indirect-block pointers per block)\n",
           (long long)NINDIR(&fs));
    printf("triple-indirect capacity (NINDR^3 blocks) = %lld\n",
           (long long)NINDIR(&fs) * NINDIR(&fs) * NINDIR(&fs));
    printf("triple-indirect max file bytes            = %lld (~%.1f TiB)\n",
           ((long long)EXT2_NDADDR
            + NINDIR(&fs)
            + NINDIR(&fs)*NINDIR(&fs)
            + NINDIR(&fs)*NINDIR(&fs)*NINDIR(&fs)) * bsize,
           ((double)((long long)EXT2_NDADDR
            + NINDIR(&fs)
            + NINDIR(&fs)*NINDIR(&fs)
            + NINDIR(&fs)*NINDIR(&fs)*NINDIR(&fs)) * bsize) / (double)(1LL<<40));
    printf("on-disk i_size  = %lld (~%.1f TiB)\n", osize, (double)osize/TB);
    printf("ftruncate length= %lld (~%.1f TiB)\n", length, (double)length/TB);
    printf("\n");

    if (length < 0) { printf("length<0 -> EINVAL (NOT our case)\n"); }
    /* ext2_truncate line 465 only checks length<0. NO structural check.
     * Falls through to ext2_ind_truncate. osize(6TiB) > length(5TiB) ->
     * SHORTEN path (the maxfilesize guard at :251 is on the LENGTHEN branch). */

    /* ext2_ind_truncate lines 310-313 */
    long long lastblock = lblkno(&fs, length + bsize - 1) - 1;
    long long lastiblock_single = lastblock - EXT2_NDADDR;
    long long lastiblock_double = lastiblock_single - NINDIR(&fs);
    long long lastiblock_triple = lastiblock_double - NINDIR(&fs)*NINDIR(&fs);

    printf("ext2_ind_truncate arithmetic (lines 310-313):\n");
    printf("  lastblock        = %lld\n", lastblock);
    printf("  lastiblock[SINGLE] = %lld\n", lastiblock_single);
    printf("  lastiblock[DOUBLE] = %lld\n", lastiblock_double);
    printf("  lastiblock[TRIPLE] = %lld\n", lastiblock_triple);
    long long triple_cap = NINDIR(&fs)*NINDIR(&fs)*NINDIR(&fs);
    printf("  NINDR^3 (max TRIPLE indices) = %lld\n", triple_cap);
    printf("  lastiblock[TRIPLE] > NINDR^3 ?  %s\n",
           lastiblock_triple > triple_cap ? "YES -> OOB" : "no");
    printf("\n");

    /* lines 321-327: lastiblock[level] normalized to -1 only if < 0.
     * For length in triple range, lastiblock[TRIPLE] is HUGE POSITIVE -> NOT normalized. */
    if (lastiblock_triple >= 0)
        printf("lastiblock[TRIPLE] >= 0 -> stays huge positive (NOT normalized to -1)\n");

    /* line 362-366: for level=TRIPLE, if i_ib[TRIPLE] != 0, call
     *   ext2_indirtrunc(ip, indir_lbn[TRIPLE], dbn, lastiblock[TRIPLE], TRIPLE, ...) */
    printf("\nAssuming i_block[14] (i_ib[TRIPLE]) is non-zero -> ext2_indirtrunc(level=TRIPLE)\n");
    struct oob_report r;
    indirtrunc_analyze(&fs, TRIPLE, lastiblock_triple, &r);

    printf("\next2_indirtrunc(level=TRIPLE, lastbn=lastiblock[TRIPLE]):\n");
    printf("  factor (NINDR^2)              = %lld\n", r.factor);
    printf("  last = lastbn / factor        = %lld\n", r.last);
    printf("  NINDR(fs)                     = %lld\n", r.nindir);
    printf("  NINDR(fs)-1 (max valid index) = %lld\n", r.nindir - 1);
    printf("  last > NINDR-1 ?              = %s\n",
           r.last > (r.nindir - 1) ? "YES -> OOB index" : "no");
    printf("\n  bzero start: &bap[%lld]  (bap has %lld entries, valid idx 0..%lld)\n",
           r.bzero_idx, r.bap_entries, r.bap_entries - 1);
    printf("  start OOB ?                   = %s\n", r.start_oob ? "YES" : "no");
    printf("  bzero len = (NINDR-(last+1))*4 = %llu bytes (size_t)\n", r.bzero_len);
    printf("  len underflow ?               = %s\n",
           r.len_underflow ? "YES -> ~2^64 byte bzero (heap OOB write)" : "no");
    printf("  bp->b_data buffer size        = %d bytes (fs->e2fs_bsize)\n", fs.e2fs_bsize);

    /* The bap[] array lives inside bp->b_data (4096 bytes). last+1 >= NINDR
     * means &bap[last+1] points PAST the end of the 4096-byte buffer, and the
     * underflowed size_t means the bzero runs for ~2^64 bytes -> immediate
     * page fault / heap corruption in the kernel. */

    int bug_fires = (r.start_oob || r.len_underflow);
    printf("\n== RESULT: %s ==\n",
           bug_fires ? "BUG CONFIRMED -- OOB heap bzero in ext2_indirtrunc"
                     : "no bug (arithmetic in bounds)");
    return bug_fires ? 2 : 0;
}