DF-0803 / harness.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | /* * 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; } |