DF-0894 / 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 | /* * harness.c - DF-0894 deterministic primitive characterizer. * * Transcribes the ffs_balloc indirect-block indexing math verbatim, with a * poisoned allocator that sizes the indirect-block buffer at exactly * `fs_bsize` bytes (the real kernel bread() size) and surrounds it with * sentinels. This proves, *without* depending on the kernel slab/page * layout, that a forged `fs_nindir` copied into `ump->um_nindir` at * ffs_vfsops.c:729 drives `bap[in_off]` indices that exceed the * `fs_bsize / sizeof(ufs_daddr_t)` entries actually present in the * bread()'d indirect-block buffer โ an out-of-bounds read (ffs_balloc.c:297) * and write (ffs_balloc.c:333/378/479). * * Source model (sys/vfs/ufs/ufs_bmap.c:281-324): * MNINDIR(ump) = ump->um_nindir = fs->fs_nindir [forged] * for single-indirect: off = bn % MNINDIR(ump) * indirs[1].in_off = off * * Sink model (sys/vfs/ufs/ffs_balloc.c:291-297): * bread(vp, ..., (int)fs->fs_bsize, &bp); // bp->b_data is fs_bsize bytes * bap = (ufs_daddr_t *)bp->b_data; // valid indices 0..fs_bsize/4-1 * nb = bap[indirs[i].in_off]; // OOB if in_off >= fs_bsize/4 * * Build: cc -o harness harness.c * Run: ./harness [<fs_bsize> <forged_nindir> <target_lbn>] * defaults: fs_bsize=4096 forged_nindir=8192 target_lbn=2012 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #define UFS_NDADDR 12 #define UFS_NIADDR 3 #define SENTINEL 0xAB static uint32_t MNINDIR; /* forged */ /* * Verbatim transcription of ufs_getlbns (ufs_bmap.c:256-327) for the * single-indirect case. Returns the in_off used to index bap[]. */ static int compute_in_off(int64_t lbn, int *numlevels){ int64_t bn = lbn; long blockcnt; int64_t qblockcnt; int i, off, numlevels_local = 0; int64_t metalbn, realbn; int num = 0; realbn = bn; if (bn < 0) bn = -bn; if (bn < UFS_NDADDR) return -1; /* direct block, no indirect */ for (blockcnt = 1, i = UFS_NIADDR, bn -= UFS_NDADDR;; i--, bn -= blockcnt) { if (i == 0) return -2; /* EFBIG */ qblockcnt = (int64_t)blockcnt * MNINDIR; if (bn < qblockcnt) break; blockcnt = qblockcnt; } if (realbn >= 0) metalbn = -(realbn - bn + UFS_NIADDR - i); else metalbn = -(-realbn - bn + UFS_NIADDR - i); off = UFS_NIADDR - i; /* indirs[0].in_off โ index into i_ib[] */ numlevels_local = 1; for (; i <= UFS_NIADDR; i++) { if (metalbn == realbn) break; off = (bn / blockcnt) % MNINDIR; /* indirs[1].in_off โ index into bap[] */ numlevels_local++; metalbn -= -1 + off * blockcnt; blockcnt /= MNINDIR; } num = numlevels_local; if (numlevels) *numlevels = num; return off; /* this is indirs[num-1].in_off for the single-indirect case */ } int main(int argc, char **argv){ uint32_t fs_bsize = (argc >= 2) ? (uint32_t)strtoul(argv[1],NULL,0) : 4096; uint32_t forged_nindir = (argc >= 3) ? (uint32_t)strtoul(argv[2],NULL,0) : 8192; int64_t target_lbn = (argc >= 4) ? (int64_t)strtoll(argv[3],NULL,0) : 2012; MNINDIR = forged_nindir; uint32_t valid_entries = fs_bsize / 4; /* sizeof(ufs_daddr_t) = 4 */ printf("=== DF-0894 primitive characterization ===\n"); printf("fs_bsize (indirect-block buffer size) = %u bytes\n", fs_bsize); printf("valid bap[] entries = %u (indices 0..%u)\n", valid_entries, valid_entries - 1); printf("forged fs_nindir -> MNINDIR(ump) = %u\n", forged_nindir); printf("target lbn = %lld\n", (long long)target_lbn); int nlev = 0; int in_off = compute_in_off(target_lbn, &nlev); if (in_off < 0) { printf("\ncompute_in_off returned %d (lbn not in indirect range)\n", in_off); return 1; } printf("\nufs_getlbns result:\n"); printf(" numlevels = %d\n", nlev); printf(" indirs[in_off] = %d <- indexes bap[] in ffs_balloc.c:297\n", in_off); /* model the bread()'d buffer with sentinels */ uint32_t slab_size = fs_bsize + 8192; /* generous pad */ uint8_t *slab = (uint8_t*)malloc(slab_size); if (!slab) { perror("malloc"); return 2; } memset(slab, SENTINEL, slab_size); uint8_t *bp_data = slab; /* the indirect-block buffer */ memset(bp_data, 0x00, fs_bsize); /* fresh zero-filled indirect block */ uint32_t idx_byte_off = (uint32_t)in_off * 4; printf("\nbap[%d] = bp->b_data + %u\n", in_off, idx_byte_off); printf("buffer ends at bp->b_data + %u\n", fs_bsize); if (idx_byte_off >= fs_bsize) { uint32_t oob = idx_byte_off - fs_bsize; uint32_t oob_rounded = oob + 4; /* the 4-byte access straddles */ printf(">>> OOB READ at ffs_balloc.c:297: %u bytes past the %u-byte buffer " "(+4 for the ufs_daddr_t access)\n", oob, fs_bsize); printf(">>> OOB WRITE at ffs_balloc.c:333/378/479 (when nb==0): " "writes a disk block number %u bytes past the buffer\n", oob); /* check what the OOB read would return (sentinel region = 0xAB) */ if (idx_byte_off + 4 <= slab_size) { uint8_t v0 = bp_data[idx_byte_off + 0]; uint8_t v1 = bp_data[idx_byte_off + 1]; uint8_t v2 = bp_data[idx_byte_off + 2]; uint8_t v3 = bp_data[idx_byte_off + 3]; uint32_t nb = (uint32_t)v0 | ((uint32_t)v1<<8) | ((uint32_t)v2<<16) | ((uint32_t)v3<<24); printf("OOB read value nb = 0x%x (%s)\n", nb, nb == 0 ? "zero -> allocation path -> OOB WRITE at :378" : "non-zero -> treated as disk addr -> further corruption"); } printf("\nRESULT: OOB index confirmed. bap[%d] reads/writes %u bytes " "past the %u-byte indirect-block buffer.\n", in_off, oob, fs_bsize); printf(" Heap/page corruption into adjacent kernel memory.\n"); free(slab); return 0; } else { printf("\nRESULT: index in-bounds (no OOB). fs_nindir not forged enough " "or target lbn too low.\n"); free(slab); return 1; } } |