โฌข DragonFlyBSD Kernel Audit
DF-0803 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0803 โ€” deterministic harness transcribing the kernel's exact arithmetic
 * for e2fs_gcount (ext2_vfsops.c:620-626) and the downstream
 * malloc/validate/vget-deref flow. No kernel required โ€” this proves the
 * truncation bug is real and characterizes the primitive on the same logic
 * the kernel runs.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

/* Verbatim transcription of the relevant kernel types / macros ------------- */
typedef uint32_t u32;
typedef uint64_t u64;

/* sys/sys/param.h:397 */
#define howmany(x, y)  (((x) + ((y) - 1)) / (y))

/* In-memory superblock layout (ext2fs.h:154-187), only what matters here. */
struct m_ext2fs {
    u64 e2fs_bcount;     /* :159 */
    u32 e2fs_bsize;      /* :163 */
    u32 e2fs_bpg;        /* :165 */
    u32 e2fs_ipg;        /* :168 */
    u32 e2fs_itpg;       /* :170 */
    u32 e2fs_gdbcount;   /* :174 */
    u32 e2fs_gcount;     /* :175 โ€” uint32, the truncation sink */
    /* ... */
};

#define EXT2F_INCOMPAT_64BIT  0x0080
#define E2FS_64BIT_GD_SIZE    64
#define EXT2_DESCS_PER_BLOCK_64BIT(bsize)  ((bsize) / E2FS_64BIT_GD_SIZE)

/* ino_to_cg (fs.h:108) and the e2fs_gd[] deref that ext2_vget performs. */
#define ino_to_cg(ipg, ino)  (((ino) - 1) / (ipg))

/* Reproduce malloc(0) behaviour: kern_slaballoc.c:888-890 returns -8. */
#define ZERO_LENGTH_PTR  ((void *)(intptr_t)-8)

