Commit 1ec5d3daa871d7d983dbdbb83b2e6ac4fc64486e
Committed by
GitHub
Merge pull request #1236 from m-holger/fuzz
Add additional xref reconstruction sanity checks and fuzz test cases
Showing
7 changed files
with
24 additions
and
14 deletions
fuzz/CMakeLists.txt
fuzz/dct_fuzzer_seed_corpus/e0b87af81384c81c7f5c3d71dfe525daeddc1d19
0 → 100644
No preview for this file type
fuzz/qpdf_extra/69977a.fuzz
0 → 100644
No preview for this file type
fuzz/qtest/fuzz.test
| @@ -13,7 +13,7 @@ my $qpdf_corpus = $ENV{'QPDF_FUZZ_CORPUS'} || die "must set QPDF_FUZZ_CORPUS"; | @@ -13,7 +13,7 @@ my $qpdf_corpus = $ENV{'QPDF_FUZZ_CORPUS'} || die "must set QPDF_FUZZ_CORPUS"; | ||
| 13 | 13 | ||
| 14 | my @fuzzers = ( | 14 | my @fuzzers = ( |
| 15 | ['ascii85' => 1], | 15 | ['ascii85' => 1], |
| 16 | - ['dct' => 1], | 16 | + ['dct' => 2], |
| 17 | ['flate' => 1], | 17 | ['flate' => 1], |
| 18 | ['hex' => 1], | 18 | ['hex' => 1], |
| 19 | ['json' => 40], | 19 | ['json' => 40], |
| @@ -21,7 +21,7 @@ my @fuzzers = ( | @@ -21,7 +21,7 @@ my @fuzzers = ( | ||
| 21 | ['pngpredictor' => 1], | 21 | ['pngpredictor' => 1], |
| 22 | ['runlength' => 6], | 22 | ['runlength' => 6], |
| 23 | ['tiffpredictor' => 2], | 23 | ['tiffpredictor' => 2], |
| 24 | - ['qpdf' => 66], # increment when adding new files | 24 | + ['qpdf' => 67], # increment when adding new files |
| 25 | ); | 25 | ); |
| 26 | 26 | ||
| 27 | my $n_tests = 0; | 27 | my $n_tests = 0; |
libqpdf/Pl_DCT.cc
| @@ -35,6 +35,16 @@ error_handler(j_common_ptr cinfo) | @@ -35,6 +35,16 @@ error_handler(j_common_ptr cinfo) | ||
| 35 | longjmp(jerr->jmpbuf, 1); | 35 | longjmp(jerr->jmpbuf, 1); |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | +static void | ||
| 39 | +emit_message(j_common_ptr cinfo, int msg_level) | ||
| 40 | +{ | ||
| 41 | + if (msg_level == -1) { | ||
| 42 | + auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err); | ||
| 43 | + jerr->msg = "Pl_DCT::decompress: JPEG data is corrupt"; | ||
| 44 | + longjmp(jerr->jmpbuf, 1); | ||
| 45 | + } | ||
| 46 | +} | ||
| 47 | + | ||
| 38 | Pl_DCT::Members::Members() : | 48 | Pl_DCT::Members::Members() : |
| 39 | action(a_decompress), | 49 | action(a_decompress), |
| 40 | buf("DCT compressed image") | 50 | buf("DCT compressed image") |
| @@ -116,6 +126,9 @@ Pl_DCT::finish() | @@ -116,6 +126,9 @@ Pl_DCT::finish() | ||
| 116 | cinfo_compress.err = jpeg_std_error(&(jerr.pub)); | 126 | cinfo_compress.err = jpeg_std_error(&(jerr.pub)); |
| 117 | cinfo_decompress.err = jpeg_std_error(&(jerr.pub)); | 127 | cinfo_decompress.err = jpeg_std_error(&(jerr.pub)); |
| 118 | jerr.pub.error_exit = error_handler; | 128 | jerr.pub.error_exit = error_handler; |
| 129 | + if (m->action == a_decompress && throw_on_corrupt_data) { | ||
| 130 | + jerr.pub.emit_message = emit_message; | ||
| 131 | + } | ||
| 119 | 132 | ||
| 120 | bool error = false; | 133 | bool error = false; |
| 121 | // The jpeg library is a "C" library, so we use setjmp and longjmp for exception handling. | 134 | // The jpeg library is a "C" library, so we use setjmp and longjmp for exception handling. |
| @@ -319,11 +332,6 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) | @@ -319,11 +332,6 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) | ||
| 319 | jpeg_buffer_src(cinfo, b); | 332 | jpeg_buffer_src(cinfo, b); |
| 320 | 333 | ||
| 321 | (void)jpeg_read_header(cinfo, TRUE); | 334 | (void)jpeg_read_header(cinfo, TRUE); |
| 322 | - if (throw_on_corrupt_data && cinfo->err->num_warnings > 0) { | ||
| 323 | - // err->num_warnings is the number of corrupt data warnings emitted. | ||
| 324 | - // err->msg_code could also be the code of an informational message. | ||
| 325 | - throw std::runtime_error("Pl_DCT::decompress: JPEG data is corrupt"); | ||
| 326 | - } | ||
| 327 | (void)jpeg_calc_output_dimensions(cinfo); | 335 | (void)jpeg_calc_output_dimensions(cinfo); |
| 328 | unsigned int width = cinfo->output_width * QIntC::to_uint(cinfo->output_components); | 336 | unsigned int width = cinfo->output_width * QIntC::to_uint(cinfo->output_components); |
| 329 | if (memory_limit > 0 && | 337 | if (memory_limit > 0 && |
| @@ -336,14 +344,10 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) | @@ -336,14 +344,10 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) | ||
| 336 | (*cinfo->mem->alloc_sarray)(reinterpret_cast<j_common_ptr>(cinfo), JPOOL_IMAGE, width, 1); | 344 | (*cinfo->mem->alloc_sarray)(reinterpret_cast<j_common_ptr>(cinfo), JPOOL_IMAGE, width, 1); |
| 337 | 345 | ||
| 338 | (void)jpeg_start_decompress(cinfo); | 346 | (void)jpeg_start_decompress(cinfo); |
| 339 | - while (cinfo->output_scanline < cinfo->output_height && | ||
| 340 | - (!throw_on_corrupt_data || cinfo->err->num_warnings == 0)) { | 347 | + while (cinfo->output_scanline < cinfo->output_height) { |
| 341 | (void)jpeg_read_scanlines(cinfo, buffer, 1); | 348 | (void)jpeg_read_scanlines(cinfo, buffer, 1); |
| 342 | getNext()->write(buffer[0], width * sizeof(buffer[0][0])); | 349 | getNext()->write(buffer[0], width * sizeof(buffer[0][0])); |
| 343 | } | 350 | } |
| 344 | (void)jpeg_finish_decompress(cinfo); | 351 | (void)jpeg_finish_decompress(cinfo); |
| 345 | - if (throw_on_corrupt_data && cinfo->err->num_warnings > 0) { | ||
| 346 | - throw std::runtime_error("Pl_DCT::decompress: JPEG data is corrupt"); | ||
| 347 | - } | ||
| 348 | getNext()->finish(); | 352 | getNext()->finish(); |
| 349 | } | 353 | } |
libqpdf/QPDF.cc
| @@ -334,7 +334,7 @@ QPDF::setSuppressWarnings(bool val) | @@ -334,7 +334,7 @@ QPDF::setSuppressWarnings(bool val) | ||
| 334 | void | 334 | void |
| 335 | QPDF::setMaxWarnings(int val) | 335 | QPDF::setMaxWarnings(int val) |
| 336 | { | 336 | { |
| 337 | - m->suppress_warnings = val; | 337 | + m->max_warnings = val; |
| 338 | } | 338 | } |
| 339 | 339 | ||
| 340 | void | 340 | void |
| @@ -641,6 +641,11 @@ QPDF::reconstruct_xref(QPDFExc& e) | @@ -641,6 +641,11 @@ QPDF::reconstruct_xref(QPDFExc& e) | ||
| 641 | 641 | ||
| 642 | throw damagedPDF("", 0, "unable to find trailer dictionary while recovering damaged file"); | 642 | throw damagedPDF("", 0, "unable to find trailer dictionary while recovering damaged file"); |
| 643 | } | 643 | } |
| 644 | + if (m->xref_table.empty()) { | ||
| 645 | + // We cannot check for an empty xref table in parse because empty tables are valid when | ||
| 646 | + // creating QPDF objects from JSON. | ||
| 647 | + throw damagedPDF("", 0, "unable to find objects while recovering damaged file"); | ||
| 648 | + } | ||
| 644 | 649 | ||
| 645 | // We could iterate through the objects looking for streams and try to find objects inside of | 650 | // We could iterate through the objects looking for streams and try to find objects inside of |
| 646 | // them, but it's probably not worth the trouble. Acrobat can't recover files with any errors | 651 | // them, but it's probably not worth the trouble. Acrobat can't recover files with any errors |
qpdf/qtest/qpdf/issue-147.out
| @@ -4,4 +4,4 @@ WARNING: issue-147.pdf: can't find startxref | @@ -4,4 +4,4 @@ WARNING: issue-147.pdf: can't find startxref | ||
| 4 | WARNING: issue-147.pdf: Attempting to reconstruct cross-reference table | 4 | WARNING: issue-147.pdf: Attempting to reconstruct cross-reference table |
| 5 | WARNING: issue-147.pdf (trailer, offset 9): expected dictionary key but found non-name object; inserting key /QPDFFake1 | 5 | WARNING: issue-147.pdf (trailer, offset 9): expected dictionary key but found non-name object; inserting key /QPDFFake1 |
| 6 | WARNING: issue-147.pdf: ignoring object with impossibly large id 62 | 6 | WARNING: issue-147.pdf: ignoring object with impossibly large id 62 |
| 7 | -qpdf: issue-147.pdf: unable to find /Root dictionary | 7 | +qpdf: issue-147.pdf: unable to find objects while recovering damaged file |