fuzz_datasource.c (12468B)
1 /* 2 This file is part of libextractor. 3 Copyright (C) 2026 Christian Grothoff 4 5 libextractor is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 libextractor is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libextractor; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 /** 21 * @file fuzz/fuzz_datasource.c 22 * @brief fuzzer for the core datasource and its transparent decompression 23 * @author Christian Grothoff 24 * 25 * `src/main/extractor_datasource.c` is the first code in the library 26 * that touches attacker-controlled bytes, and it does so *before* any 27 * plugin is consulted: it sniffs for gzip and bzip2 wrappers and 28 * transparently decompresses them, maintaining its own seekable view 29 * over a stream that is not seekable underneath. The gzip header 30 * walker (FEXTRA / FNAME / FCOMMENT / FHCRC) and the "seek backwards in 31 * a decompressed stream" path are the interesting parts. 32 * 33 * Input format: 34 * 35 * byte 0 read chunk size selector 36 * byte 1 behaviour bits: 37 * 0x01 create from a file on disk instead of a buffer 38 * (exercises the mmap/read path in the datasource) 39 * 0x02 call get_size_() with force=1 up front, which is 40 * what makes the datasource decompress eagerly 41 * 0x04 perform the seek script backwards 42 * 0x08 stop at the first read error instead of continuing 43 * byte 2 number of operations in the seek/read script 44 * byte 3 seed selecting the individual operations 45 * byte 4.. the file image 46 */ 47 48 #define FUZZ_HARNESS_NAME "fuzz_datasource" 49 50 #include "fuzz_common.h" 51 #include "platform.h" 52 #include "extractor.h" 53 #include "extractor_datasource.h" 54 55 #define DS_FROM_FILE 0x01 56 #define DS_FORCE_SIZE 0x02 57 #define DS_REVERSE 0x04 58 #define DS_STOP_ERR 0x08 59 60 /** 61 * Same window sizes as the plugin harness; see fuzz_ec.h. 62 */ 63 static const size_t ds_chunks[] = { 64 16 * 1024, 1, 2, 3, 7, 16, 64, 255, 256, 1024, 4096, 8192, 16383, 65 32768, 65536 66 }; 67 68 /** 69 * Upper bound on the number of script operations, so that a 70 * pathological input cannot turn one execution into a fuzzing session. 71 */ 72 #define MAX_OPS 96 73 74 75 /** 76 * Metadata callback; the datasource reports the compression type it 77 * detected through it. 78 */ 79 static int 80 ds_proc (void *cls, 81 const char *plugin_name, 82 enum EXTRACTOR_MetaType type, 83 enum EXTRACTOR_MetaFormat format, 84 const char *data_mime_type, 85 const char *data, 86 size_t data_len) 87 { 88 volatile unsigned int sink = 0; 89 size_t i; 90 91 (void) cls; 92 (void) type; 93 (void) format; 94 if (NULL == plugin_name) 95 fuzz_report_finding ("datasource reported a NULL plugin name"); 96 sink += (unsigned int) strlen (plugin_name); 97 if (NULL != data_mime_type) 98 sink += (unsigned int) strlen (data_mime_type); 99 for (i = 0; (NULL != data) && (i < data_len); i++) 100 sink += (unsigned char) data[i]; 101 (void) sink; 102 return 0; 103 } 104 105 106 int 107 LLVMFuzzerTestOneInput (const uint8_t *data, 108 size_t size) 109 { 110 struct EXTRACTOR_Datasource *ds; 111 struct fuzz_rng rng; 112 const uint8_t *img; 113 size_t img_len; 114 size_t chunk; 115 unsigned int flags; 116 unsigned int ops; 117 unsigned int i; 118 int64_t fsize; 119 char tmpl[] = "/tmp/le-fuzz-ds-XXXXXX"; 120 int fd = -1; 121 122 fuzz_ignore_sigpipe (); 123 if (size < 4) 124 return 0; 125 chunk = ds_chunks[data[0] % (sizeof (ds_chunks) / sizeof (ds_chunks[0]))]; 126 flags = data[1]; 127 ops = data[2] % MAX_OPS; 128 fuzz_rng_seed (&rng, data[3]); 129 img = data + 4; 130 img_len = size - 4; 131 132 if (0 != (flags & DS_FROM_FILE)) 133 { 134 fd = mkstemp (tmpl); 135 if (-1 == fd) 136 return 0; 137 if ( (0 != img_len) && 138 (img_len != (size_t) write (fd, img, img_len)) ) 139 { 140 (void) close (fd); 141 (void) unlink (tmpl); 142 return 0; 143 } 144 (void) close (fd); 145 ds = EXTRACTOR_datasource_create_from_file_ (tmpl, 146 &ds_proc, 147 NULL); 148 } 149 else 150 { 151 ds = EXTRACTOR_datasource_create_from_buffer_ ((const char *) img, 152 img_len, 153 &ds_proc, 154 NULL); 155 } 156 if (NULL == ds) 157 { 158 if (0 != (flags & DS_FROM_FILE)) 159 (void) unlink (tmpl); 160 return 0; 161 } 162 fsize = EXTRACTOR_datasource_get_size_ (ds, 163 (0 != (flags & DS_FORCE_SIZE))); 164 for (i = 0; i < ops; i++) 165 { 166 uint32_t op = fuzz_below (&rng, 10); 167 168 if (op < 5) 169 { 170 /* Exact-size destination: the datasource is told how many bytes 171 it may write, so writing more is an overflow in the caller's 172 buffer -- which in production is InProcessContext::buf. */ 173 size_t want = (op < 4) ? chunk : (1 + fuzz_below (&rng, 4096)); 174 unsigned char *buf = (unsigned char *) malloc ((0 == want) ? 1 : want); 175 ssize_t got; 176 177 if (NULL == buf) 178 abort (); 179 got = EXTRACTOR_datasource_read_ (ds, buf, want); 180 if (got > (ssize_t) want) 181 { 182 free (buf); 183 fuzz_report_finding ("EXTRACTOR_datasource_read_() reported more " 184 "bytes than the buffer size it was given"); 185 } 186 free (buf); 187 if ( (0 > got) && 188 (0 != (flags & DS_STOP_ERR)) ) 189 break; 190 } 191 else if (op < 9) 192 { 193 static const int whences[] = { SEEK_SET, SEEK_CUR, SEEK_END }; 194 int whence = whences[fuzz_below (&rng, 3)]; 195 int64_t pos; 196 197 switch (fuzz_below (&rng, 6)) 198 { 199 case 0: 200 pos = 0; 201 break; 202 case 1: 203 pos = (int64_t) fuzz_below (&rng, 4096); 204 break; 205 case 2: 206 pos = -(int64_t) fuzz_below (&rng, 4096); 207 break; 208 case 3: 209 pos = (0 > fsize) ? 0 : fsize; 210 break; 211 case 4: 212 pos = INT64_MAX; 213 break; 214 default: 215 pos = INT64_MIN; 216 break; 217 } 218 if ( (0 != (flags & DS_REVERSE)) && 219 (INT64_MIN != pos) ) 220 pos = -pos; 221 (void) EXTRACTOR_datasource_seek_ (ds, pos, whence); 222 } 223 else 224 { 225 (void) EXTRACTOR_datasource_get_size_ (ds, (int) fuzz_below (&rng, 2)); 226 } 227 } 228 EXTRACTOR_datasource_destroy_ (ds); 229 if (0 != (flags & DS_FROM_FILE)) 230 (void) unlink (tmpl); 231 return 0; 232 } 233 234 235 /* ------------------------------------------------------------------ */ 236 /* Generator */ 237 /* ------------------------------------------------------------------ */ 238 239 /** 240 * Build a gzip member by hand. The point is the *header*: the optional 241 * FEXTRA / FNAME / FCOMMENT / FHCRC fields are variable length and are 242 * skipped by a hand-written walker in extractor_datasource.c, which is 243 * where a length that runs off the end of the buffer bites. 244 */ 245 static size_t 246 gen_gzip (struct fuzz_rng *rng, 247 uint8_t *buf, 248 size_t len, 249 size_t cap) 250 { 251 uint8_t flg = 0; 252 unsigned int k; 253 254 if (fuzz_chance (rng, 2)) 255 flg |= 0x04; /* FEXTRA */ 256 if (fuzz_chance (rng, 2)) 257 flg |= 0x08; /* FNAME */ 258 if (fuzz_chance (rng, 2)) 259 flg |= 0x10; /* FCOMMENT */ 260 if (fuzz_chance (rng, 3)) 261 flg |= 0x02; /* FHCRC */ 262 fuzz_put_mem (buf, &len, cap, "\x1f\x8b\x08", 3); 263 fuzz_put_le (buf, &len, cap, flg, 1); 264 fuzz_put_le (buf, &len, cap, 0, 4); /* mtime */ 265 fuzz_put_le (buf, &len, cap, 0, 1); /* xfl */ 266 fuzz_put_le (buf, &len, cap, 3, 1); /* os */ 267 if (0 != (flg & 0x04)) 268 { 269 /* the declared extra length is what matters; usually make it lie */ 270 uint16_t xlen = fuzz_chance (rng, 2) 271 ? (uint16_t) fuzz_below (rng, 0x10000) 272 : (uint16_t) fuzz_below (rng, 32); 273 274 fuzz_put_le (buf, &len, cap, xlen, 2); 275 if (! fuzz_chance (rng, 3)) 276 for (k = 0; (k < xlen) && (len < cap); k++) 277 buf[len++] = fuzz_byte (rng); 278 } 279 if (0 != (flg & 0x08)) 280 { 281 unsigned int n = fuzz_below (rng, 64); 282 283 for (k = 0; (k < n) && (len < cap); k++) 284 buf[len++] = (uint8_t) (1 + fuzz_below (rng, 254)); 285 /* half the time forget the terminator */ 286 if ( (! fuzz_chance (rng, 2)) && (len < cap) ) 287 buf[len++] = 0; 288 } 289 if (0 != (flg & 0x10)) 290 { 291 unsigned int n = fuzz_below (rng, 64); 292 293 for (k = 0; (k < n) && (len < cap); k++) 294 buf[len++] = (uint8_t) (1 + fuzz_below (rng, 254)); 295 if ( (! fuzz_chance (rng, 2)) && (len < cap) ) 296 buf[len++] = 0; 297 } 298 if (0 != (flg & 0x02)) 299 fuzz_put_le (buf, &len, cap, fuzz_next (rng), 2); 300 /* deflate payload: mostly garbage, occasionally a valid stored block */ 301 if (fuzz_chance (rng, 3)) 302 { 303 uint16_t n = (uint16_t) fuzz_below (rng, 64); 304 305 fuzz_put_le (buf, &len, cap, 0x01, 1); /* final, stored */ 306 fuzz_put_le (buf, &len, cap, n, 2); 307 fuzz_put_le (buf, &len, cap, (uint16_t) ~n, 2); 308 for (k = 0; (k < n) && (len < cap); k++) 309 buf[len++] = fuzz_byte (rng); 310 } 311 else 312 { 313 unsigned int n = fuzz_below (rng, 256); 314 315 for (k = 0; (k < n) && (len < cap); k++) 316 buf[len++] = fuzz_byte (rng); 317 } 318 fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); /* crc32 */ 319 fuzz_put_le (buf, &len, cap, 320 fuzz_chance (rng, 2) ? fuzz_next (rng) : 0, 4); /* isize */ 321 return len; 322 } 323 324 325 static size_t 326 fuzz_generate (struct fuzz_rng *rng, 327 uint8_t *buf, 328 size_t cap) 329 { 330 size_t len = 0; 331 uint32_t kind; 332 333 if (cap < 128) 334 return 0; 335 buf[len++] = fuzz_byte (rng); 336 buf[len++] = (uint8_t) fuzz_below (rng, 16); 337 buf[len++] = fuzz_byte (rng); 338 buf[len++] = fuzz_byte (rng); 339 340 kind = fuzz_below (rng, 10); 341 if (kind < 5) 342 { 343 len = gen_gzip (rng, buf, len, cap); 344 } 345 else if (kind < 8) 346 { 347 /* bzip2: the library rejects almost everything, so the value here is 348 in the header-sniffing code rather than in libbz2 */ 349 unsigned int n = fuzz_below (rng, 512); 350 unsigned int k; 351 352 fuzz_put_mem (buf, &len, cap, "BZh", 3); 353 fuzz_put_le (buf, &len, cap, 354 (uint8_t) ('0' + 1 + fuzz_below (rng, 9)), 1); 355 fuzz_put_mem (buf, &len, cap, "\x31\x41\x59\x26\x53\x59", 6); 356 for (k = 0; (k < n) && (len < cap); k++) 357 buf[len++] = fuzz_byte (rng); 358 } 359 else 360 { 361 /* plain data, sometimes with a truncated compression magic so that 362 the sniffing code has to decide on very few bytes */ 363 unsigned int n = fuzz_below (rng, 1024); 364 unsigned int k; 365 366 if (fuzz_chance (rng, 2)) 367 fuzz_put_mem (buf, &len, cap, 368 fuzz_chance (rng, 2) ? "\x1f\x8b" : "BZ", 2); 369 for (k = 0; (k < n) && (len < cap); k++) 370 buf[len++] = fuzz_byte (rng); 371 } 372 return len; 373 } 374 375 376 /* ------------------------------------------------------------------ */ 377 /* Seed corpus */ 378 /* ------------------------------------------------------------------ */ 379 380 #define DS_NSEEDS 10 381 382 static uint8_t ds_seed_buf[DS_NSEEDS][1024]; 383 static size_t ds_seed_len[DS_NSEEDS]; 384 static int ds_seeds_ready; 385 386 387 static void 388 ds_build_seeds (void) 389 { 390 struct fuzz_rng rng; 391 unsigned int i; 392 393 if (ds_seeds_ready) 394 return; 395 ds_seeds_ready = 1; 396 for (i = 0; i < DS_NSEEDS; i++) 397 { 398 fuzz_rng_seed (&rng, 0xD5000u + i); 399 ds_seed_len[i] = fuzz_generate (&rng, 400 ds_seed_buf[i], 401 sizeof (ds_seed_buf[i])); 402 if (ds_seed_len[i] >= 4) 403 { 404 ds_seed_buf[i][0] = (uint8_t) i; 405 ds_seed_buf[i][1] = (uint8_t) (i % 16u); 406 ds_seed_buf[i][2] = 32; 407 } 408 } 409 } 410 411 412 static size_t 413 fuzz_seed_count (void) 414 { 415 ds_build_seeds (); 416 return DS_NSEEDS; 417 } 418 419 420 static const uint8_t * 421 fuzz_seed_get (size_t idx, 422 size_t *len) 423 { 424 ds_build_seeds (); 425 *len = ds_seed_len[idx]; 426 return ds_seed_buf[idx]; 427 }