DF-0933 / 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 | /* * DF-0933 โ deterministic userspace transcription of the uninitialized-tail * bug in ntfs_uncompblock() (sys/vfs/ntfs/ntfs_compr.c:68-92). * * Bug: the COMPRESSED branch of ntfs_uncompblock decompresses an LZNT1 block * into buf[], advancing `pos` as it writes output bytes. When the compressed * payload is consumed (cpos reaches len+3) before `pos` reaches * NTFS_COMPBLOCK_SIZE, the function returns at ntfs_compr.c:92 WITHOUT * zeroing buf[pos..NTFS_COMPBLOCK_SIZE-1]. Contrast the UNCOMPRESSED branch * at ntfs_compr.c:65 which explicitly does: * * bzero(buf + len + 1, NTFS_COMPBLOCK_SIZE - 1 - len); * * The caller's output buffer (uup, sys/vfs/ntfs/ntfs_subr.c:1687-1690) is * allocated with kmalloc(M_WAITOK) โ NO M_ZERO โ so the unwritten tail holds * stale slab/kernel content, which uiomove (ntfs_subr.c:1722-1725) ships to * the reader. Pure confidentiality impact (CWE-908). * * Trigger (3-byte LZNT1 block, zero-padded to a full cluster on disk): * * 0x01 0x80 0x00 * header 0x8001 (LE) -> bit15=1 COMPRESSED, len = 0x001 = 1 * ctag 0x00 -> all 8 sub-tokens are LITERALS * * Trace of ntfs_uncompblock on this block: * len = 1; cpos = 2; pos = 0; * outer while: cpos(2) < len+3(4) -> true * ctag = cbuf[2] = 0x00; cpos = 3; * inner for i=0..7 (ctag == 0x00, every token is a literal): * buf[0]=cbuf[3]; buf[1]=cbuf[4]; ... buf[7]=cbuf[10]; pos = 8; cpos = 11 * outer while: cpos(11) < 4 -> false -> EXIT * return len+3 = 4; * -> buf[8..4095] is NEVER WRITTEN, NEVER ZEROED (4088 bytes of stale heap) * * This harness transcribes ntfs_uncompblock EXACTLY (lines 46-93). The output * buffer is pre-filled with a recognisable sentinel (0xDE repeating + an ASCII * marker). After running the trigger we inspect buf[8..4095]: if the sentinel * is still present, the compressed branch provably failed to zero the tail โ * the exact bytes a kernel reader would receive as stale heap. * * Build (guest): cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #define NTFS_COMPBLOCK_SIZE 0x1000 #define GET_UINT16(addr) (*((uint16_t *)(addr))) /* Verbatim transcription of ntfs_uncompblock (sys/vfs/ntfs/ntfs_compr.c:46-93). */ static int ntfs_uncompblock_h(uint8_t *buf, uint8_t *cbuf) { uint32_t ctag; int len, dshift, lmask; int blen, boff; int i, j; int pos, cpos; len = GET_UINT16(cbuf) & 0xFFF; if (!(GET_UINT16(cbuf) & 0x8000)) { if ((len + 1) != NTFS_COMPBLOCK_SIZE) { /* dprintf only */ } memcpy(buf, cbuf + 2, len + 1); bzero(buf + len + 1, NTFS_COMPBLOCK_SIZE - 1 - len); return len + 3; } cpos = 2; pos = 0; while ((cpos < len + 3) && (pos < NTFS_COMPBLOCK_SIZE)) { ctag = cbuf[cpos++]; for (i = 0; (i < 8) && (pos < NTFS_COMPBLOCK_SIZE); i++) { if (ctag & 1) { for (j = pos - 1, lmask = 0xFFF, dshift = 12; j >= 0x10; j >>= 1) { dshift--; lmask >>= 1; } boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift); blen = 3 + (GET_UINT16(cbuf + cpos) & lmask); for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) { buf[pos] = buf[pos + boff]; pos++; } cpos += 2; } else { buf[pos++] = cbuf[cpos++]; } ctag >>= 1; } } /* *** BUG SITE: no bzero(buf+pos, NTFS_COMPBLOCK_SIZE-pos) here *** */ return len + 3; } int main(void) { uint8_t cbuf[16]; memset(cbuf, 0, sizeof cbuf); cbuf[0] = 0x01; cbuf[1] = 0x80; /* header: compressed, len=1 */ cbuf[2] = 0x00; /* ctag: all 8 tokens literal */ printf("=== DF-0933 ntfs_uncompblock uninitialized-tail harness ===\n"); printf("[harness] transcribes sys/vfs/ntfs/ntfs_compr.c:46-93 line-for-line\n"); printf("[harness] trigger block (3 bytes + zero pad): %02X %02X %02X\n", cbuf[0], cbuf[1], cbuf[2]); printf("[harness] header 0x8001: COMPRESSED, len=1 (payload+header = 4 B)\n"); printf("[harness] ctag 0x00: all 8 sub-tokens are LITERALS\n"); printf("[harness] -> decompresses only buf[0..7], pos ends at 8\n"); printf("[harness] -> buf[8..4095] (4088 B) never written, never zeroed\n\n"); /* Pre-fill the output buffer with a sentinel that mimics stale slab * content. On the real kernel this region holds whatever the * M_NTFSDECOMP slab previously contained (kernel pointers, freed * objects, previously-decompressed file data). */ static uint8_t buf[NTFS_COMPBLOCK_SIZE]; memset(buf, 0xDE, NTFS_COMPBLOCK_SIZE); /* put an ascii marker in the tail so the leak is visually obvious */ memcpy(buf + NTFS_COMPBLOCK_SIZE - 16, "STALE-KMALLOC-HEAP", 16); int rc = ntfs_uncompblock_h(buf, cbuf); printf("[harness] ntfs_uncompblock returned %d (len+3)\n\n", rc); /* Inspect: buf[0..7] = literals (all 0x00 from zero-padded cbuf); * buf[8..4095] = the sentinel, proving the tail was untouched. */ printf("[harness] buf[0..15] (first 8 = literals, 8..15 = tail):\n "); for (int i = 0; i < 16; i++) printf("%02X ", buf[i]); printf("\n\n"); int tail_untouched = 1; int first_touched; for (first_touched = NTFS_COMPBLOCK_SIZE - 1; first_touched >= 0; first_touched--) { if (buf[first_touched] != 0xDE && first_touched < NTFS_COMPBLOCK_SIZE - 16) { /* not sentinel, not in ascii marker region */ break; } } /* Count how many tail bytes still hold the 0xDE sentinel. */ long stale_bytes = 0; for (int i = 8; i < NTFS_COMPBLOCK_SIZE; i++) { if (buf[i] == 0xDE) stale_bytes++; } printf("[harness] bytes still == 0xDE sentinel in buf[8..4095]: %ld / %d\n", stale_bytes, NTFS_COMPBLOCK_SIZE - 8); printf("[harness] buf[4080..4095] (ascii marker region):\n "); for (int i = NTFS_COMPBLOCK_SIZE - 16; i < NTFS_COMPBLOCK_SIZE; i++) printf("%02X ", buf[i]); printf(" |"); for (int i = NTFS_COMPBLOCK_SIZE - 16; i < NTFS_COMPBLOCK_SIZE; i++) printf("%c", (buf[i] >= 32 && buf[i] < 127) ? buf[i] : '.'); printf("|\n\n"); /* A properly-fixed ntfs_uncompblock would bzero(buf+8, 4088) and * stale_bytes would be 0. On the buggy version, the overwhelming * majority of the tail is still 0xDE. */ if (stale_bytes >= 4000) { printf("[harness] LEAK CONFIRMED: buf[8..4095] retains the sentinel.\n"); printf("[harness] The compressed branch of ntfs_uncompblock returned\n"); printf("[harness] without zeroing %ld bytes of the output block.\n", NTFS_COMPBLOCK_SIZE - 8); printf("[harness] On the kernel these bytes are stale M_NTFSDECOMP slab\n"); printf("[harness] content shipped to any reader via uiomove.\n"); return 0; } else if (stale_bytes == 0) { printf("[harness] TAIL ZEROED: buf[8..4095] was cleared (fixed kernel).\n"); return 1; } else { printf("[harness] PARTIAL: %ld stale bytes remain (unexpected).\n", stale_bytes); return 2; } } |