fuzz_ec.h (16251B)
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_ec.h 22 * @brief contract-exact model of `struct EXTRACTOR_ExtractContext` 23 * @author Christian Grothoff 24 * 25 * This is what makes the plugin harnesses find things that running 26 * `extract` over a mutated file does not. Two properties matter: 27 * 28 * 1. **The read window is exact.** `plugin_env_read()` in 29 * extractor_plugin_main.c hands the plugin a pointer *into the shared 30 * memory window* and returns how many bytes are valid there -- which 31 * is at most `shm_map_size` (16 KiB by default), and at most what is 32 * left of the file. A plugin that asks for 100 KiB and gets 16 KiB 33 * but then dereferences all 100 KiB reads memory it was never given. 34 * In production that memory is a live mmap, so nothing crashes and 35 * the bug is invisible; here every window is a fresh `malloc()` of 36 * *exactly* the returned byte count, so AddressSanitizer's redzone 37 * turns the same access into a hard error. 38 * 39 * The window size is fuzzer-controlled, because "read() returned less 40 * than I asked for" is the single most productive precondition in 41 * this library and a 16 KiB default hides it for every file smaller 42 * than that. 43 * 44 * 2. **The window slides.** The pointer a plugin got from `read()` stays 45 * valid only for as long as the shared memory window it points into 46 * still covers that part of the file. A `read()` or `seek()` that 47 * needs data outside the window makes the core refill it, and the 48 * plugin's old pointer then addresses *different file bytes* than the 49 * plugin believes it holds. The model tracks the same window as 50 * `plugin_env_read()`/`plugin_env_seek()` do -- so a `seek(0, 51 * SEEK_CUR)`, which never leaves the window, does not invalidate 52 * anything, exactly as in production -- and frees every slice handed 53 * out from a window when that window slides, so ASAN reports a 54 * retained pointer as a use-after-free. 55 * 56 * Set `LE_FUZZ_STRICT_WINDOW=1` to invalidate on *every* call instead. 57 * That models the in-process implementation, whose single `ctx->buf` 58 * is overwritten by each read; it finds more, at the price of also 59 * flagging plugins that are correct under the default out-of-process 60 * policy. 61 * 62 * On top of that the metadata processor is an oracle. The plugin-side 63 * `transmit_reply()` writes `data_len` bytes from `data` to a pipe and 64 * calls `strlen()` on the mime type, so this harness touches exactly the 65 * same bytes -- an over-long `data_len` is a real out-of-bounds read in 66 * production, not an artefact of the harness. 67 */ 68 #ifndef LE_FUZZ_EC_H 69 #define LE_FUZZ_EC_H 1 70 71 #include "platform.h" 72 #include "extractor.h" 73 #include "fuzz_common.h" 74 75 /** 76 * Size of the shared memory window the core actually uses; must be kept 77 * in sync with #DEFAULT_SHM_SIZE in src/main/extractor.c. 78 */ 79 #define LE_FUZZ_DEFAULT_SHM (16 * 1024) 80 81 /** 82 * Upper bound the plugin side enforces on a single meta data item; see 83 * #MAX_META_DATA in src/main/extractor_ipc.h. 84 */ 85 #define LE_FUZZ_MAX_META_DATA (32 * 1024) 86 87 /** 88 * Number of configuration bytes that precede the file image in every 89 * plugin-harness input. An all-zero prefix means "no fault injection, 90 * real 16 KiB window", i.e. exactly what production does, so a corpus 91 * entry is just four zero bytes followed by the file. 92 */ 93 #define LE_FUZZ_EC_PREFIX 4 94 95 /** 96 * Candidate read-window sizes. Index 0 is the production value; the 97 * rest deliberately include sizes that force a short read on almost 98 * every call, and sizes just below/above a power of two. 99 */ 100 static const size_t le_fuzz_windows[] = { 101 LE_FUZZ_DEFAULT_SHM, 1, 2, 3, 5, 7, 15, 16, 31, 64, 127, 255, 256, 102 511, 1024, 4095, 8192, 16383, 16385, 32768, 65536 103 }; 104 105 /** 106 * State behind the #EXTRACTOR_ExtractContext handed to the plugin. 107 */ 108 struct fuzz_ec_state 109 { 110 /** 111 * The file image. Owned by the caller. 112 */ 113 const uint8_t *img; 114 115 /** 116 * Number of bytes in #img. 117 */ 118 size_t img_len; 119 120 /** 121 * Current read position, in [0, #img_len]. 122 */ 123 uint64_t pos; 124 125 /** 126 * Size of the shared memory window, i.e. the largest number of bytes 127 * a single read() may return. Models `shm_map_size`. 128 */ 129 size_t window; 130 131 /** 132 * File offset the current window starts at. Models `shm_off`. 133 */ 134 uint64_t win_off; 135 136 /** 137 * Number of bytes of the file the current window holds. Models 138 * `shm_ready_bytes`. 139 */ 140 size_t win_len; 141 142 /** 143 * Non-zero once a window has been established at all. 144 */ 145 int win_valid; 146 147 /** 148 * Slices handed out from the current window. Each is an exact-size 149 * allocation, so ASAN's redzone starts right after the last byte the 150 * plugin was promised; all of them are freed when the window slides. 151 */ 152 uint8_t **slices; 153 154 /** 155 * Number of entries in #slices. 156 */ 157 size_t slices_len; 158 159 /** 160 * Capacity of #slices. Grown geometrically: a plugin can issue 161 * millions of one-byte reads inside a single window, and a 162 * grow-by-one realloc() would make the *harness* quadratic and turn 163 * every such input into a bogus "slow unit". 164 */ 165 size_t slices_cap; 166 167 /** 168 * Number of read() calls so far. 169 */ 170 unsigned int reads; 171 172 /** 173 * Number of seek() calls so far. 174 */ 175 unsigned int seeks; 176 177 /** 178 * Number of proc() calls so far. 179 */ 180 unsigned int procs; 181 182 /** 183 * Fault-injection bitmask, see #LE_FUZZ_FAULT_*. 184 */ 185 unsigned int faults; 186 187 /** 188 * Call index at which an injected fault fires. 189 */ 190 unsigned int fault_at; 191 192 /** 193 * Set once get_size() has returned UINT64_MAX; the failure is 194 * transient in production (one failed IPC round), so it only fires 195 * once here too. 196 */ 197 int size_failed; 198 }; 199 200 #define LE_FUZZ_FAULT_SIZE 0x01 201 #define LE_FUZZ_FAULT_READ 0x02 202 #define LE_FUZZ_FAULT_SEEK 0x04 203 #define LE_FUZZ_FAULT_STOP 0x08 204 #define LE_FUZZ_RUN_TWICE 0x10 205 #define LE_FUZZ_ODD_ALIGN 0x20 206 207 /** 208 * Cached value of $LE_FUZZ_STRICT_WINDOW. 209 */ 210 static int le_fuzz_strict_window = -1; 211 212 /** 213 * Cached value of $LE_FUZZ_STRICT_PROC. 214 */ 215 static int le_fuzz_strict_proc = -1; 216 217 218 /** 219 * Free every slice handed out from the window that is about to be 220 * replaced. In production those pointers keep addressing live shared 221 * memory, but the bytes behind them are no longer the ones the plugin 222 * read, so any use of them is a defect; freeing turns that into an ASAN 223 * report. 224 * 225 * @param st harness state 226 */ 227 static void 228 le_fuzz_drop_slices (struct fuzz_ec_state *st) 229 { 230 size_t i; 231 232 for (i = 0; i < st->slices_len; i++) 233 free (st->slices[i]); 234 free (st->slices); 235 st->slices = NULL; 236 st->slices_len = 0; 237 st->slices_cap = 0; 238 } 239 240 241 /** 242 * Slide the window so that it starts at @a off. 243 * 244 * @param st harness state 245 * @param off new window start 246 */ 247 static void 248 le_fuzz_slide (struct fuzz_ec_state *st, 249 uint64_t off) 250 { 251 le_fuzz_drop_slices (st); 252 st->win_off = off; 253 st->win_len = (size_t) (st->img_len - off); 254 if (st->win_len > st->window) 255 st->win_len = st->window; 256 st->win_valid = 1; 257 } 258 259 260 /** 261 * @return non-zero if @a off is covered by the current window 262 */ 263 static int 264 le_fuzz_in_window (const struct fuzz_ec_state *st, 265 uint64_t off) 266 { 267 if (! st->win_valid) 268 return 0; 269 if (le_fuzz_strict_window) 270 return 0; 271 return ( (st->win_off <= off) && 272 (off < st->win_off + st->win_len) ); 273 } 274 275 276 /** 277 * Hand out an exact-size copy of @a n bytes starting at file offset 278 * @a off. The allocation is remembered so that it can be freed when the 279 * window slides. 280 * 281 * @param st harness state 282 * @param off file offset 283 * @param n number of bytes 284 * @return pointer the plugin may read @a n bytes from 285 */ 286 static uint8_t * 287 le_fuzz_slice (struct fuzz_ec_state *st, 288 uint64_t off, 289 size_t n) 290 { 291 uint8_t *p = (uint8_t *) malloc ((0 == n) ? 1 : n); 292 293 if (NULL == p) 294 abort (); 295 if (0 != n) 296 memcpy (p, st->img + off, n); 297 if (st->slices_len == st->slices_cap) 298 { 299 size_t cap = (0 == st->slices_cap) ? 64 : st->slices_cap * 2; 300 uint8_t **v = (uint8_t **) realloc (st->slices, 301 sizeof (uint8_t *) * cap); 302 303 if (NULL == v) 304 abort (); 305 st->slices = v; 306 st->slices_cap = cap; 307 } 308 st->slices[st->slices_len++] = p; 309 return p; 310 } 311 312 313 /** 314 * Model of `plugin_env_read()` from src/main/extractor_plugin_main.c. 315 * 316 * @param cls a `struct fuzz_ec_state` 317 * @param[out] data set to the start of the window 318 * @param count number of bytes requested 319 * @return number of bytes available at @a data, -1 on error 320 */ 321 static ssize_t 322 le_fuzz_read (void *cls, 323 void **data, 324 size_t count) 325 { 326 struct fuzz_ec_state *st = cls; 327 328 *data = NULL; 329 st->reads++; 330 if ( (0 != (st->faults & LE_FUZZ_FAULT_READ)) && 331 (st->reads == st->fault_at) ) 332 return -1; /* models a failed IPC round */ 333 /* clamp to what is left of the file, exactly as plugin_env_read() 334 does (including its overflow guard) */ 335 if ( (count + st->pos > st->img_len) || 336 (count + st->pos < st->pos) ) 337 count = (size_t) (st->img_len - st->pos); 338 /* refill the window if the read starts outside it, as 339 plugin_env_read() does via its plugin_env_seek() call */ 340 if (! le_fuzz_in_window (st, st->pos)) 341 le_fuzz_slide (st, st->pos); 342 /* clamp to what the window holds */ 343 if (st->pos + count > st->win_off + st->win_len) 344 count = (size_t) (st->win_off + st->win_len - st->pos); 345 *data = le_fuzz_slice (st, st->pos, count); 346 st->pos += count; 347 return (ssize_t) count; 348 } 349 350 351 /** 352 * Model of `plugin_env_seek()` from src/main/extractor_plugin_main.c. 353 * 354 * @param cls a `struct fuzz_ec_state` 355 * @param pos offset to seek to 356 * @param whence SEEK_SET, SEEK_CUR or SEEK_END 357 * @return new absolute position, -1 on error 358 */ 359 static int64_t 360 le_fuzz_seek (void *cls, 361 int64_t pos, 362 int whence) 363 { 364 struct fuzz_ec_state *st = cls; 365 uint64_t npos; 366 367 st->seeks++; 368 if ( (0 != (st->faults & LE_FUZZ_FAULT_SEEK)) && 369 (st->seeks == st->fault_at) ) 370 return -1; 371 switch (whence) 372 { 373 case SEEK_CUR: 374 if ( (pos < 0) && 375 (st->pos < (uint64_t) (-pos)) ) 376 return -1; 377 pos = (int64_t) (st->pos + pos); 378 break; 379 case SEEK_END: 380 if (pos > 0) 381 return -1; 382 pos = (int64_t) (st->img_len + pos); 383 break; 384 case SEEK_SET: 385 break; 386 default: 387 return -1; 388 } 389 if ( (pos < 0) || 390 ((uint64_t) pos > st->img_len) ) 391 return -1; 392 npos = (uint64_t) pos; 393 /* plugin_env_seek() returns without touching the window whenever the 394 target is already inside it; otherwise the core refills it and every 395 pointer into the old window goes stale */ 396 if (! le_fuzz_in_window (st, npos)) 397 le_fuzz_slide (st, npos); 398 st->pos = npos; 399 return (int64_t) npos; 400 } 401 402 403 /** 404 * Model of `plugin_env_get_size()`. 405 * 406 * @param cls a `struct fuzz_ec_state` 407 * @return file size, UINT64_MAX when the injected IPC failure fires 408 */ 409 static uint64_t 410 le_fuzz_get_size (void *cls) 411 { 412 struct fuzz_ec_state *st = cls; 413 414 if ( (0 != (st->faults & LE_FUZZ_FAULT_SIZE)) && 415 (! st->size_failed) ) 416 { 417 st->size_failed = 1; 418 return UINT64_MAX; /* documented "IPC failure" return */ 419 } 420 return (uint64_t) st->img_len; 421 } 422 423 424 /** 425 * Metadata processor used as an oracle. Touches precisely the bytes 426 * that `transmit_reply()` in extractor_plugin_main.c touches. 427 * 428 * @param cls a `struct fuzz_ec_state` 429 * @param plugin_name name of the reporting plugin 430 * @param type metadata type 431 * @param format metadata format 432 * @param data_mime_type mime type of @a data, may be NULL 433 * @param data the metadata 434 * @param data_len number of bytes in @a data 435 * @return 0 to continue, 1 to abort 436 */ 437 static int 438 le_fuzz_proc (void *cls, 439 const char *plugin_name, 440 enum EXTRACTOR_MetaType type, 441 enum EXTRACTOR_MetaFormat format, 442 const char *data_mime_type, 443 const char *data, 444 size_t data_len) 445 { 446 struct fuzz_ec_state *st = cls; 447 volatile unsigned int sink = 0; 448 size_t i; 449 450 st->procs++; 451 if (NULL == plugin_name) 452 fuzz_report_finding ("plugin reported a NULL plugin name"); 453 /* transmit_reply() does strlen() on both of these */ 454 sink += (unsigned int) strlen (plugin_name); 455 if (NULL != data_mime_type) 456 sink += (unsigned int) strlen (data_mime_type); 457 if ( (NULL == data) && 458 (0 != data_len) ) 459 fuzz_report_finding ("plugin reported NULL data with a non-zero length"); 460 if (NULL != data) 461 { 462 /* transmit_reply() writes exactly data_len bytes starting at data; 463 reading them here is what production does */ 464 for (i = 0; i < data_len; i++) 465 sink += (unsigned char) data[i]; 466 if ( (le_fuzz_strict_proc) && 467 (0 != data_len) && 468 ( (EXTRACTOR_METAFORMAT_UTF8 == format) || 469 (EXTRACTOR_METAFORMAT_C_STRING == format) ) && 470 ('\0' != data[data_len - 1]) ) 471 fuzz_report_finding ("plugin reported a string metadata value that is " 472 "not 0-terminated"); 473 } 474 (void) sink; 475 (void) type; 476 if ( (0 != (st->faults & LE_FUZZ_FAULT_STOP)) && 477 (st->procs > st->fault_at) ) 478 return 1; /* application asked us to stop */ 479 return 0; 480 } 481 482 483 /** 484 * Read the harness-wide environment knobs once. 485 */ 486 static void 487 le_fuzz_ec_init_env (void) 488 { 489 if (-1 != le_fuzz_strict_window) 490 return; 491 le_fuzz_strict_window = (0 != fuzz_env_ulong ("LE_FUZZ_STRICT_WINDOW", 0)); 492 le_fuzz_strict_proc = (0 != fuzz_env_ulong ("LE_FUZZ_STRICT_PROC", 0)); 493 } 494 495 496 /** 497 * Set up @a ec and @a st from a fuzz input. The first 498 * #LE_FUZZ_EC_PREFIX bytes are configuration, the rest is the file 499 * image. 500 * 501 * @param[out] ec context to fill in 502 * @param[out] st state to fill in 503 * @param data the fuzz input 504 * @param size number of bytes in @a data 505 * @return 0 if the input was too short to be usable 506 */ 507 static int 508 le_fuzz_ec_setup (struct EXTRACTOR_ExtractContext *ec, 509 struct fuzz_ec_state *st, 510 const uint8_t *data, 511 size_t size) 512 { 513 size_t off; 514 515 le_fuzz_ec_init_env (); 516 if (size < LE_FUZZ_EC_PREFIX) 517 return 0; 518 memset (st, 0, sizeof (*st)); 519 st->window = le_fuzz_windows[data[0] 520 % (sizeof (le_fuzz_windows) 521 / sizeof (le_fuzz_windows[0]))]; 522 st->faults = data[1]; 523 st->fault_at = data[2]; 524 off = LE_FUZZ_EC_PREFIX; 525 if ( (0 != (st->faults & LE_FUZZ_ODD_ALIGN)) && 526 (off < size) ) 527 off++; 528 st->img = data + off; 529 st->img_len = size - off; 530 memset (ec, 0, sizeof (*ec)); 531 ec->cls = st; 532 ec->config = NULL; 533 ec->read = &le_fuzz_read; 534 ec->seek = &le_fuzz_seek; 535 ec->get_size = &le_fuzz_get_size; 536 ec->proc = &le_fuzz_proc; 537 return 1; 538 } 539 540 541 /** 542 * Release everything @a st still owns. 543 * 544 * @param st state to clean up 545 */ 546 static void 547 le_fuzz_ec_cleanup (struct fuzz_ec_state *st) 548 { 549 le_fuzz_drop_slices (st); 550 st->win_valid = 0; 551 st->win_off = 0; 552 st->win_len = 0; 553 } 554 555 556 /** 557 * Rewind @a st so that the same context can be handed to the extract 558 * method a second time. Plugins must be stateless across files. 559 * 560 * @param st state to reset 561 */ 562 static void 563 le_fuzz_ec_rewind (struct fuzz_ec_state *st) 564 { 565 le_fuzz_ec_cleanup (st); 566 st->pos = 0; 567 st->reads = 0; 568 st->seeks = 0; 569 st->procs = 0; 570 st->size_failed = 0; 571 } 572 573 574 #endif /* LE_FUZZ_EC_H */