diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt index d492bfea..98c980d6 100644 --- a/fuzz/CMakeLists.txt +++ b/fuzz/CMakeLists.txt @@ -121,6 +121,7 @@ set(CORPUS_OTHER 69857.fuzz 69913.fuzz 69969.fuzz + 69977.fuzz ) set(CORPUS_DIR ${CMAKE_CURRENT_BINARY_DIR}/qpdf_corpus) diff --git a/fuzz/qpdf_extra/69977.fuzz b/fuzz/qpdf_extra/69977.fuzz new file mode 100644 index 00000000..f15c5c98 Binary files /dev/null and b/fuzz/qpdf_extra/69977.fuzz differ diff --git a/fuzz/qpdf_fuzzer.cc b/fuzz/qpdf_fuzzer.cc index 678bec8e..814c59c4 100644 --- a/fuzz/qpdf_fuzzer.cc +++ b/fuzz/qpdf_fuzzer.cc @@ -173,11 +173,11 @@ FuzzHelper::doChecks() { // Get as much coverage as possible in parts of the library that // might benefit from fuzzing. - std::cout << "starting testWrite\n"; + std::cerr << "\ninfo: starting testWrite\n"; testWrite(); - std::cout << "\nstarting testPages\n\n"; + std::cerr << "\ninfo: starting testPages\n"; testPages(); - std::cout << "\nstarting testOutlines\n\n"; + std::cerr << "\ninfo: starting testOutlines\n"; testOutlines(); } diff --git a/fuzz/qtest/fuzz.test b/fuzz/qtest/fuzz.test index fffecc19..49ae52d9 100644 --- a/fuzz/qtest/fuzz.test +++ b/fuzz/qtest/fuzz.test @@ -21,7 +21,7 @@ my @fuzzers = ( ['pngpredictor' => 1], ['runlength' => 6], ['tiffpredictor' => 2], - ['qpdf' => 63], # increment when adding new files + ['qpdf' => 64], # increment when adding new files ); my $n_tests = 0; diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index a922ae9d..c83bcb0b 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -1502,6 +1502,9 @@ class QPDF std::shared_ptr encp; std::string pdf_version; std::map xref_table; + // Various tables are indexed by object id, with potential size id + 1 + int xref_table_max_id{std::numeric_limits::max() - 1}; + qpdf_offset_t xref_table_max_offset{0}; std::set deleted_objects; std::map obj_cache; std::set resolving; diff --git a/libqpdf/Pl_DCT.cc b/libqpdf/Pl_DCT.cc index 5875a0e9..d2544ab0 100644 --- a/libqpdf/Pl_DCT.cc +++ b/libqpdf/Pl_DCT.cc @@ -320,7 +320,7 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) cinfo->mem->max_memory_to_use = 1'000'000'000; // For some corrupt files the memory used internally by libjpeg stays within the above limits // even though the size written to the next pipeline is significantly larger. - m->corrupt_data_limit = 100'000'000; + m->corrupt_data_limit = 10'000'000; #endif jpeg_buffer_src(cinfo, b); diff --git a/libqpdf/QPDF.cc b/libqpdf/QPDF.cc index f46885a1..915518af 100644 --- a/libqpdf/QPDF.cc +++ b/libqpdf/QPDF.cc @@ -441,6 +441,12 @@ QPDF::parse(char const* password) // 30 characters to leave room for the startxref stuff. m->file->seek(0, SEEK_END); qpdf_offset_t end_offset = m->file->tell(); + m->xref_table_max_offset = end_offset; + // Sanity check on object ids. All objects must appear in xref table / stream. In all realistic + // scenarios at least 3 bytes are required. + if (m->xref_table_max_id > m->xref_table_max_offset / 3) { + m->xref_table_max_id = static_cast(m->xref_table_max_offset / 3); + } qpdf_offset_t start_offset = (end_offset > 1054 ? end_offset - 1054 : 0); PatternFinder sf(*this, &QPDF::findStartxref); qpdf_offset_t xref_offset = 0; @@ -494,6 +500,13 @@ QPDF::warn(QPDFExc const& e) { m->warnings.push_back(e); if (!m->suppress_warnings) { +#ifdef QPDF_OSS_FUZZ + if (m->warnings.size() > 20) { + *m->log->getWarn() << "WARNING: too many warnings - additional warnings surpressed\n"; + m->suppress_warnings = true; + return; + } +#endif *m->log->getWarn() << "WARNING: " << m->warnings.back().what() << "\n"; } } @@ -547,9 +560,6 @@ QPDF::reconstruct_xref(QPDFExc& e) m->file->seek(0, SEEK_END); qpdf_offset_t eof = m->file->tell(); - // Sanity check on object ids. All objects must appear in xref table / stream. In all realistic - // scenarios at leat 3 bytes are required. - auto max_obj_id = eof / 3; m->file->seek(0, SEEK_SET); qpdf_offset_t line_start = 0; // Don't allow very long tokens here during recovery. @@ -567,7 +577,7 @@ QPDF::reconstruct_xref(QPDFExc& e) if ((t2.isInteger()) && (readToken(m->file, MAX_LEN).isWord("obj"))) { int obj = QUtil::string_to_int(t1.getValue().c_str()); int gen = QUtil::string_to_int(t2.getValue().c_str()); - if (obj <= max_obj_id) { + if (obj <= m->xref_table_max_id) { insertReconstructedXrefEntry(obj, token_start, gen); } else { warn(damagedPDF( @@ -702,7 +712,7 @@ QPDF::read_xref(qpdf_offset_t xref_offset) int size = m->trailer.getKey("/Size").getIntValueAsInt(); int max_obj = 0; if (!m->xref_table.empty()) { - max_obj = (*(m->xref_table.rbegin())).first.getObj(); + max_obj = m->xref_table.rbegin()->first.getObj(); } if (!m->deleted_objects.empty()) { max_obj = std::max(max_obj, *(m->deleted_objects.rbegin())); @@ -1255,11 +1265,21 @@ QPDF::insertXrefEntry(int obj, int f0, qpdf_offset_t f1, int f2) // If there is already an entry for this object and generation in the table, it means that a // later xref table has registered this object. Disregard this one. + if (obj > m->xref_table_max_id) { + // ignore impossibly large object ids or object ids > Size. + return; + } + if (m->deleted_objects.count(obj)) { QTC::TC("qpdf", "QPDF xref deleted object"); return; } + if (f0 == 2 && static_cast(f1) == obj) { + warn(damagedPDF("xref stream", "self-referential object stream " + std::to_string(obj))); + return; + } + auto [iter, created] = m->xref_table.try_emplace(QPDFObjGen(obj, (f0 == 2 ? 0 : f2))); if (!created) { QTC::TC("qpdf", "QPDF xref reused object"); @@ -1296,12 +1316,11 @@ QPDF::insertFreeXrefEntry(QPDFObjGen og) void QPDF::insertReconstructedXrefEntry(int obj, qpdf_offset_t f1, int f2) { - // Various tables are indexed by object id, with potential size id + 1 - constexpr static int max_id = std::numeric_limits::max() - 1; - if (!(obj > 0 && obj <= max_id && 0 <= f2 && f2 < 65535)) { + if (!(obj > 0 && obj <= m->xref_table_max_id && 0 <= f2 && f2 < 65535)) { QTC::TC("qpdf", "QPDF xref overwrite invalid objgen"); return; } + QPDFObjGen og(obj, f2); if (!m->deleted_objects.count(obj)) { // deleted_objects stores the uncompressed objects removed from the xref table at the start @@ -1911,6 +1930,17 @@ QPDF::resolveObjectsInStream(int obj_stream_number) int num = QUtil::string_to_int(tnum.getValue().c_str()); long long offset = QUtil::string_to_int(toffset.getValue().c_str()); + if (num > m->xref_table_max_id) { + continue; + } + if (num == obj_stream_number) { + warn(damagedPDF( + input, + m->last_object_description, + input->getLastOffset(), + "object stream claims to contain itself")); + continue; + } offsets[num] = toI(offset + first); } @@ -1922,8 +1952,9 @@ QPDF::resolveObjectsInStream(int obj_stream_number) m->last_object_description += "object "; for (auto const& iter: offsets) { QPDFObjGen og(iter.first, 0); - QPDFXRefEntry const& entry = m->xref_table[og]; - if ((entry.getType() == 2) && (entry.getObjStreamNumber() == obj_stream_number)) { + auto entry = m->xref_table.find(og); + if (entry != m->xref_table.end() && entry->second.getType() == 2 && + entry->second.getObjStreamNumber() == obj_stream_number) { int offset = iter.second; input->seek(offset, SEEK_SET); QPDFObjectHandle oh = readObjectInStream(input, iter.first); diff --git a/qpdf/qtest/qpdf/issue-118.out b/qpdf/qtest/qpdf/issue-118.out index 1a5f3f57..2b219b20 100644 --- a/qpdf/qtest/qpdf/issue-118.out +++ b/qpdf/qtest/qpdf/issue-118.out @@ -1,4 +1,3 @@ WARNING: issue-118.pdf: can't find PDF header -WARNING: issue-118.pdf (offset 732): loop detected resolving object 2 0 -WARNING: issue-118.pdf (xref stream: object 8 0, offset 732): supposed object stream 2 is not a stream +WARNING: issue-118.pdf (xref stream, offset 732): self-referential object stream 2 issue-118.pdf: unable to find /Root dictionary diff --git a/qpdf/qtest/qpdf/issue-120.out b/qpdf/qtest/qpdf/issue-120.out index cc03ccfa..dbef34db 100644 --- a/qpdf/qtest/qpdf/issue-120.out +++ b/qpdf/qtest/qpdf/issue-120.out @@ -1 +1,2 @@ +WARNING: issue-120.pdf (xref stream, offset 712): self-referential object stream 3 qpdf: issue-120.pdf: unable to find page tree diff --git a/qpdf/qtest/qpdf/issue-143.out b/qpdf/qtest/qpdf/issue-143.out index 44144b4d..7f787278 100644 --- a/qpdf/qtest/qpdf/issue-143.out +++ b/qpdf/qtest/qpdf/issue-143.out @@ -3,6 +3,7 @@ WARNING: issue-143.pdf (xref stream: object 3 0, offset 654): stream keyword not WARNING: issue-143.pdf (xref stream: object 3 0, offset 607): stream dictionary lacks /Length key WARNING: issue-143.pdf (xref stream: object 3 0, offset 654): attempting to recover stream length WARNING: issue-143.pdf (xref stream: object 3 0, offset 654): recovered stream length: 36 +WARNING: issue-143.pdf (xref stream, offset 654): self-referential object stream 3 WARNING: issue-143.pdf: file is damaged WARNING: issue-143.pdf (object 1 0, offset 48): expected n n obj WARNING: issue-143.pdf: Attempting to reconstruct cross-reference table