DF-0888 / 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | /* * 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; } |