DF-0903 / 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 | /* * harness.c - DF-0903 deterministic transcription of the ffs_write math. * * Reproduces the exact blkoff()/xfersize arithmetic from * ufs_readwrite.c:290-356 with a POISONED allocator, WITHOUT requiring a * real mount. Crucially, this harness models the ACTUAL kernel behavior * including the size_t/int promotion at line 294-295 that the finding's * narrative overlooked: * * uio->uio_resid is `size_t` (sys/sys/_uio.h:69), xfersize is `int` * (ufs_readwrite.c:220). Line 294 `if (uio->uio_resid < xfersize)` * promotes the int to size_t, so a NEGATIVE xfersize becomes a huge * size_t, the comparison is TRUE, and line 295 CLAMPS xfersize to * uio_resid (a small positive). The negative xfersize therefore never * reaches line 356's uiomove; instead VOP_BALLOC at line 329 is called * with the clamped (small positive) xfersize, and ffs_balloc panics with * "blk too big" because blkoffset+xfersize > fs_bsize. * * The finding's claimed end-state (OOB write via negative xfersize as * size_t at uiomove) is NOT reachable on master. The real, demonstrated * impact is a kernel panic (DoS) via crafted fs_qbmask. * * Build: cc -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> /* Mirror the kernel macros from sys/vfs/ufs/fs.h:487-488. */ static int64_t fs_qbmask; /* attacker-controlled on-disk field */ static int32_t fs_bsize; #define blkoff(offset) ((int64_t)(offset) & fs_qbmask) /* A simulated bp->b_data buffer (kernel buffer of fs_bsize bytes). */ static unsigned char *bp_data; static size_t bp_bufsize; static size_t uiomove_oob(unsigned char *cp, size_t n, size_t resid, const unsigned char *src){ /* Transcribe kern_subr.c uiomove(): loops while n>0 && resid>0, copies * min(iov_len, n) per iter. Here iov_len = resid (single iovec). */ size_t copied = 0; while (n > 0 && resid > 0) { size_t cnt = resid; if (cnt > n) cnt = n; for (size_t i = 0; i < cnt; i++) cp[i] = src[i]; copied += cnt; cp += cnt; n -= cnt; resid -= cnt; } return copied; } int main(void){ /* Default reproduction: crafted fs_qbmask = 0x3FFF, fs_bsize = 8192. */ fs_qbmask = 0x3fff; fs_bsize = 8192; bp_bufsize = fs_bsize; bp_data = calloc(1, bp_bufsize + 16384); /* +guard */ /* Sentinel pattern in the guard region. */ memset(bp_data + bp_bufsize, 0xCC, 16384); printf("=== DF-0903 ffs_write math harness (accurate kernel model) ===\n"); printf("crafted fs_qbmask = 0x%llx (vs normal 0x%x = fs_bsize-1)\n", (unsigned long long)fs_qbmask, (unsigned)(fs_bsize - 1)); printf("fs_bsize = %d\n", fs_bsize); printf("buffer bp_data = [%p, %p) size=%zu\n", (void*)bp_data, (void*)(bp_data + bp_bufsize), bp_bufsize); printf("\nNOTE: uio_resid is size_t (unsigned); xfersize is int (signed).\n"); printf("Line 294 `uio_resid < xfersize' promotes xfersize to size_t,\n"); printf("so negative xfersize becomes huge => comparison TRUE => clamp.\n"); long long offsets[] = {8192, 8193, 10000, 12000, 16383, 16384}; size_t uio_resid = 16; /* pwrite(fd, buf, 16, offset) */ for (size_t i = 0; i < sizeof(offsets)/sizeof(offsets[0]); i++) { long long offset = offsets[i]; long long blkoffset = blkoff(offset); int xfersize_pre = (int)((int64_t)fs_bsize - blkoffset); int xfersize = xfersize_pre; /* Model line 294-295 with size_t promotion: */ if ((size_t)uio_resid < (size_t)xfersize) /* promotion */ xfersize = (int)uio_resid; /* clamp */ printf("\n--- pwrite offset=%lld uio_resid=%zu ---\n", offset, uio_resid); printf(" blkoff(offset) = offset & 0x%llx = %lld\n", (unsigned long long)fs_qbmask, blkoffset); printf(" xfersize (line 293) = fs_bsize - blkoffset = %d\n", xfersize_pre); if (xfersize_pre != xfersize) printf(" xfersize (after clamp) = %d (clamped to uio_resid by line 294-295)\n", xfersize); else printf(" xfersize (after clamp) = %d (no clamp)\n", xfersize); /* balloc size check (ffs_balloc.c:89-91): */ long long balloc_size = blkoffset + xfersize; printf(" balloc size = blkoffset + xfersize = %lld + %d = %lld", blkoffset, xfersize, balloc_size); if (balloc_size > fs_bsize) { printf(" > fs_bsize=%d => PANIC 'ffs_balloc: blk too big'\n", fs_bsize); printf(" >>> ACTUAL KERNEL BEHAVIOR: panic here, never reaches uiomove <<<\n"); continue; } printf(" <= fs_bsize => no balloc panic\n"); /* size clamp at line 352-354 (only reached if balloc passed): */ int size_352 = fs_bsize - 0; /* assume b_resid=0, BLKSIZE=fs_bsize */ if (size_352 < xfersize) xfersize = size_352; /* uiomove dst: */ unsigned char *dst = bp_data + blkoffset; long long oob_extent = (long long)blkoffset - (long long)bp_bufsize; printf(" uiomove dst = bp_data + %lld = %p", blkoffset, (void*)dst); if (oob_extent > 0) { printf(" -- %lld bytes PAST buffer end\n", oob_extent); size_t xfersize_st = (size_t)xfersize; unsigned char src[16]; memset(src, 0x41, sizeof(src)); size_t copied = uiomove_oob(dst, xfersize_st, uio_resid, src); printf(" uiomove would copy %zu bytes (defeated by clamp above; unreachable in kernel)\n", copied); memset(bp_data + bp_bufsize, 0xCC, 16384); } else if (xfersize <= 0) { printf(" xfersize=%d (non-positive); ffs_read:147 would panic, ffs_write does not\n", xfersize); size_t xfersize_st = (size_t)xfersize; unsigned char src[16]; memset(src, 0x41, sizeof(src)); size_t copied = uiomove_oob(dst, xfersize_st, uio_resid, src); printf(" uiomove would copy %zu bytes (defeated by clamp above; unreachable in kernel)\n", copied); memset(bp_data + bp_bufsize, 0xCC, 16384); } else { printf(" in-bounds, xfersize=%d (normal write)\n", xfersize); } } printf("\n=== Conclusion ===\n"); printf("Crafted fs_qbmask=0x3FFF (vs normal 0x1FFF) is loaded verbatim by\n"); printf("ffs_mountfs (ffs_vfsops.c:673 bcopy; ffs_oldfscompat only re-derives\n"); printf("for fs_inodefmt < FS_44INODEFMT, but newfs sets =2). blkoffset =\n"); printf("offset & 0x3FFF can exceed fs_bsize=8192, momentarily driving\n"); printf("xfersize = fs_bsize - blkoffset negative.\n\n"); printf("HOWEVER the finding's claimed OOB-write chain (negative xfersize as\n"); printf("size_t at uiomove) is NOT reachable: line 294-295 promotes the int\n"); printf("xfersize to size_t for the comparison against size_t uio_resid,\n"); printf("clamping xfersize to a small positive value first. The clamped\n"); printf("xfersize then triggers ffs_balloc's unconditional panic\n"); printf("'ffs_balloc: blk too big' (ffs_balloc.c:91) because\n"); printf("blkoffset + xfersize > fs_bsize. Net impact: kernel PANIC (DoS)\n"); printf("from a root-mounted crafted filesystem -- not a heap OOB write.\n"); free(bp_data); return 0; } |