static const char *simulate_mount(const char *variant, u64 bcount64,
                                  u32 bsize, u32 bpg, u32 first_dblock,
                                  u32 ipg, u32 feat_incompat, u32 ino_root)
{
    static char buf[512];
    struct m_ext2fs fs = {0};
    fs.e2fs_bcount = bcount64;
    fs.e2fs_bsize  = bsize;
    fs.e2fs_bpg    = bpg;
    fs.e2fs_ipg    = ipg;
    fs.e2fs_itpg   = ipg / (bsize / 256);  /* ipb = bsize/isize=256 */
    int has_64bit  = (feat_incompat & EXT2F_INCOMPAT_64BIT) != 0;

    /* ext2_vfsops.c:620-626 โ€” verbatim. */
    fs.e2fs_gcount = howmany(fs.e2fs_bcount - first_dblock,
                             fs.e2fs_bpg);
    /* ^^^ this is the bug: howmany returns u64, e2fs_gcount is u32 -> truncate */

    u64 true_gcount = howmany(fs.e2fs_bcount - first_dblock, fs.e2fs_bpg);

    /* ext2_vfsops.c:622-626 โ€” verbatim (post-truncation check). */
    u32 descpb = has_64bit
        ? EXT2_DESCS_PER_BLOCK_64BIT(fs.e2fs_bsize)
        : fs.e2fs_bsize / 32;
    int check_trips = (fs.e2fs_gcount > ((u64)1 << 32) - descpb);

    /* ext2_vfsops.c:638-648 โ€” allocation sizing (uses truncated gcount). */
    u32 e2fs_descpb = has_64bit
        ? fs.e2fs_bsize / E2FS_64BIT_GD_SIZE
        : fs.e2fs_bsize / 32;
    u32 gdbcount_alloc = howmany(fs.e2fs_gcount, e2fs_descpb);
    fs.e2fs_gdbcount   = howmany(fs.e2fs_gcount, e2fs_descpb);

    /* malloc(0) -> ZERO_LENGTH_PTR per kern_slaballoc.c:888-890. */
    size_t gd_alloc = (size_t)gdbcount_alloc * fs.e2fs_bsize;
    void *e2fs_gd   = (gd_alloc == 0) ? ZERO_LENGTH_PTR : malloc(gd_alloc);

    /* ext2_cg_validate (ext2_vfsops.c:366-451) iterates i < e2fs_gcount. */
    u32 cg_validated = 0;
    for (u32 i = 0; i < fs.e2fs_gcount; i++) cg_validated++;

    /* ext2_vget(EXT2_ROOTINO=2) derefs e2fs_gd[ino_to_cg(fs, ino)]. */
    u32 cg_of_root = ino_to_cg(fs.e2fs_ipg, ino_root);
    int deref_oob  = (cg_of_root >= gdbcount_alloc * descpb);
    /* With gcount=0, e2fs_gd is ZERO_LENGTH_PTR; any index derefs (-8). */
    int deref_badptr = (e2fs_gd == ZERO_LENGTH_PTR);

    snprintf(buf, sizeof(buf),
        "[%s] bcount=%#llx bsize=%u bpg=%u first_dblock=%u 64bit=%d\n"
        "  true howmany(bcount-fdb, bpg)   = %llu (%#llx)\n"
        "  stored e2fs_gcount (u32)        = %u  (truncation!)\n"
        "  post-trunc check threshold      = %llu\n"
        "  post-trunc check trips?         = %s\n"
        "  gdbcount_alloc (gcount dep.)    = %u\n"
        "  e2fs_gd alloc size              = %zu  (ptr=%p)\n"
        "  ext2_cg_validate iterations     = %u\n"
        "  root inode=%u -> cg=%u  (deref e2fs_gd[%u])\n"
        "  deref result                    = %s\n",
        variant, (unsigned long long)bcount64, bsize, bpg, first_dblock,
        has_64bit,
        (unsigned long long)true_gcount, (unsigned long long)true_gcount,
        fs.e2fs_gcount,
        (unsigned long long)(((u64)1 << 32) - descpb),
        check_trips ? "YES (mount rejected)" : "NO (mount proceeds)",
        gdbcount_alloc,
        gd_alloc, e2fs_gd,
        cg_validated,
        ino_root, cg_of_root, cg_of_root,
        deref_badptr ? "PANIC (ZERO_LENGTH_PTR deref, addr -8)"
                     : (deref_oob ? "OOB read/write of e2fs_gd[cg]"
                                  : "in-bounds, attacker-controlled GD entry"));

    if (e2fs_gd != ZERO_LENGTH_PTR) free(e2fs_gd);
    return buf;
}

int main(void)
{
    /* Match the values craft_img.py bakes into df0803_g0.img / _g1.img. */
    u32 bsize = 4096, bpg = bsize * 8, first_dblock = 0, ipg = 16;
    u32 feat_incompat = EXT2F_INCOMPAT_64BIT;
    u32 ino_root = 2;  /* EXT2_ROOTINO */

    printf("=== DF-0803 e2fs_gcount truncation harness ===\n\n");

    /* gcount = 0 variant: howmany wraps to 2^32, truncated to 0. */
    u64 bcount0 = (u64)1 << 47;     /* 2^32 * bpg = 2^47 */
    printf("%s\n", simulate_mount("gcount=0", bcount0, bsize, bpg,
                                  first_dblock, ipg, feat_incompat, ino_root));

    /* gcount = 1 variant: howmany wraps to 2^32 + 1, truncated to 1. */
    u64 bcount1 = ((u64)1 << 47) + 1;
    printf("%s\n", simulate_mount("gcount=1", bcount1, bsize, bpg,
                                  first_dblock, ipg, feat_incompat, ino_root));

    /* gcount = 64 variant: fills the 4096-byte GD alloc exactly. */
    u64 bcount64 = ((u64)1 << 47) + 64 * bpg;
    printf("%s\n", simulate_mount("gcount=64", bcount64, bsize, bpg,
                                  first_dblock, ipg, feat_incompat, ino_root));

    /* gcount = 65 variant: would need cg=65 -> OOB e2fs_gd[65]. */
    u64 bcount65 = ((u64)1 << 47) + 65 * bpg;
    printf("%s\n", simulate_mount("gcount=65", bcount65, bsize, bpg,
                                  first_dblock, ipg, feat_incompat, ino_root));

    return 0;
}