Commit f30635771c16cb1ddb3b5aa21cbb10414c0405db
Committed by
GitHub
Merge pull request #1605 from m-holger/fuzz
Refactor AcroForm field traversal for robustness and add new fuzz case
Showing
5 changed files
with
34 additions
and
11 deletions
fuzz/CMakeLists.txt
| @@ -161,6 +161,7 @@ set(CORPUS_OTHER | @@ -161,6 +161,7 @@ set(CORPUS_OTHER | ||
| 161 | 433311400.fuzz | 161 | 433311400.fuzz |
| 162 | 440599107.fuzz | 162 | 440599107.fuzz |
| 163 | 440747125.fuzz | 163 | 440747125.fuzz |
| 164 | + 464655077.fuzz | ||
| 164 | 4720043549327360.fuzz | 165 | 4720043549327360.fuzz |
| 165 | 4797504999981056.fuzz | 166 | 4797504999981056.fuzz |
| 166 | 4876793183272960.fuzz | 167 | 4876793183272960.fuzz |
fuzz/qpdf_extra/464655077.fuzz
0 → 100644
No preview for this file type
fuzz/qtest/fuzz.test
| @@ -11,7 +11,7 @@ my $td = new TestDriver('fuzz'); | @@ -11,7 +11,7 @@ my $td = new TestDriver('fuzz'); | ||
| 11 | 11 | ||
| 12 | my $qpdf_corpus = $ENV{'QPDF_FUZZ_CORPUS'} || die "must set QPDF_FUZZ_CORPUS"; | 12 | my $qpdf_corpus = $ENV{'QPDF_FUZZ_CORPUS'} || die "must set QPDF_FUZZ_CORPUS"; |
| 13 | 13 | ||
| 14 | -my $n_qpdf_files = 107; # increment when adding new files | 14 | +my $n_qpdf_files = 108; # increment when adding new files |
| 15 | 15 | ||
| 16 | my @fuzzers = ( | 16 | my @fuzzers = ( |
| 17 | ['ascii85' => 1], | 17 | ['ascii85' => 1], |
include/qpdf/QPDFAcroFormDocumentHelper.hh
| @@ -226,7 +226,7 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper | @@ -226,7 +226,7 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper | ||
| 226 | 226 | ||
| 227 | private: | 227 | private: |
| 228 | void analyze(); | 228 | void analyze(); |
| 229 | - void traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth); | 229 | + bool traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth); |
| 230 | QPDFObjectHandle getOrCreateAcroForm(); | 230 | QPDFObjectHandle getOrCreateAcroForm(); |
| 231 | void adjustInheritedFields( | 231 | void adjustInheritedFields( |
| 232 | QPDFObjectHandle obj, | 232 | QPDFObjectHandle obj, |
libqpdf/QPDFAcroFormDocumentHelper.cc
| @@ -31,6 +31,7 @@ class QPDFAcroFormDocumentHelper::Members | @@ -31,6 +31,7 @@ class QPDFAcroFormDocumentHelper::Members | ||
| 31 | std::map<QPDFObjGen, FieldData> field_to; | 31 | std::map<QPDFObjGen, FieldData> field_to; |
| 32 | std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field; | 32 | std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field; |
| 33 | std::map<std::string, std::set<QPDFObjGen>> name_to_fields; | 33 | std::map<std::string, std::set<QPDFObjGen>> name_to_fields; |
| 34 | + std::set<QPDFObjGen> bad_fields; | ||
| 34 | }; | 35 | }; |
| 35 | 36 | ||
| 36 | QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF& qpdf) : | 37 | QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF& qpdf) : |
| @@ -312,35 +313,36 @@ QPDFAcroFormDocumentHelper::analyze() | @@ -312,35 +313,36 @@ QPDFAcroFormDocumentHelper::analyze() | ||
| 312 | } | 313 | } |
| 313 | } | 314 | } |
| 314 | 315 | ||
| 315 | -void | 316 | +bool |
| 316 | QPDFAcroFormDocumentHelper::traverseField( | 317 | QPDFAcroFormDocumentHelper::traverseField( |
| 317 | QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth) | 318 | QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth) |
| 318 | { | 319 | { |
| 319 | if (depth > 100) { | 320 | if (depth > 100) { |
| 320 | // Arbitrarily cut off recursion at a fixed depth to avoid specially crafted files that | 321 | // Arbitrarily cut off recursion at a fixed depth to avoid specially crafted files that |
| 321 | // could cause stack overflow. | 322 | // could cause stack overflow. |
| 322 | - return; | 323 | + return false; |
| 323 | } | 324 | } |
| 324 | if (!field.indirect()) { | 325 | if (!field.indirect()) { |
| 325 | field.warn( | 326 | field.warn( |
| 326 | "encountered a direct object as a field or annotation while traversing /AcroForm; " | 327 | "encountered a direct object as a field or annotation while traversing /AcroForm; " |
| 327 | "ignoring field or annotation"); | 328 | "ignoring field or annotation"); |
| 328 | - return; | 329 | + return false; |
| 329 | } | 330 | } |
| 330 | if (field == parent) { | 331 | if (field == parent) { |
| 331 | field.warn("loop detected while traversing /AcroForm"); | 332 | field.warn("loop detected while traversing /AcroForm"); |
| 332 | - return; | 333 | + return false; |
| 333 | } | 334 | } |
| 334 | if (!field.isDictionary()) { | 335 | if (!field.isDictionary()) { |
| 335 | field.warn( | 336 | field.warn( |
| 336 | "encountered a non-dictionary as a field or annotation while traversing /AcroForm; " | 337 | "encountered a non-dictionary as a field or annotation while traversing /AcroForm; " |
| 337 | "ignoring field or annotation"); | 338 | "ignoring field or annotation"); |
| 338 | - return; | 339 | + return false; |
| 339 | } | 340 | } |
| 340 | QPDFObjGen og(field.getObjGen()); | 341 | QPDFObjGen og(field.getObjGen()); |
| 341 | - if (m->field_to.contains(og) || m->annotation_to_field.contains(og)) { | 342 | + if (m->field_to.contains(og) || m->annotation_to_field.contains(og) || |
| 343 | + m->bad_fields.contains(og)) { | ||
| 342 | field.warn("loop detected while traversing /AcroForm"); | 344 | field.warn("loop detected while traversing /AcroForm"); |
| 343 | - return; | 345 | + return false; |
| 344 | } | 346 | } |
| 345 | 347 | ||
| 346 | // A dictionary encountered while traversing the /AcroForm field may be a form field, an | 348 | // A dictionary encountered while traversing the /AcroForm field may be a form field, an |
| @@ -357,6 +359,13 @@ QPDFAcroFormDocumentHelper::traverseField( | @@ -357,6 +359,13 @@ QPDFAcroFormDocumentHelper::traverseField( | ||
| 357 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper field found", (depth == 0) ? 0 : 1); | 359 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper field found", (depth == 0) ? 0 : 1); |
| 358 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper annotation found", (is_field ? 0 : 1)); | 360 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper annotation found", (is_field ? 0 : 1)); |
| 359 | 361 | ||
| 362 | + if (!is_field && !is_annotation) { | ||
| 363 | + field.warn( | ||
| 364 | + "encountered an object that is neither field nor annotation while traversing " | ||
| 365 | + "/AcroForm"); | ||
| 366 | + return false; | ||
| 367 | + } | ||
| 368 | + | ||
| 360 | if (is_annotation) { | 369 | if (is_annotation) { |
| 361 | QPDFObjectHandle our_field = (is_field ? field : parent); | 370 | QPDFObjectHandle our_field = (is_field ? field : parent); |
| 362 | m->field_to[our_field.getObjGen()].annotations.emplace_back(field); | 371 | m->field_to[our_field.getObjGen()].annotations.emplace_back(field); |
| @@ -365,9 +374,15 @@ QPDFAcroFormDocumentHelper::traverseField( | @@ -365,9 +374,15 @@ QPDFAcroFormDocumentHelper::traverseField( | ||
| 365 | 374 | ||
| 366 | if (is_field && depth != 0 && field["/Parent"] != parent) { | 375 | if (is_field && depth != 0 && field["/Parent"] != parent) { |
| 367 | for (auto const& kid: Array(field["/Parent"]["/Kids"])) { | 376 | for (auto const& kid: Array(field["/Parent"]["/Kids"])) { |
| 377 | + if (kid == field) { | ||
| 378 | + field.warn("while traversing /AcroForm found field with two parents"); | ||
| 379 | + return true; | ||
| 380 | + } | ||
| 381 | + } | ||
| 382 | + for (auto const& kid: Array(field["/Parent"]["/Kids"])) { | ||
| 368 | if (kid == parent) { | 383 | if (kid == parent) { |
| 369 | field.warn("loop detected while traversing /AcroForm"); | 384 | field.warn("loop detected while traversing /AcroForm"); |
| 370 | - return; | 385 | + return false; |
| 371 | } | 386 | } |
| 372 | } | 387 | } |
| 373 | field.warn("encountered invalid /Parent entry while traversing /AcroForm; correcting"); | 388 | field.warn("encountered invalid /Parent entry while traversing /AcroForm; correcting"); |
| @@ -387,8 +402,15 @@ QPDFAcroFormDocumentHelper::traverseField( | @@ -387,8 +402,15 @@ QPDFAcroFormDocumentHelper::traverseField( | ||
| 387 | } | 402 | } |
| 388 | 403 | ||
| 389 | for (auto const& kid: Kids) { | 404 | for (auto const& kid: Kids) { |
| 390 | - traverseField(kid, field, 1 + depth); | 405 | + if (m->bad_fields.contains(kid)) { |
| 406 | + continue; | ||
| 407 | + } | ||
| 408 | + | ||
| 409 | + if (!traverseField(kid, field, 1 + depth)) { | ||
| 410 | + m->bad_fields.insert(kid); | ||
| 411 | + } | ||
| 391 | } | 412 | } |
| 413 | + return true; | ||
| 392 | } | 414 | } |
| 393 | 415 | ||
| 394 | bool | 416 | bool |