Commit bcf81a1423f9a539e4fc05e595ea5f6560590737
Committed by
GitHub
Merge pull request #1242 from m-holger/fuzz
Tighten page tree checks
Showing
9 changed files
with
32 additions
and
12 deletions
fuzz/CMakeLists.txt
| @@ -127,7 +127,9 @@ set(CORPUS_OTHER | @@ -127,7 +127,9 @@ set(CORPUS_OTHER | ||
| 127 | 69977b.fuzz | 127 | 69977b.fuzz |
| 128 | 69977c.fuzz | 128 | 69977c.fuzz |
| 129 | 70055.fuzz | 129 | 70055.fuzz |
| 130 | - 4599089157701632.fuzz | 130 | + 70245.fuzz |
| 131 | + 70306.fuzz | ||
| 132 | + 4826608268017664.fuzz | ||
| 131 | ) | 133 | ) |
| 132 | 134 | ||
| 133 | set(CORPUS_DIR ${CMAKE_CURRENT_BINARY_DIR}/qpdf_corpus) | 135 | set(CORPUS_DIR ${CMAKE_CURRENT_BINARY_DIR}/qpdf_corpus) |
fuzz/json_fuzzer.cc
| @@ -33,6 +33,7 @@ FuzzHelper::doChecks() | @@ -33,6 +33,7 @@ FuzzHelper::doChecks() | ||
| 33 | std::cerr << "runtime_error parsing json: " << e.what() << std::endl; | 33 | std::cerr << "runtime_error parsing json: " << e.what() << std::endl; |
| 34 | } | 34 | } |
| 35 | QPDF q; | 35 | QPDF q; |
| 36 | + q.setMaxWarnings(1000); | ||
| 36 | Buffer buf(const_cast<unsigned char*>(data), size); | 37 | Buffer buf(const_cast<unsigned char*>(data), size); |
| 37 | auto is = std::make_shared<BufferInputSource>("json", &buf); | 38 | auto is = std::make_shared<BufferInputSource>("json", &buf); |
| 38 | q.createFromJSON(is); | 39 | q.createFromJSON(is); |
fuzz/qpdf_extra/4826608268017664.fuzz
0 → 100644
No preview for this file type
fuzz/qpdf_extra/70245.fuzz
0 → 100644
fuzz/qpdf_extra/4599089157701632.fuzz renamed to fuzz/qpdf_extra/70306.fuzz
No preview for this file type
fuzz/qtest/fuzz.test
| @@ -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' => 70], # increment when adding new files | 24 | + ['qpdf' => 72], # increment when adding new files |
| 25 | ); | 25 | ); |
| 26 | 26 | ||
| 27 | my $n_tests = 0; | 27 | my $n_tests = 0; |
include/qpdf/QPDF.hh
| @@ -1515,6 +1515,7 @@ class QPDF | @@ -1515,6 +1515,7 @@ class QPDF | ||
| 1515 | std::set<QPDFObjGen> resolving; | 1515 | std::set<QPDFObjGen> resolving; |
| 1516 | QPDFObjectHandle trailer; | 1516 | QPDFObjectHandle trailer; |
| 1517 | std::vector<QPDFObjectHandle> all_pages; | 1517 | std::vector<QPDFObjectHandle> all_pages; |
| 1518 | + bool invalid_page_found{false}; | ||
| 1518 | std::map<QPDFObjGen, int> pageobj_to_pages_pos; | 1519 | std::map<QPDFObjGen, int> pageobj_to_pages_pos; |
| 1519 | bool pushed_inherited_attributes_to_pages{false}; | 1520 | bool pushed_inherited_attributes_to_pages{false}; |
| 1520 | bool ever_pushed_inherited_attributes_to_pages{false}; | 1521 | bool ever_pushed_inherited_attributes_to_pages{false}; |
libqpdf/QPDF_json.cc
| @@ -233,12 +233,13 @@ provide_data(std::shared_ptr<InputSource> is, qpdf_offset_t start, qpdf_offset_t | @@ -233,12 +233,13 @@ provide_data(std::shared_ptr<InputSource> is, qpdf_offset_t start, qpdf_offset_t | ||
| 233 | class QPDF::JSONReactor: public JSON::Reactor | 233 | class QPDF::JSONReactor: public JSON::Reactor |
| 234 | { | 234 | { |
| 235 | public: | 235 | public: |
| 236 | - JSONReactor(QPDF& pdf, std::shared_ptr<InputSource> is, bool must_be_complete) : | 236 | + JSONReactor(QPDF& pdf, std::shared_ptr<InputSource> is, bool must_be_complete, int max_warnings) : |
| 237 | pdf(pdf), | 237 | pdf(pdf), |
| 238 | is(is), | 238 | is(is), |
| 239 | must_be_complete(must_be_complete), | 239 | must_be_complete(must_be_complete), |
| 240 | descr(std::make_shared<QPDFValue::Description>( | 240 | descr(std::make_shared<QPDFValue::Description>( |
| 241 | - QPDFValue::JSON_Descr(std::make_shared<std::string>(is->getName()), ""))) | 241 | + QPDFValue::JSON_Descr(std::make_shared<std::string>(is->getName()), ""))), |
| 242 | + max_warnings(max_warnings) | ||
| 242 | { | 243 | { |
| 243 | for (auto& oc: pdf.m->obj_cache) { | 244 | for (auto& oc: pdf.m->obj_cache) { |
| 244 | if (oc.second.object->getTypeCode() == ::ot_reserved) { | 245 | if (oc.second.object->getTypeCode() == ::ot_reserved) { |
| @@ -291,7 +292,8 @@ class QPDF::JSONReactor: public JSON::Reactor | @@ -291,7 +292,8 @@ class QPDF::JSONReactor: public JSON::Reactor | ||
| 291 | std::shared_ptr<InputSource> is; | 292 | std::shared_ptr<InputSource> is; |
| 292 | bool must_be_complete{true}; | 293 | bool must_be_complete{true}; |
| 293 | std::shared_ptr<QPDFValue::Description> descr; | 294 | std::shared_ptr<QPDFValue::Description> descr; |
| 294 | - bool errors{false}; | 295 | + int errors{0}; |
| 296 | + int max_warnings{0}; | ||
| 295 | bool saw_qpdf{false}; | 297 | bool saw_qpdf{false}; |
| 296 | bool saw_qpdf_meta{false}; | 298 | bool saw_qpdf_meta{false}; |
| 297 | bool saw_objects{false}; | 299 | bool saw_objects{false}; |
| @@ -314,18 +316,21 @@ class QPDF::JSONReactor: public JSON::Reactor | @@ -314,18 +316,21 @@ class QPDF::JSONReactor: public JSON::Reactor | ||
| 314 | void | 316 | void |
| 315 | QPDF::JSONReactor::error(qpdf_offset_t offset, std::string const& msg) | 317 | QPDF::JSONReactor::error(qpdf_offset_t offset, std::string const& msg) |
| 316 | { | 318 | { |
| 317 | - this->errors = true; | 319 | + ++errors; |
| 318 | std::string object = this->cur_object; | 320 | std::string object = this->cur_object; |
| 319 | if (is->getName() != pdf.getFilename()) { | 321 | if (is->getName() != pdf.getFilename()) { |
| 320 | object += " from " + is->getName(); | 322 | object += " from " + is->getName(); |
| 321 | } | 323 | } |
| 322 | this->pdf.warn(qpdf_e_json, object, offset, msg); | 324 | this->pdf.warn(qpdf_e_json, object, offset, msg); |
| 325 | + if (max_warnings > 0 && errors >= max_warnings) { | ||
| 326 | + throw std::runtime_error("errors found in JSON"); | ||
| 327 | + } | ||
| 323 | } | 328 | } |
| 324 | 329 | ||
| 325 | bool | 330 | bool |
| 326 | QPDF::JSONReactor::anyErrors() const | 331 | QPDF::JSONReactor::anyErrors() const |
| 327 | { | 332 | { |
| 328 | - return this->errors; | 333 | + return errors > 0; |
| 329 | } | 334 | } |
| 330 | 335 | ||
| 331 | void | 336 | void |
| @@ -820,7 +825,7 @@ QPDF::updateFromJSON(std::shared_ptr<InputSource> is) | @@ -820,7 +825,7 @@ QPDF::updateFromJSON(std::shared_ptr<InputSource> is) | ||
| 820 | void | 825 | void |
| 821 | QPDF::importJSON(std::shared_ptr<InputSource> is, bool must_be_complete) | 826 | QPDF::importJSON(std::shared_ptr<InputSource> is, bool must_be_complete) |
| 822 | { | 827 | { |
| 823 | - JSONReactor reactor(*this, is, must_be_complete); | 828 | + JSONReactor reactor(*this, is, must_be_complete, m->max_warnings); |
| 824 | try { | 829 | try { |
| 825 | JSON::parse(*is, &reactor); | 830 | JSON::parse(*is, &reactor); |
| 826 | } catch (std::runtime_error& e) { | 831 | } catch (std::runtime_error& e) { |
libqpdf/QPDF_pages.cc
| @@ -40,7 +40,7 @@ std::vector<QPDFObjectHandle> const& | @@ -40,7 +40,7 @@ std::vector<QPDFObjectHandle> const& | ||
| 40 | QPDF::getAllPages() | 40 | QPDF::getAllPages() |
| 41 | { | 41 | { |
| 42 | // Note that pushInheritedAttributesToPage may also be used to initialize m->all_pages. | 42 | // Note that pushInheritedAttributesToPage may also be used to initialize m->all_pages. |
| 43 | - if (m->all_pages.empty()) { | 43 | + if (m->all_pages.empty() && !m->invalid_page_found) { |
| 44 | m->ever_called_get_all_pages = true; | 44 | m->ever_called_get_all_pages = true; |
| 45 | QPDFObjGen::set visited; | 45 | QPDFObjGen::set visited; |
| 46 | QPDFObjGen::set seen; | 46 | QPDFObjGen::set seen; |
| @@ -66,9 +66,15 @@ QPDF::getAllPages() | @@ -66,9 +66,15 @@ QPDF::getAllPages() | ||
| 66 | getRoot().replaceKey("/Pages", pages); | 66 | getRoot().replaceKey("/Pages", pages); |
| 67 | } | 67 | } |
| 68 | seen.clear(); | 68 | seen.clear(); |
| 69 | - if (pages.hasKey("/Kids")) { | 69 | + if (!pages.hasKey("/Kids")) { |
| 70 | // Ensure we actually found a /Pages object. | 70 | // Ensure we actually found a /Pages object. |
| 71 | - getAllPagesInternal(pages, visited, seen, false); | 71 | + throw QPDFExc( |
| 72 | + qpdf_e_pages, m->file->getName(), "", 0, "root of pages tree has no /Kids array"); | ||
| 73 | + } | ||
| 74 | + getAllPagesInternal(pages, visited, seen, false); | ||
| 75 | + if (m->invalid_page_found) { | ||
| 76 | + flattenPagesTree(); | ||
| 77 | + m->invalid_page_found = false; | ||
| 72 | } | 78 | } |
| 73 | } | 79 | } |
| 74 | return m->all_pages; | 80 | return m->all_pages; |
| @@ -100,6 +106,7 @@ QPDF::getAllPagesInternal( | @@ -100,6 +106,7 @@ QPDF::getAllPagesInternal( | ||
| 100 | auto kid = kids.getArrayItem(i); | 106 | auto kid = kids.getArrayItem(i); |
| 101 | if (!kid.isDictionary()) { | 107 | if (!kid.isDictionary()) { |
| 102 | kid.warnIfPossible("Pages tree includes non-dictionary object; ignoring"); | 108 | kid.warnIfPossible("Pages tree includes non-dictionary object; ignoring"); |
| 109 | + m->invalid_page_found = true; | ||
| 103 | continue; | 110 | continue; |
| 104 | } | 111 | } |
| 105 | if (kid.hasKey("/Kids")) { | 112 | if (kid.hasKey("/Kids")) { |
| @@ -181,7 +188,11 @@ QPDF::flattenPagesTree() | @@ -181,7 +188,11 @@ QPDF::flattenPagesTree() | ||
| 181 | pages.replaceKey("/Kids", QPDFObjectHandle::newArray(m->all_pages)); | 188 | pages.replaceKey("/Kids", QPDFObjectHandle::newArray(m->all_pages)); |
| 182 | // /Count has not changed | 189 | // /Count has not changed |
| 183 | if (pages.getKey("/Count").getUIntValue() != len) { | 190 | if (pages.getKey("/Count").getUIntValue() != len) { |
| 184 | - throw std::runtime_error("/Count is wrong after flattening pages tree"); | 191 | + if (m->invalid_page_found && pages.getKey("/Count").getUIntValue() > len) { |
| 192 | + pages.replaceKey("/Count", QPDFObjectHandle::newInteger(toI(len))); | ||
| 193 | + } else { | ||
| 194 | + throw std::runtime_error("/Count is wrong after flattening pages tree"); | ||
| 195 | + } | ||
| 185 | } | 196 | } |
| 186 | } | 197 | } |
| 187 | 198 |