Commit f30635771c16cb1ddb3b5aa21cbb10414c0405db

Authored by m-holger
Committed by GitHub
2 parents c1b45018 1e2c50c0

Merge pull request #1605 from m-holger/fuzz

Refactor AcroForm field traversal for robustness and add new fuzz case
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