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
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 | 11 | |
| 12 | 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 | 16 | my @fuzzers = ( |
| 17 | 17 | ['ascii85' => 1], | ... | ... |
include/qpdf/QPDFAcroFormDocumentHelper.hh
| ... | ... | @@ -226,7 +226,7 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper |
| 226 | 226 | |
| 227 | 227 | private: |
| 228 | 228 | void analyze(); |
| 229 | - void traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth); | |
| 229 | + bool traverseField(QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth); | |
| 230 | 230 | QPDFObjectHandle getOrCreateAcroForm(); |
| 231 | 231 | void adjustInheritedFields( |
| 232 | 232 | QPDFObjectHandle obj, | ... | ... |
libqpdf/QPDFAcroFormDocumentHelper.cc
| ... | ... | @@ -31,6 +31,7 @@ class QPDFAcroFormDocumentHelper::Members |
| 31 | 31 | std::map<QPDFObjGen, FieldData> field_to; |
| 32 | 32 | std::map<QPDFObjGen, QPDFFormFieldObjectHelper> annotation_to_field; |
| 33 | 33 | std::map<std::string, std::set<QPDFObjGen>> name_to_fields; |
| 34 | + std::set<QPDFObjGen> bad_fields; | |
| 34 | 35 | }; |
| 35 | 36 | |
| 36 | 37 | QPDFAcroFormDocumentHelper::QPDFAcroFormDocumentHelper(QPDF& qpdf) : |
| ... | ... | @@ -312,35 +313,36 @@ QPDFAcroFormDocumentHelper::analyze() |
| 312 | 313 | } |
| 313 | 314 | } |
| 314 | 315 | |
| 315 | -void | |
| 316 | +bool | |
| 316 | 317 | QPDFAcroFormDocumentHelper::traverseField( |
| 317 | 318 | QPDFObjectHandle field, QPDFObjectHandle const& parent, int depth) |
| 318 | 319 | { |
| 319 | 320 | if (depth > 100) { |
| 320 | 321 | // Arbitrarily cut off recursion at a fixed depth to avoid specially crafted files that |
| 321 | 322 | // could cause stack overflow. |
| 322 | - return; | |
| 323 | + return false; | |
| 323 | 324 | } |
| 324 | 325 | if (!field.indirect()) { |
| 325 | 326 | field.warn( |
| 326 | 327 | "encountered a direct object as a field or annotation while traversing /AcroForm; " |
| 327 | 328 | "ignoring field or annotation"); |
| 328 | - return; | |
| 329 | + return false; | |
| 329 | 330 | } |
| 330 | 331 | if (field == parent) { |
| 331 | 332 | field.warn("loop detected while traversing /AcroForm"); |
| 332 | - return; | |
| 333 | + return false; | |
| 333 | 334 | } |
| 334 | 335 | if (!field.isDictionary()) { |
| 335 | 336 | field.warn( |
| 336 | 337 | "encountered a non-dictionary as a field or annotation while traversing /AcroForm; " |
| 337 | 338 | "ignoring field or annotation"); |
| 338 | - return; | |
| 339 | + return false; | |
| 339 | 340 | } |
| 340 | 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 | 344 | field.warn("loop detected while traversing /AcroForm"); |
| 343 | - return; | |
| 345 | + return false; | |
| 344 | 346 | } |
| 345 | 347 | |
| 346 | 348 | // A dictionary encountered while traversing the /AcroForm field may be a form field, an |
| ... | ... | @@ -357,6 +359,13 @@ QPDFAcroFormDocumentHelper::traverseField( |
| 357 | 359 | QTC::TC("qpdf", "QPDFAcroFormDocumentHelper field found", (depth == 0) ? 0 : 1); |
| 358 | 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 | 369 | if (is_annotation) { |
| 361 | 370 | QPDFObjectHandle our_field = (is_field ? field : parent); |
| 362 | 371 | m->field_to[our_field.getObjGen()].annotations.emplace_back(field); |
| ... | ... | @@ -365,9 +374,15 @@ QPDFAcroFormDocumentHelper::traverseField( |
| 365 | 374 | |
| 366 | 375 | if (is_field && depth != 0 && field["/Parent"] != parent) { |
| 367 | 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 | 383 | if (kid == parent) { |
| 369 | 384 | field.warn("loop detected while traversing /AcroForm"); |
| 370 | - return; | |
| 385 | + return false; | |
| 371 | 386 | } |
| 372 | 387 | } |
| 373 | 388 | field.warn("encountered invalid /Parent entry while traversing /AcroForm; correcting"); |
| ... | ... | @@ -387,8 +402,15 @@ QPDFAcroFormDocumentHelper::traverseField( |
| 387 | 402 | } |
| 388 | 403 | |
| 389 | 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 | 416 | bool | ... | ... |