Commit b45e3420d6046c9a7258d36e2536b8fb0485c824
Committed by
GitHub
Merge pull request #1228 from m-holger/fuzz7
Add further sanity and loop detection checks
Showing
10 changed files
with
53 additions
and
17 deletions
fuzz/CMakeLists.txt
fuzz/qpdf_extra/69977.fuzz
0 → 100644
No preview for this file type
fuzz/qpdf_fuzzer.cc
| ... | ... | @@ -173,11 +173,11 @@ FuzzHelper::doChecks() |
| 173 | 173 | { |
| 174 | 174 | // Get as much coverage as possible in parts of the library that |
| 175 | 175 | // might benefit from fuzzing. |
| 176 | - std::cout << "starting testWrite\n"; | |
| 176 | + std::cerr << "\ninfo: starting testWrite\n"; | |
| 177 | 177 | testWrite(); |
| 178 | - std::cout << "\nstarting testPages\n\n"; | |
| 178 | + std::cerr << "\ninfo: starting testPages\n"; | |
| 179 | 179 | testPages(); |
| 180 | - std::cout << "\nstarting testOutlines\n\n"; | |
| 180 | + std::cerr << "\ninfo: starting testOutlines\n"; | |
| 181 | 181 | testOutlines(); |
| 182 | 182 | } |
| 183 | 183 | ... | ... |
fuzz/qtest/fuzz.test
include/qpdf/QPDF.hh
| ... | ... | @@ -1502,6 +1502,9 @@ class QPDF |
| 1502 | 1502 | std::shared_ptr<EncryptionParameters> encp; |
| 1503 | 1503 | std::string pdf_version; |
| 1504 | 1504 | std::map<QPDFObjGen, QPDFXRefEntry> xref_table; |
| 1505 | + // Various tables are indexed by object id, with potential size id + 1 | |
| 1506 | + int xref_table_max_id{std::numeric_limits<int>::max() - 1}; | |
| 1507 | + qpdf_offset_t xref_table_max_offset{0}; | |
| 1505 | 1508 | std::set<int> deleted_objects; |
| 1506 | 1509 | std::map<QPDFObjGen, ObjCache> obj_cache; |
| 1507 | 1510 | std::set<QPDFObjGen> resolving; | ... | ... |
libqpdf/Pl_DCT.cc
| ... | ... | @@ -320,7 +320,7 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) |
| 320 | 320 | cinfo->mem->max_memory_to_use = 1'000'000'000; |
| 321 | 321 | // For some corrupt files the memory used internally by libjpeg stays within the above limits |
| 322 | 322 | // even though the size written to the next pipeline is significantly larger. |
| 323 | - m->corrupt_data_limit = 100'000'000; | |
| 323 | + m->corrupt_data_limit = 10'000'000; | |
| 324 | 324 | #endif |
| 325 | 325 | jpeg_buffer_src(cinfo, b); |
| 326 | 326 | ... | ... |
libqpdf/QPDF.cc
| ... | ... | @@ -441,6 +441,12 @@ QPDF::parse(char const* password) |
| 441 | 441 | // 30 characters to leave room for the startxref stuff. |
| 442 | 442 | m->file->seek(0, SEEK_END); |
| 443 | 443 | qpdf_offset_t end_offset = m->file->tell(); |
| 444 | + m->xref_table_max_offset = end_offset; | |
| 445 | + // Sanity check on object ids. All objects must appear in xref table / stream. In all realistic | |
| 446 | + // scenarios at least 3 bytes are required. | |
| 447 | + if (m->xref_table_max_id > m->xref_table_max_offset / 3) { | |
| 448 | + m->xref_table_max_id = static_cast<int>(m->xref_table_max_offset / 3); | |
| 449 | + } | |
| 444 | 450 | qpdf_offset_t start_offset = (end_offset > 1054 ? end_offset - 1054 : 0); |
| 445 | 451 | PatternFinder sf(*this, &QPDF::findStartxref); |
| 446 | 452 | qpdf_offset_t xref_offset = 0; |
| ... | ... | @@ -494,6 +500,13 @@ QPDF::warn(QPDFExc const& e) |
| 494 | 500 | { |
| 495 | 501 | m->warnings.push_back(e); |
| 496 | 502 | if (!m->suppress_warnings) { |
| 503 | +#ifdef QPDF_OSS_FUZZ | |
| 504 | + if (m->warnings.size() > 20) { | |
| 505 | + *m->log->getWarn() << "WARNING: too many warnings - additional warnings surpressed\n"; | |
| 506 | + m->suppress_warnings = true; | |
| 507 | + return; | |
| 508 | + } | |
| 509 | +#endif | |
| 497 | 510 | *m->log->getWarn() << "WARNING: " << m->warnings.back().what() << "\n"; |
| 498 | 511 | } |
| 499 | 512 | } |
| ... | ... | @@ -547,9 +560,6 @@ QPDF::reconstruct_xref(QPDFExc& e) |
| 547 | 560 | |
| 548 | 561 | m->file->seek(0, SEEK_END); |
| 549 | 562 | qpdf_offset_t eof = m->file->tell(); |
| 550 | - // Sanity check on object ids. All objects must appear in xref table / stream. In all realistic | |
| 551 | - // scenarios at leat 3 bytes are required. | |
| 552 | - auto max_obj_id = eof / 3; | |
| 553 | 563 | m->file->seek(0, SEEK_SET); |
| 554 | 564 | qpdf_offset_t line_start = 0; |
| 555 | 565 | // Don't allow very long tokens here during recovery. |
| ... | ... | @@ -567,7 +577,7 @@ QPDF::reconstruct_xref(QPDFExc& e) |
| 567 | 577 | if ((t2.isInteger()) && (readToken(m->file, MAX_LEN).isWord("obj"))) { |
| 568 | 578 | int obj = QUtil::string_to_int(t1.getValue().c_str()); |
| 569 | 579 | int gen = QUtil::string_to_int(t2.getValue().c_str()); |
| 570 | - if (obj <= max_obj_id) { | |
| 580 | + if (obj <= m->xref_table_max_id) { | |
| 571 | 581 | insertReconstructedXrefEntry(obj, token_start, gen); |
| 572 | 582 | } else { |
| 573 | 583 | warn(damagedPDF( |
| ... | ... | @@ -702,7 +712,7 @@ QPDF::read_xref(qpdf_offset_t xref_offset) |
| 702 | 712 | int size = m->trailer.getKey("/Size").getIntValueAsInt(); |
| 703 | 713 | int max_obj = 0; |
| 704 | 714 | if (!m->xref_table.empty()) { |
| 705 | - max_obj = (*(m->xref_table.rbegin())).first.getObj(); | |
| 715 | + max_obj = m->xref_table.rbegin()->first.getObj(); | |
| 706 | 716 | } |
| 707 | 717 | if (!m->deleted_objects.empty()) { |
| 708 | 718 | 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) |
| 1255 | 1265 | // If there is already an entry for this object and generation in the table, it means that a |
| 1256 | 1266 | // later xref table has registered this object. Disregard this one. |
| 1257 | 1267 | |
| 1268 | + if (obj > m->xref_table_max_id) { | |
| 1269 | + // ignore impossibly large object ids or object ids > Size. | |
| 1270 | + return; | |
| 1271 | + } | |
| 1272 | + | |
| 1258 | 1273 | if (m->deleted_objects.count(obj)) { |
| 1259 | 1274 | QTC::TC("qpdf", "QPDF xref deleted object"); |
| 1260 | 1275 | return; |
| 1261 | 1276 | } |
| 1262 | 1277 | |
| 1278 | + if (f0 == 2 && static_cast<int>(f1) == obj) { | |
| 1279 | + warn(damagedPDF("xref stream", "self-referential object stream " + std::to_string(obj))); | |
| 1280 | + return; | |
| 1281 | + } | |
| 1282 | + | |
| 1263 | 1283 | auto [iter, created] = m->xref_table.try_emplace(QPDFObjGen(obj, (f0 == 2 ? 0 : f2))); |
| 1264 | 1284 | if (!created) { |
| 1265 | 1285 | QTC::TC("qpdf", "QPDF xref reused object"); |
| ... | ... | @@ -1296,12 +1316,11 @@ QPDF::insertFreeXrefEntry(QPDFObjGen og) |
| 1296 | 1316 | void |
| 1297 | 1317 | QPDF::insertReconstructedXrefEntry(int obj, qpdf_offset_t f1, int f2) |
| 1298 | 1318 | { |
| 1299 | - // Various tables are indexed by object id, with potential size id + 1 | |
| 1300 | - constexpr static int max_id = std::numeric_limits<int>::max() - 1; | |
| 1301 | - if (!(obj > 0 && obj <= max_id && 0 <= f2 && f2 < 65535)) { | |
| 1319 | + if (!(obj > 0 && obj <= m->xref_table_max_id && 0 <= f2 && f2 < 65535)) { | |
| 1302 | 1320 | QTC::TC("qpdf", "QPDF xref overwrite invalid objgen"); |
| 1303 | 1321 | return; |
| 1304 | 1322 | } |
| 1323 | + | |
| 1305 | 1324 | QPDFObjGen og(obj, f2); |
| 1306 | 1325 | if (!m->deleted_objects.count(obj)) { |
| 1307 | 1326 | // deleted_objects stores the uncompressed objects removed from the xref table at the start |
| ... | ... | @@ -1911,6 +1930,17 @@ QPDF::resolveObjectsInStream(int obj_stream_number) |
| 1911 | 1930 | |
| 1912 | 1931 | int num = QUtil::string_to_int(tnum.getValue().c_str()); |
| 1913 | 1932 | long long offset = QUtil::string_to_int(toffset.getValue().c_str()); |
| 1933 | + if (num > m->xref_table_max_id) { | |
| 1934 | + continue; | |
| 1935 | + } | |
| 1936 | + if (num == obj_stream_number) { | |
| 1937 | + warn(damagedPDF( | |
| 1938 | + input, | |
| 1939 | + m->last_object_description, | |
| 1940 | + input->getLastOffset(), | |
| 1941 | + "object stream claims to contain itself")); | |
| 1942 | + continue; | |
| 1943 | + } | |
| 1914 | 1944 | offsets[num] = toI(offset + first); |
| 1915 | 1945 | } |
| 1916 | 1946 | |
| ... | ... | @@ -1922,8 +1952,9 @@ QPDF::resolveObjectsInStream(int obj_stream_number) |
| 1922 | 1952 | m->last_object_description += "object "; |
| 1923 | 1953 | for (auto const& iter: offsets) { |
| 1924 | 1954 | QPDFObjGen og(iter.first, 0); |
| 1925 | - QPDFXRefEntry const& entry = m->xref_table[og]; | |
| 1926 | - if ((entry.getType() == 2) && (entry.getObjStreamNumber() == obj_stream_number)) { | |
| 1955 | + auto entry = m->xref_table.find(og); | |
| 1956 | + if (entry != m->xref_table.end() && entry->second.getType() == 2 && | |
| 1957 | + entry->second.getObjStreamNumber() == obj_stream_number) { | |
| 1927 | 1958 | int offset = iter.second; |
| 1928 | 1959 | input->seek(offset, SEEK_SET); |
| 1929 | 1960 | QPDFObjectHandle oh = readObjectInStream(input, iter.first); | ... | ... |
qpdf/qtest/qpdf/issue-118.out
| 1 | 1 | WARNING: issue-118.pdf: can't find PDF header |
| 2 | -WARNING: issue-118.pdf (offset 732): loop detected resolving object 2 0 | |
| 3 | -WARNING: issue-118.pdf (xref stream: object 8 0, offset 732): supposed object stream 2 is not a stream | |
| 2 | +WARNING: issue-118.pdf (xref stream, offset 732): self-referential object stream 2 | |
| 4 | 3 | issue-118.pdf: unable to find /Root dictionary | ... | ... |
qpdf/qtest/qpdf/issue-120.out
qpdf/qtest/qpdf/issue-143.out
| ... | ... | @@ -3,6 +3,7 @@ WARNING: issue-143.pdf (xref stream: object 3 0, offset 654): stream keyword not |
| 3 | 3 | WARNING: issue-143.pdf (xref stream: object 3 0, offset 607): stream dictionary lacks /Length key |
| 4 | 4 | WARNING: issue-143.pdf (xref stream: object 3 0, offset 654): attempting to recover stream length |
| 5 | 5 | WARNING: issue-143.pdf (xref stream: object 3 0, offset 654): recovered stream length: 36 |
| 6 | +WARNING: issue-143.pdf (xref stream, offset 654): self-referential object stream 3 | |
| 6 | 7 | WARNING: issue-143.pdf: file is damaged |
| 7 | 8 | WARNING: issue-143.pdf (object 1 0, offset 48): expected n n obj |
| 8 | 9 | WARNING: issue-143.pdf: Attempting to reconstruct cross-reference table | ... | ... |