DF-0859 / 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 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 | /* * DF-0859 โ Deterministic harness for missing ab_busycnt validation in * hpfs_splitalsec() / hpfs_concatalsec() / hpfs_alblk2alsec() * (sys/vfs/hpfs/hpfs_alsubr.c:225/229, :271, :314). * * These three helper routines are reached from the HPFS WRITE / TRUNCATE * path (hpfs_addextent โ hpfs_alblk2alsec / hpfs_splitalsec, * hpfs_truncatealblk โ hpfs_concatalsec). Their bcopy length arithmetic * takes ab_busycnt verbatim from the on-disk alblk_t (u_int8_t โ never * validated) and uses it to size both the source offset and the byte count * of a bcopy into a freshly allocated 512-byte alsec buffer. * * This harness is a FAITHFUL userspace transcription of the three bcopys * against the EXACT on-disk struct layouts from sys/vfs/hpfs/hpfs.h: * * alblk_t = 8 bytes (ab_flag, ab_res[3], ab_freecnt, ab_busycnt, ab_freeoff) * alleaf_t = 12 bytes (al_off, al_len, al_lsn) * alnode_t = 8 bytes (an_nextoff, an_lsn) * fnode.fn_abd[0x60] = 96-byte data area * alsec.as_abd[0x1E0] = 480-byte data area * * Container layouts: * - struct alsec is 4+4+4 (as_magic, as_self, as_parent) + 8 (as_ab) + * 0x1E0 (as_abd) = 500 bytes, allocated inside a DEV_BSIZE=512 buffer * (getblk in hpfs_allocalsec, sys/vfs/hpfs/hpfs_alsubr.c:178). * - The fnode fn_ab lives inside struct hpfsnode (kmalloc'd in * hpfs_vfsops.c:488). Its data area fn_abd is 96 bytes; the source * bcopy in hpfs_alblk2alsec reads straight from fn_ab. * * Poisoned allocator: the destination container is placed at the END of the * first mmap'd page; the next page is poisoned with 0xAA. Any byte written * past the container's data area lands in the 0xAA page, modelling a slab * neighbour the attacker does not own. * * Compile: cc -O2 -o harness harness.c * Run: ./harness * * Expected (BUG PRESENT): * [BUG] alblk2alsec busycnt=255 sz=12 (alleaf) WRITTEN=3068B OOB past alsec buffer=2568B poison=HIT * [BUG] alblk2alsec busycnt=255 sz=8 (alnode) WRITTEN=2048B OOB past alsec buffer=1548B poison=HIT * [BUG] splitalsec busycnt=255 sz=12 (alleaf) WRITTEN=1524B OOB past alsec buffer=1032B poison=HIT * [BUG] splitalsec busycnt=255 sz=8 (alnode) WRITTEN=1016B OOB past alsec buffer=524B poison=HIT * [BUG] concatalsec busycnt=255 sz=12 (alleaf) WRITTEN=3060B OOB past alsec buffer=2568B poison=HIT * [BUG] concatalsec busycnt=255 sz=8 (alnode) WRITTEN=2040B OOB past alsec buffer=1548B poison=HIT * * Expected (FIXED): all six cases โ REJECTED (EINVAL, no bcopy run). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <sys/mman.h> #include <unistd.h> /* ---- Exact struct layouts from sys/vfs/hpfs/hpfs.h --------------------- */ #define AB_NODES 0x80 #define AB_FNPARENT 0x20 #define AS_MAGIC 0x37E40AAE typedef struct alblk { uint8_t ab_flag; uint8_t ab_res[3]; uint8_t ab_freecnt; uint8_t ab_busycnt; /* <-- attacker-controlled, UNVALIDATED */ uint16_t ab_freeoff; } alblk_t; /* sizeof = 8 */ typedef struct alleaf { uint32_t al_off; uint32_t al_len; uint32_t al_lsn; } alleaf_t; /* sizeof = 12 */ typedef struct alnode { uint32_t an_nextoff; uint32_t an_lsn; } alnode_t; /* sizeof = 8 */ /* fnode alloc-block data area (96 bytes) */ #define FN_ABD_SIZE 0x60 /* alsec alloc-block data area (480 bytes) */ #define AS_ABD_SIZE 0x1E0 /* alsec total = 12 (as_magic+as_self+as_parent) + 8 (as_ab) + 480 = 500 */ #define AS_TOTAL_SIZE (3*sizeof(uint32_t) + sizeof(alblk_t) + AS_ABD_SIZE) /* buffer cache backing for an alsec is DEV_BSIZE = 512 */ #define DEV_BSIZE 512 /* Legitimate maximum busycnt per container (data area / element size) */ #define FN_MAX_LEAF (FN_ABD_SIZE / sizeof(alleaf_t)) /* 8 */ #define FN_MAX_NODE (FN_ABD_SIZE / sizeof(alnode_t)) /* 12 */ #define AS_MAX_LEAF (AS_ABD_SIZE / sizeof(alleaf_t)) /* 40 */ #define AS_MAX_NODE (AS_ABD_SIZE / sizeof(alnode_t)) /* 60 */ /* A struct that mimics just the head of struct fnode the way the source * bcopy in hpfs_alblk2alsec sees it (ab pointer + abd data area). */ typedef struct fnode_container { alblk_t fn_ab; uint8_t fn_abd[FN_ABD_SIZE]; } fnode_container_t; typedef struct alsec_container { uint32_t as_magic; uint32_t as_self; uint32_t as_parent; alblk_t as_ab; uint8_t as_abd[AS_ABD_SIZE]; } alsec_container_t; /* sizeof = 500 */ /* ---- Poisoned allocator ----------------------------------------------- */ /* Place the container at the END of the first page; the second page is * poisoned (0xAA). Any write past the container's data area lands in the * poison page, modelling the slab-neighbour case. */ static void * poison_alloc(size_t container_size, size_t pagesz, uint8_t poison) { size_t total = pagesz * 2; void *m = mmap(NULL, total, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (m == MAP_FAILED) { perror("mmap"); exit(2); } /* poison the second page */ memset((char *)m + pagesz, poison, pagesz); /* place container so its end is exactly at the page boundary */ char *p = (char *)m + (pagesz - container_size); return p; } /* ---- BUG transcription: hpfs_alblk2alsec (sys/vfs/hpfs/hpfs_alsubr.c:297-322) * bcopy(abp, nabp, sizeof(alblk_t) + sz * abp->ab_busycnt) [line 314] * * Source = fnode fn_ab (FN_ABD_SIZE data area). Destination = new alsec * as_ab (inside the 512-byte buffer-cache buffer). Returns bytes written * past the end of the 512-byte backing buffer. */ static size_t bug_alblk2alsec(alblk_t *abp_src /* fnode fn_ab */, alsec_container_t *nasp_dst /* new alsec in 512B buffer */, size_t *written_out, int *poison_hit) { size_t sz = (abp_src->ab_flag & AB_NODES) ? sizeof(alnode_t) : sizeof(alleaf_t); alblk_t *nabp = &nasp_dst->as_ab; /* The kernel bcopy: */ size_t n = sizeof(alblk_t) + sz * abp_src->ab_busycnt; *written_out = n; /* Destination starts at &nasp_dst->as_ab (offset 12 in the 512B buffer). * The buffer ends at offset DEV_BSIZE = 512. Compute overrun. */ char *dst_start = (char *)nabp; char *buf_end = (char *)nasp_dst + DEV_BSIZE; /* emulate write: each byte from src to dst */ char *dst_cur = dst_start; *poison_hit = 0; for (size_t i = 0; i < n; i++) { if (dst_cur >= buf_end) { if (*dst_cur == 0xAA) *poison_hit = 1; } *dst_cur = 0xCC; /* mark "written" */ dst_cur++; } size_t past = (size_t)((dst_cur > buf_end) ? (dst_cur - buf_end) : 0); return past; } /* ---- BUG transcription: hpfs_splitalsec (sys/vfs/hpfs/hpfs_alsubr.c:207-245) * n1=(busycnt+1)/2; n2=busycnt-n1; sz=... * bcopy(abp + sizeof(alblk_t) + n1*sz, nabp + sizeof(alblk_t), n2*sz) [line 229] * * Source abp = asp->as_ab (alsec, 480B data area). Destination nabp = * new alsec as_ab in 512B buffer. */ static size_t bug_splitalsec(alblk_t *abp_src /* asp->as_ab */, alsec_container_t *nasp_dst /* new alsec in 512B buffer */, size_t *written_out, int *poison_hit, size_t *src_oob_out) { int n1 = (abp_src->ab_busycnt + 1) / 2; int n2 = (abp_src->ab_busycnt - n1); size_t sz = (abp_src->ab_flag & AB_NODES) ? sizeof(alnode_t) : sizeof(alleaf_t); alblk_t *nabp = &nasp_dst->as_ab; /* kernel bcopy args: */ char *src = (char *)abp_src + sizeof(alblk_t) + (size_t)n1 * sz; char *dst = (char *)nabp + sizeof(alblk_t); size_t n = (size_t)n2 * sz; *written_out = n; /* source OOB past as_abd (480 bytes data area): src+n vs abd_end */ char *abd_end = (char *)abp_src + sizeof(alblk_t) + AS_ABD_SIZE; size_t src_oob = 0; if (src + n > abd_end) src_oob = (size_t)((src + n) - abd_end); *src_oob_out = src_oob; /* destination OOB past 512B backing buffer */ char *buf_end = (char *)nasp_dst + DEV_BSIZE; char *dst_cur = dst; *poison_hit = 0; for (size_t i = 0; i < n; i++) { if (dst_cur >= buf_end) { if (*dst_cur == 0xAA) *poison_hit = 1; } *dst_cur = 0xCC; dst_cur++; } size_t past = (size_t)((dst_cur > buf_end) ? (dst_cur - buf_end) : 0); return past; } /* ---- BUG transcription: hpfs_concatalsec (sys/vfs/hpfs/hpfs_alsubr.c:257-288) * if (ab0p->ab_freecnt > ab1p->ab_busycnt) { * bcopy(AB_ALNODE(ab1p), AB_FREEANP(ab0p), ab1p->ab_busycnt * sz); [line 278] * } * * Here we model ab0p as a fresh alsec with ab_freeoff=sizeof(alblk_t) (start * of data area) and ab_freecnt=0xFF (so the guard always passes โ both * fields come from disk). Source ab1p is another alsec. The destination * is ab0p's as_abd, written from ab_freeoff onwards. */ static size_t bug_concatalsec(alblk_t *ab0p, alsec_container_t *as0_container, alblk_t *ab1p, size_t *written_out, int *poison_hit, size_t *src_oob_out) { size_t sz = (ab0p->ab_flag & AB_NODES) ? sizeof(alnode_t) : sizeof(alleaf_t); /* Caller sets ab0p->ab_freecnt and ab0p->ab_freeoff; we honour both. * The kernel guard at :271 uses these UNVALIDATED on-disk bytes. */ if (!(ab0p->ab_freecnt > ab1p->ab_busycnt)) { *written_out = 0; *poison_hit = 0; *src_oob_out = 0; return 0; } /* kernel bcopy args: * src = AB_ALNODE(ab1p) = ab1p + sizeof(alblk_t) * dst = AB_FREEANP(ab0p) = ab0p + ab0p->ab_freeoff (caller-set) * n = ab1p->ab_busycnt * sz */ char *src = (char *)ab1p + sizeof(alblk_t); char *dst = (char *)ab0p + ab0p->ab_freeoff; size_t n = (size_t)ab1p->ab_busycnt * sz; *written_out = n; /* source OOB past ab1p's as_abd */ char *abd_end = (char *)ab1p + sizeof(alblk_t) + AS_ABD_SIZE; size_t src_oob = 0; if (src + n > abd_end) src_oob = (size_t)((src + n) - abd_end); *src_oob_out = src_oob; /* destination OOB past 512B backing buffer of as0_container */ char *buf_end = (char *)as0_container + DEV_BSIZE; char *dst_cur = dst; *poison_hit = 0; for (size_t i = 0; i < n; i++) { if (dst_cur >= buf_end) { if (*dst_cur == 0xAA) *poison_hit = 1; } *dst_cur = 0xCC; dst_cur++; } size_t past = (size_t)((dst_cur > buf_end) ? (dst_cur - buf_end) : 0); return past; } /* ---- FIXED transcription: validate ab_busycnt against container max ---- */ /* Returns 0 if the (forged) busycnt would be ACCEPTED by an unfixed kernel * (i.e. it passes the bounds check), or -1 if the fix REJECTS it. For * forged=255 and any container, the fix rejects. */ static int validate(int is_fnode, uint8_t ab_flag, uint8_t busycnt) { size_t maxcnt = (ab_flag & AB_NODES) ? (is_fnode ? FN_MAX_NODE : AS_MAX_NODE) : (is_fnode ? FN_MAX_LEAF : AS_MAX_LEAF); /* mirror the proposed fix: reject when busycnt > maxcnt */ return (busycnt > maxcnt) ? -1 : 0; } int main(void) { size_t pagesz = sysconf(_SC_PAGESIZE); printf("=== DF-0859 deterministic OOB-WRITE proof ===\n"); printf("struct sizes: alblk=%zu alleaf=%zu alnode=%zu\n", sizeof(alblk_t), sizeof(alleaf_t), sizeof(alnode_t)); printf("containers : fnode fn_abd=0x%x alsec as_abd=0x%x alsec total=%zu buffer-cache buf=%d\n", FN_ABD_SIZE, AS_ABD_SIZE, sizeof(alsec_container_t), DEV_BSIZE); printf("legit max : fnode-leaves=%d fnode-nodes=%d " "alsec-leaves=%d alsec-nodes=%d\n\n", (int)FN_MAX_LEAF, (int)FN_MAX_NODE, (int)AS_MAX_LEAF, (int)AS_MAX_NODE); uint8_t forged = 255; int any_oob = 0, all_rejected = 1; size_t worst = 0; /* ---- hpfs_alblk2alsec : source = fnode fn_ab, dest = new alsec 512B buf */ printf("--- hpfs_alblk2alsec (sys/vfs/hpfs/hpfs_alsubr.c:314) ---\n"); printf(" bcopy(abp=fnode fn_ab, nabp=new alsec as_ab, sizeof(alblk_t)+sz*busycnt)\n"); for (int nodes = 0; nodes <= 1; nodes++) { size_t sz = nodes ? sizeof(alnode_t) : sizeof(alleaf_t); void *fnbase = poison_alloc(sizeof(fnode_container_t), pagesz, 0xAA); fnode_container_t *fn = (fnode_container_t *)fnbase; fn->fn_ab.ab_flag = nodes ? AB_NODES : 0; fn->fn_ab.ab_busycnt = forged; /* destination: a fresh 512B alsec buffer (modelled as alsec_container * placed at the end of a poison-tailed page; the backing malloc in * the kernel is exactly DEV_BSIZE). */ void *asbase = poison_alloc(sizeof(alsec_container_t), pagesz, 0xAA); alsec_container_t *as = (alsec_container_t *)asbase; size_t written; int poison; size_t oob = bug_alblk2alsec(&fn->fn_ab, as, &written, &poison); printf("[BUG] alblk2alsec busycnt=%-3u sz=%2zu (%s) WRITTEN=%zuB OOB past 512B buf=%zuB poison=%s\n", forged, sz, nodes?"alnode":"alleaf", written, oob, poison?"HIT":"no"); if (oob > 0) any_oob = 1; if (oob > worst) worst = oob; if (validate(/*is_fnode=*/1, fn->fn_ab.ab_flag, forged) == 0) all_rejected = 0; } printf("\n"); /* ---- hpfs_splitalsec : source = old alsec, dest = new alsec 512B buf */ printf("--- hpfs_splitalsec (sys/vfs/hpfs/hpfs_alsubr.c:225/229) ---\n"); printf(" n1=(busycnt+1)/2 n2=busycnt-n1 ; bcopy(abp+8+n1*sz, nabp+8, n2*sz)\n"); for (int nodes = 0; nodes <= 1; nodes++) { size_t sz = nodes ? sizeof(alnode_t) : sizeof(alleaf_t); /* old alsec: doesn't matter for dest OOB; just need its alblk */ void *oldbase = poison_alloc(sizeof(alsec_container_t), pagesz, 0xBB); alsec_container_t *old = (alsec_container_t *)oldbase; old->as_ab.ab_flag = nodes ? AB_NODES : 0; old->as_ab.ab_busycnt = forged; /* new alsec 512B buffer */ void *newbase = poison_alloc(sizeof(alsec_container_t), pagesz, 0xAA); alsec_container_t *nw = (alsec_container_t *)newbase; size_t written, src_oob; int poison; size_t oob = bug_splitalsec(&old->as_ab, nw, &written, &poison, &src_oob); printf("[BUG] splitalsec busycnt=%-3u sz=%2zu (%s) WRITTEN=%zuB OOB past 512B buf=%zuB src OOB past as_abd=%zuB poison=%s\n", forged, sz, nodes?"alnode":"alleaf", written, oob, src_oob, poison?"HIT":"no"); if (oob > 0) any_oob = 1; if (oob > worst) worst = oob; if (validate(/*is_fnode=*/0, old->as_ab.ab_flag, forged) == 0) all_rejected = 0; } printf("\n"); /* ---- hpfs_concatalsec : source = ab1p alsec, dest = ab0p alsec */ printf("--- hpfs_concatalsec (sys/vfs/hpfs/hpfs_alsubr.c:271/278) ---\n"); printf(" if(ab0.freecnt>ab1.busycnt) bcopy(AB_ALNODE(ab1), AB_FREEANP(ab0), ab1.busycnt*sz)\n"); /* The guard at :271 is `ab0p->ab_freecnt > ab1p->ab_busycnt` โ BOTH * fields are untrusted on-disk bytes (DF-0859). We pick the maximally * overflowing combination: ab0.freecnt = 0xFF (max u8), ab1.busycnt = * a value smaller than 0xFF but still > alsec max (so the bcopy * overflows the destination data area). We use 0xFE so the guard is * `0xFF > 0xFE` = TRUE and the bcopy writes 0xFE*sz bytes. */ uint8_t concat_busycnt = 0xFE; /* 254 โ passes the forged guard */ for (int nodes = 0; nodes <= 1; nodes++) { size_t sz = nodes ? sizeof(alnode_t) : sizeof(alleaf_t); /* ab0 = destination alsec (512B buf) */ void *ab0base = poison_alloc(sizeof(alsec_container_t), pagesz, 0xAA); alsec_container_t *ab0c = (alsec_container_t *)ab0base; ab0c->as_ab.ab_flag = nodes ? AB_NODES : 0; /* ab0's freecnt is FORGED to 0xFF so the guard passes regardless; * ab_freeoff forged to sizeof(alblk_t) so the bcopy destination * starts at the beginning of the data area (worst case). */ ab0c->as_ab.ab_freecnt = 0xFF; ab0c->as_ab.ab_freeoff = sizeof(alblk_t); /* ab1 = source alsec */ void *ab1base = poison_alloc(sizeof(alsec_container_t), pagesz, 0xBB); alsec_container_t *ab1c = (alsec_container_t *)ab1base; ab1c->as_ab.ab_flag = nodes ? AB_NODES : 0; ab1c->as_ab.ab_busycnt = concat_busycnt; size_t written, src_oob; int poison; size_t oob = bug_concatalsec(&ab0c->as_ab, ab0c, &ab1c->as_ab, &written, &poison, &src_oob); printf("[BUG] concatalsec ab1.busycnt=%-3u ab0.freecnt=%-3u sz=%2zu (%s) WRITTEN=%zuB OOB past 512B buf=%zuB src OOB past as_abd=%zuB poison=%s\n", concat_busycnt, 0xFF, sz, nodes?"alnode":"alleaf", written, oob, src_oob, poison?"HIT":"no"); if (oob > 0) any_oob = 1; if (oob > worst) worst = oob; /* fix: validate ab0.busycnt AND ab1.busycnt; the guard's use of * freecnt is also covered because the fix caps freecnt implicitly * by capping busycnt (ab_freecnt = max - busycnt). */ if (validate(/*is_fnode=*/0, ab1c->as_ab.ab_flag, concat_busycnt) == 0) all_rejected = 0; } printf("\n"); /* ---- FIXED mode: validate rejects forged busycnt ----------------- */ printf("--- FIXED: validate ab_busycnt against container max before bcopy ---\n"); /* Note: validate() returns 0 if the value passes (would be ACCEPTED by * an unfixed kernel) and -1 if the fix REJECTS it (out-of-range). For * forged busycnt=255 the fix always rejects. */ int rc; rc = validate(/*is_fnode=*/1, 0, forged); printf("[FIX] alblk2alsec leaf forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)"); if (rc == 0) all_rejected = 0; rc = validate(/*is_fnode=*/1, AB_NODES, forged); printf("[FIX] alblk2alsec node forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)"); if (rc == 0) all_rejected = 0; rc = validate(/*is_fnode=*/0, 0, forged); printf("[FIX] split/concat leaf forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)"); if (rc == 0) all_rejected = 0; rc = validate(/*is_fnode=*/0, AB_NODES, forged); printf("[FIX] split/concat node forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)"); if (rc == 0) all_rejected = 0; printf("\n=== SUMMARY ===\n"); printf("DF_0859_BUG_OOB_WRITE_MAX_BYTES=%zu\n", worst); printf("DF_0859_BUG_CONFIRMED=%d\n", any_oob); printf("DF_0859_FIX_REJECTS_FORGED_BUSYCNT=%d\n", all_rejected); return any_oob ? 0 : 1; } |