DF-0790 / 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 | /* * harness.c - Userspace replication of the ATTRLIST walk in * ntfs_ntvattrget() (sys/vfs/ntfs/ntfs_subr.c:196-236). * * THE BUG (DF-0790) * ----------------- * ntfs_ntvattrget() reads the $ATTRIBUTE_LIST (NTFS_A_ATTRLIST, type 0x20) * resident attribute's data into a heap buffer `alpool` (kmalloc(va_datalen)) * and then walks the list of `struct attr_attrlist` entries looking for the * requested attribute in other MFT records: * * 186: len = lvap->va_datalen; * 187: alpool = kmalloc(len, M_TEMP, M_WAITOK); * 188: error = ntfs_readntvattr_plain(ntmp, ip, lvap, 0, len, alpool, &len, NULL); * ... * 193: aalp = (struct attr_attrlist *) alpool; * 194: nextaalp = NULL; * 196: for(; len > 0; aalp = nextaalp) { * 202: if (len > aalp->reclen) { * 203: nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *); * 204: } else { * 205: nextaalp = NULL; * 206: } * 207: len -= aalp->reclen; // <-- NO CHECK that reclen != 0 * ... * 236: } * * `struct attr_attrlist` (sys/vfs/ntfs/ntfs.h:144-154): * u32 al_type; u16 reclen; u8 al_namelen; u8 al_nameoff; * u64 al_vcnstart; u32 al_inumber; u32 reserved; u16 al_index; u16 al_name[1]; * * NTFS_NEXTREC (ntfs.h:273): * #define NTFS_NEXTREC(s, type) ((type)(((caddr_t) s) + (s)->reclen)) * * Two distinct malformed-image cases, both unguarded: * * (A) reclen == 0 -> len -= 0 (unchanged); nextaalp = aalp + 0 == aalp; * the for() never advances => INFINITE LOOP (CWE-835). * In the kernel this hangs the calling thread at 100% * CPU forever; mount(2) or the triggering syscall * never returns. Local DoS. * * (B) reclen > len -> the else branch sets nextaalp = NULL; * len -= reclen UNDERFLOWS (size_t) to a huge value; * next iteration: len > 0 is true, aalp = NULL; * the `len > aalp->reclen` check dereferences NULL * => PANIC (NULL deref, CWE-476). * * WHY A HARNESS * ------------- * The live NTFS trigger requires (1) root to mount the crafted image and * (2) reaching ntfs_ntvattrget() with an attribute that is referenced by a * corrupted $ATTRIBUTE_LIST but not present inline in the MFT record. On this * guest the sibling DF-0786 lockmgr panic ("locking against itself" in * ntfs_ntget during directory lookup) races the lookup path; depending on * timing the lockmgr panic may fire first. To make the DF-0790 bug * DETERMINISTIC and to exercise the fix in isolation, this harness reproduces * the EXACT walk logic with a guard page immediately after the ATTRLIST * buffer, so any OOB read faults and any reclen==0 spins (caught by an * iteration cap that the kernel does NOT have). * * The harness mirrors the struct layout from sys/vfs/ntfs/ntfs.h. * * Build: cc -O2 -o harness harness.c * Run: ./harness {clean|loop|null|oob_reclen} [apply_fix] * Exit: 0 = walk completed cleanly (len exhausted, or fix rejected bad input) * 1 = iteration cap hit (would be infinite loop in kernel) * 2 = SIGSEGV caught (would be OOB read / NULL deref in kernel) */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> #define AAL_BYTES 4096 /* mirror a typical resident ATTRLIST alloc */ #define ITER_CAP 100000 /* kernel has no cap; this catches inf loops */ /* Mirror of struct attr_attrlist (sys/vfs/ntfs/ntfs.h:144-154). */ struct attr_attrlist { uint32_t al_type; /* Attribute type */ uint16_t reclen; /* length of this entry */ uint8_t al_namelen; /* Attribute name len */ uint8_t al_nameoff; /* Name offset from entry start */ uint64_t al_vcnstart; /* VCN number */ uint32_t al_inumber; /* Parent ntnode */ uint32_t reserved; uint16_t al_index; /* Attribute index in MFT record */ uint16_t al_name[1]; /* Name (variable) */ } __attribute__((packed)); #define NTFS_NEXTREC(s, type) ((type)(((char *)(s)) + (s)->reclen)) /* Mirror of the buggy walk in ntfs_ntvattrget() lines 196-236. * Returns 0 on clean completion, 1 if the iteration cap is hit (infinite * loop in the kernel), 2 if a guard-page fault is caught by the caller. */ static int buggy_walk(struct attr_attrlist *aalp, size_t len) { struct attr_attrlist *nextaalp = NULL; unsigned iters = 0; for (; len > 0; aalp = nextaalp) { if (++iters > ITER_CAP) return 1; /* infinite loop */ if (len > aalp->reclen) { nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *); } else { nextaalp = NULL; } len -= aalp->reclen; /* UNCHECKED: no !=0 guard */ /* The kernel body continues with NTFS_AALPCMP / ntfs_vgetex etc. * For the harness we only care that the walk advances; a real * mismatched entry would `continue`, which we mimic by looping. */ } return 0; /* len exhausted cleanly */ } /* The proposed fix: reject reclen==0 and reclen>remaining before advancing, * and require the fixed entry header to fit in the remaining buffer before * any field is read. Returns 0 on clean completion, -1 (EINVAL) if a * malformed entry is seen. */ static int fixed_walk(struct attr_attrlist *aalp, size_t len) { struct attr_attrlist *nextaalp; unsigned iters = 0; /* fixed header size (no variable name tail): the kernel check is * `sizeof(struct attr_attrlist)` against the remaining buffer; here we * use the 26-byte fixed part, matching the C struct minus the trailing * flexible al_name[1] member. */ const size_t hdr = 26; for (; len > 0; aalp = nextaalp) { if (++iters > ITER_CAP) return -1; /* DF-0790 fix step 1: the fixed entry header must fit in the * remaining buffer before we dereference ANY field of *aalp. * (This is what blocks the oob_reclen case -- without it, reading * aalp->reclen on a pointer that straddles the buffer end faults.) */ if (len < hdr) return -1; /* DF-0790 fix step 2: reclen must be sane -- at least the header, * and not greater than the remaining buffer. This rejects * reclen==0 (infinite loop) and reclen>len (size_t underflow -> * NULL deref). */ if (aalp->reclen < hdr) return -1; if ((size_t)aalp->reclen > len) return -1; nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *); len -= aalp->reclen; } return 0; } /* ---- signal handling so we can catch the OOB / NULL-deref SIGSEGV cleanly */ static sigjmp_buf jb; static volatile int got_sig; static void segv_handler(int sig) { (void)sig; got_sig = SIGSEGV; siglongjmp(jb, 1); } static uint8_t *buf; /* the synthetic ATTRLIST buffer */ static struct attr_attrlist *fill_buffer(const char *mode, size_t *out_len) { memset(buf, 0, AAL_BYTES); /* Entry 0 at offset 0: a $DATA (0x80) attr-list entry pointing at ino 5. */ struct attr_attrlist *e0 = (struct attr_attrlist *)buf; e0->al_type = 0x80; /* NTFS_A_DATA */ e0->al_namelen = 0; e0->al_nameoff = 0; e0->al_vcnstart = 0; e0->al_inumber = 5; e0->al_index = 0; size_t len; if (strcmp(mode, "clean") == 0) { /* one well-formed 32-byte entry, then len runs out -> clean exit */ e0->reclen = 32; len = 32; /* exactly one entry, then len==0 */ } else if (strcmp(mode, "loop") == 0) { /* DF-0790 case (A): reclen == 0 -> walk never advances */ e0->reclen = 0; len = 64; /* len > 0 so the for() body runs */ } else if (strcmp(mode, "null") == 0) { /* DF-0790 case (B): reclen > len -> else sets nextaalp=NULL, * len underflows to huge, next iter dereferences aalp=NULL. * We make len small (8) so reclen(=32) > len triggers the else. */ e0->reclen = 32; len = 8; /* len < reclen -> NULL-deref path */ } else if (strcmp(mode, "oob_reclen") == 0) { /* Variant: reclen pushes nextaalp to straddle the buffer end so the * next iteration's aalp->reclen deref reads the guard page -> OOB. * (Same shape as DF-0787's oob_reclen, here on the ATTRLIST walk.) * With buf at the page tail, nextaalp = buf + (AAL_BYTES-1); the * following aalp->reclen access reads buf + AAL_BYTES + 3 -> fault. */ e0->reclen = AAL_BYTES - 1; len = AAL_BYTES; } else { fprintf(stderr, "unknown mode '%s'\n", mode); exit(3); } *out_len = len; return e0; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s {clean|loop|null|oob_reclen} [apply_fix]\n", argv[0]); return 3; } const char *mode = argv[1]; int apply_fix = (argc >= 3 && strcmp(argv[2], "apply_fix") == 0); /* Allocate the ATTRLIST buffer with a guard page immediately after it, * so any OOB read (case oob_reclen, or the NULL-deref wild pointer) * faults deterministically -- mimicking the kernel reading past the * kmalloc(va_datalen) slab chunk. */ long pagesz = 4096; size_t mapsz = (AAL_BYTES + pagesz - 1) & ~((size_t)pagesz - 1); /* round up */ uint8_t *base = mmap(NULL, mapsz + pagesz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 3; } if (mprotect(base + mapsz, pagesz, PROT_NONE) != 0) { perror("mprotect"); return 3; } /* place buf at the END of the writable region so the guard page is right * after byte AAL_BYTES-1 (truest mirror of kmalloc(AAL_BYTES) on a slab). */ buf = base + mapsz - AAL_BYTES; size_t len; struct attr_attrlist *aalp = fill_buffer(mode, &len); /* install SIGSEGV handler */ struct sigaction sa, oldsa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = segv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGSEGV, &sa, &oldsa); got_sig = 0; int rc; if (sigsetjmp(jb, 1) == 0) { if (apply_fix) rc = fixed_walk(aalp, len); else rc = buggy_walk(aalp, len); } else { rc = 2; /* SIGSEGV caught */ } sigaction(SIGSEGV, &oldsa, NULL); const char *verdict; switch (rc) { case 0: verdict = "clean exit (ATTRLIST exhausted)"; break; case 1: verdict = "ITERATION CAP HIT -> would be infinite loop in kernel"; break; case 2: verdict = "SIGSEGV -> NULL deref / OOB read in kernel"; break; case -1: verdict = "FIX REJECTED malformed entry (EINVAL in kernel)"; break; default: verdict = "unknown"; break; } printf("mode=%-11s apply_fix=%-3d -> rc=%d %s\n", mode, apply_fix, rc, verdict); return (rc == 0 || rc == -1) ? 0 : rc; } |