DF-0787 / 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 | /* * harness.c - Userspace replication of the unbounded attribute walk in * ntfs_loadntnode() (sys/vfs/ntfs/ntfs_subr.c:305-320). * * WHY A HARNESS * ------------- * The live NTFS trigger (mount_ntfs on a crafted image) reaches the buggy * loop at mount time before the DF-0786 lockmgr panic (which only fires on * directory LOOKUP, not on the initial MFT load). But on a default GENERIC * kernel the OOB-read variant may or may not fault depending on what byte * pattern lives in the adjacent slab chunk; only the reclen==0 variant is * guaranteed to manifest (as a kernel hang). To make the bug deterministic * AND to demonstrate the fix in isolation, this harness reproduces the exact * walk logic with a guard page immediately after the (4096-byte) record * 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 exact struct layout from sys/vfs/ntfs/ntfs.h: * struct attrhdr { u32 a_type; u32 reclen; u8 a_flag; u8 a_namelen; * u8 a_nameoff; u8 reserved1; u8 a_compression; * u8 reserved2; u16 a_index; }; // 16 bytes * * Build: cc -O2 -o harness harness.c * Run: ./harness {clean|loop|oob_attroff|oob_reclen} [apply_fix] * Exit: 0 = walk completed cleanly (end marker found, or fix rejected bad input) * 1 = iteration cap hit (would be infinite loop in kernel) * 2 = SIGSEGV caught (would be OOB read 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 REC_BYTES 4096 /* mirror ntm_bpmftrec=8 sectors for our image */ #define ATTR_END 0xFFFFFFFFu #define ITER_CAP 100000 /* kernel has no cap; this catches infinite loops */ struct attrhdr { uint32_t a_type; uint32_t reclen; uint8_t a_flag; uint8_t a_namelen; uint8_t a_nameoff; uint8_t reserved1; uint8_t a_compression; uint8_t reserved2; uint16_t a_index; } __attribute__((packed)); /* Mirror of the buggy walk in ntfs_loadntnode() lines 305-320. */ static int buggy_walk(uint8_t *rec, uint16_t fr_attroff) { int off = fr_attroff; struct attrhdr *ap; unsigned iters = 0; while (1) { ap = (struct attrhdr *)(rec + off); if (ap->a_type == ATTR_END) return 0; /* clean end-of-attributes */ if (++iters > ITER_CAP) return 1; /* infinite loop (reclen==0 etc) */ /* ntfs_attrtontvattr(ap) would be called here in the kernel */ off += ap->reclen; /* UNCHECKED: no bound, no !=0 */ } } /* The proposed fix: bound the walk by the record size and reject zero/overflow. */ static int fixed_walk(uint8_t *rec, uint16_t fr_attroff, int recsz) { int off = fr_attroff; struct attrhdr *ap; unsigned iters = 0; if (off <= 0 || off >= recsz) /* NEW: bound initial offset */ return -1; while (1) { if (off + (int)sizeof(struct attrhdr) > recsz) /* NEW */ return -1; ap = (struct attrhdr *)(rec + off); if (ap->a_type == ATTR_END) return 0; if (ap->reclen < sizeof(struct attrhdr)) /* NEW: reject 0 / tiny */ return -1; if (ap->reclen > recsz - off) /* NEW: no overflow past end */ return -1; if (++iters > REC_BYTES) /* paranoia cap */ return -1; off += ap->reclen; } } /* ---- signal handling so we can catch the OOB 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 *rec; /* the synthetic MFT record */ static void fill_record(const char *mode) { /* start from a clean record: resident attribute + end marker */ memset(rec, 0, REC_BYTES); uint16_t attroff = 0x48; /* mirror the sibling generator's 72 */ memcpy(rec + attroff, "\x90\x00\x00\x00", 4); /* a_type = NTFS_A_INDXROOT */ /* reclen field at attroff+4 — set per mode below */ uint32_t end = ATTR_END; memcpy(rec + attroff + 8, &end, 4); /* placeholder end marker (overwritten) */ /* place end-of-attributes marker after a 64-byte resident attr body */ uint32_t real_end = ATTR_END; memcpy(rec + attroff + 64, &real_end, 4); /* (We don't write fr_attroff into a filerec header; we pass it directly.) */ if (strcmp(mode, "clean") == 0) { uint32_t rl = 64; memcpy(rec + attroff + 4, &rl, 4); /* reclen = 64 -> next is end marker */ } else if (strcmp(mode, "loop") == 0) { uint32_t rl = 0; memcpy(rec + attroff + 4, &rl, 4); /* reclen = 0 -> infinite loop */ } else if (strcmp(mode, "oob_attroff") == 0) { /* caller will pass an out-of-range attroff to the walker */ uint32_t rl = 64; memcpy(rec + attroff + 4, &rl, 4); } else if (strcmp(mode, "oob_reclen") == 0) { uint32_t rl = 0x1000; /* reclen = 4096 -> next iter off end */ memcpy(rec + attroff + 4, &rl, 4); } else { fprintf(stderr, "unknown mode '%s'\n", mode); exit(3); } } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s {clean|loop|oob_attroff|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 record with a guard page immediately after it, so any OOB read faults deterministically (mimicking the kernel reading past the 4096-byte kmalloc'd mfrp). */ /* DragonFly's <unistd.h> doesn't always expose _SC_PAGESIZE; hardcode. */ long pagesz = 4096; size_t mapsz = (REC_BYTES + pagesz - 1) & ~(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 rec at the END of the writable region so the guard page is right after byte REC_BYTES-1 (truest mirror of kmalloc(4096) on a slab). */ rec = base + mapsz - REC_BYTES; fill_record(mode); /* 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); /* choose the attroff we pass to the walker */ uint16_t pass_attroff = 0x48; if (strcmp(mode, "oob_attroff") == 0) pass_attroff = 0x0FF0; /* past resident attr list, inside allocation */ got_sig = 0; int rc; if (sigsetjmp(jb, 1) == 0) { if (apply_fix) rc = fixed_walk(rec, pass_attroff, REC_BYTES); else rc = buggy_walk(rec, pass_attroff); } else { rc = 2; /* SIGSEGV caught */ } sigaction(SIGSEGV, &oldsa, NULL); const char *verdict; switch (rc) { case 0: verdict = "clean exit (end-of-attributes reached)"; break; case 1: verdict = "ITERATION CAP HIT -> would be infinite loop in kernel"; break; case 2: verdict = "SIGSEGV -> OOB read past record (into adjacent slab in kernel)"; break; case -1: verdict = "FIX REJECTED input ( EINVAL in kernel)"; break; default: verdict = "unknown"; break; } printf("mode=%-12s apply_fix=%-3d -> rc=%d %s\n", mode, apply_fix, rc, verdict); return (rc == 0 || rc == -1) ? 0 : rc; } |