From d8d73679e71942c4d53e6f8826aa5b51eaa57137 Mon Sep 17 00:00:00 2001 From: m-holger Date: Tue, 3 Sep 2024 00:57:34 +0100 Subject: [PATCH] Split qpdf_fuzzer into six separate fuzzers --- fuzz/CMakeLists.txt | 7 +- fuzz/qpdf_crypt_fuzzer.cc | 153 ++++++++++++++++++++++++ fuzz/qpdf_crypt_fuzzer.options | 2 + fuzz/qpdf_crypt_insecure_fuzzer.cc | 153 ++++++++++++++++++++++++ fuzz/qpdf_crypt_insecure_fuzzer.options | 2 + fuzz/qpdf_fuzzer.cc | 75 ------------ fuzz/qpdf_lin_fuzzer.cc | 152 +++++++++++++++++++++++ fuzz/qpdf_lin_fuzzer.options | 2 + fuzz/qpdf_outlines_fuzzer.cc | 129 ++++++++++++++++++++ fuzz/qpdf_outlines_fuzzer.options | 2 + fuzz/qpdf_pages_fuzzer.cc | 150 +++++++++++++++++++++++ fuzz/qpdf_pages_fuzzer.options | 2 + fuzz/qtest/fuzz.test | 9 +- 13 files changed, 761 insertions(+), 77 deletions(-) create mode 100644 fuzz/qpdf_crypt_fuzzer.cc create mode 100644 fuzz/qpdf_crypt_fuzzer.options create mode 100644 fuzz/qpdf_crypt_insecure_fuzzer.cc create mode 100644 fuzz/qpdf_crypt_insecure_fuzzer.options create mode 100644 fuzz/qpdf_lin_fuzzer.cc create mode 100644 fuzz/qpdf_lin_fuzzer.options create mode 100644 fuzz/qpdf_outlines_fuzzer.cc create mode 100644 fuzz/qpdf_outlines_fuzzer.options create mode 100644 fuzz/qpdf_pages_fuzzer.cc create mode 100644 fuzz/qpdf_pages_fuzzer.options diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt index cfda4ff6..020dd053 100644 --- a/fuzz/CMakeLists.txt +++ b/fuzz/CMakeLists.txt @@ -3,6 +3,11 @@ set(FUZZERS qpdf_fuzzer + qpdf_crypt_fuzzer + qpdf_crypt_insecure_fuzzer + qpdf_lin_fuzzer + qpdf_pages_fuzzer + qpdf_outlines_fuzzer ascii85_fuzzer dct_fuzzer flate_fuzzer @@ -174,7 +179,7 @@ add_test( if(OSS_FUZZ) list(APPEND SEED_CORPUS_ZIPS) foreach(F ${FUZZERS}) - if(F STREQUAL qpdf_fuzzer) + if((F STRGREATER qpdf_) AND (F STRLESS qpdg)) set(SEED_DIR ${CORPUS_DIR}) else() set(SEED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${F}_seed_corpus) diff --git a/fuzz/qpdf_crypt_fuzzer.cc b/fuzz/qpdf_crypt_fuzzer.cc new file mode 100644 index 00000000..0e391f8a --- /dev/null +++ b/fuzz/qpdf_crypt_fuzzer.cc @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class DiscardContents: public QPDFObjectHandle::ParserCallbacks +{ + public: + ~DiscardContents() override = default; + void + handleObject(QPDFObjectHandle) override + { + } + void + handleEOF() override + { + } +}; + +class FuzzHelper +{ + public: + FuzzHelper(unsigned char const* data, size_t size); + void run(); + + private: + std::shared_ptr getQpdf(); + std::shared_ptr getWriter(std::shared_ptr); + void doWrite(std::shared_ptr w); + void testWrite(); + void doChecks(); + + Buffer input_buffer; + Pl_Discard discard; +}; + +FuzzHelper::FuzzHelper(unsigned char const* data, size_t size) : + // We do not modify data, so it is safe to remove the const for Buffer + input_buffer(const_cast(data), size) +{ +} + +std::shared_ptr +FuzzHelper::getQpdf() +{ + auto is = + std::shared_ptr(new BufferInputSource("fuzz input", &this->input_buffer)); + auto qpdf = QPDF::create(); + qpdf->setMaxWarnings(200); + qpdf->processInputSource(is); + return qpdf; +} + +std::shared_ptr +FuzzHelper::getWriter(std::shared_ptr qpdf) +{ + auto w = std::make_shared(*qpdf); + w->setOutputPipeline(&this->discard); + w->setDecodeLevel(qpdf_dl_all); + return w; +} + +void +FuzzHelper::doWrite(std::shared_ptr w) +{ + try { + w->write(); + } catch (QPDFExc const& e) { + std::cerr << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << e.what() << std::endl; + } +} + +void +FuzzHelper::testWrite() +{ + // Write in various ways to exercise QPDFWriter + + std::shared_ptr q; + std::shared_ptr w; + + q = getQpdf(); + w = getWriter(q); + w->setStaticID(true); + w->setLinearization(true); + w->setR6EncryptionParameters("u", "o", true, true, true, true, true, true, qpdf_r3p_full, true); + doWrite(w); +} + +void +FuzzHelper::doChecks() +{ + // Limit the memory used to decompress JPEG files during fuzzing. Excessive memory use during + // fuzzing is due to corrupt JPEG data which sometimes cannot be detected before + // jpeg_start_decompress is called. During normal use of qpdf very large JPEGs can occasionally + // occur legitimately and therefore must be allowed during normal operations. + Pl_DCT::setMemoryLimit(100'000'000); + Pl_DCT::setScanLimit(50); + + Pl_PNGFilter::setMemoryLimit(1'000'000); + Pl_TIFFPredictor::setMemoryLimit(1'000'000); + Pl_Flate::setMemoryLimit(1'000'000); + + // Do not decompress corrupt data. This may cause extended runtime within jpeglib without + // exercising additional code paths in qpdf, and potentially causing counterproductive timeouts. + Pl_DCT::setThrowOnCorruptData(true); + + // Get as much coverage as possible in parts of the library that + // might benefit from fuzzing. + std::cerr << "\ninfo: starting testWrite\n"; + testWrite(); +} + +void +FuzzHelper::run() +{ + // The goal here is that you should be able to throw anything at + // libqpdf and it will respond without any memory errors and never + // do anything worse than throwing a QPDFExc or + // std::runtime_error. Throwing any other kind of exception, + // segfaulting, or having a memory error (when built with + // appropriate sanitizers) will all cause abnormal exit. + try { + doChecks(); + } catch (QPDFExc const& e) { + std::cerr << "QPDFExc: " << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << "runtime_error: " << e.what() << std::endl; + } +} + +extern "C" int +LLVMFuzzerTestOneInput(unsigned char const* data, size_t size) +{ +#ifndef _WIN32 + // Used by jpeg library to work around false positives in memory + // sanitizer. + setenv("JSIMD_FORCENONE", "1", 1); +#endif + FuzzHelper f(data, size); + f.run(); + return 0; +} diff --git a/fuzz/qpdf_crypt_fuzzer.options b/fuzz/qpdf_crypt_fuzzer.options new file mode 100644 index 00000000..a15a7745 --- /dev/null +++ b/fuzz/qpdf_crypt_fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +dict = pdf.dict diff --git a/fuzz/qpdf_crypt_insecure_fuzzer.cc b/fuzz/qpdf_crypt_insecure_fuzzer.cc new file mode 100644 index 00000000..22a7f0e4 --- /dev/null +++ b/fuzz/qpdf_crypt_insecure_fuzzer.cc @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DiscardContents: public QPDFObjectHandle::ParserCallbacks +{ + public: + ~DiscardContents() override = default; + void + handleObject(QPDFObjectHandle) override + { + } + void + handleEOF() override + { + } +}; + +class FuzzHelper +{ + public: + FuzzHelper(unsigned char const* data, size_t size); + void run(); + + private: + std::shared_ptr getQpdf(); + std::shared_ptr getWriter(std::shared_ptr); + void doWrite(std::shared_ptr w); + void testWrite(); + void doChecks(); + + Buffer input_buffer; + Pl_Discard discard; +}; + +FuzzHelper::FuzzHelper(unsigned char const* data, size_t size) : + // We do not modify data, so it is safe to remove the const for Buffer + input_buffer(const_cast(data), size) +{ +} + +std::shared_ptr +FuzzHelper::getQpdf() +{ + auto is = + std::shared_ptr(new BufferInputSource("fuzz input", &this->input_buffer)); + auto qpdf = QPDF::create(); + qpdf->setMaxWarnings(200); + qpdf->processInputSource(is); + return qpdf; +} + +std::shared_ptr +FuzzHelper::getWriter(std::shared_ptr qpdf) +{ + auto w = std::make_shared(*qpdf); + w->setOutputPipeline(&this->discard); + w->setDecodeLevel(qpdf_dl_all); + return w; +} + +void +FuzzHelper::doWrite(std::shared_ptr w) +{ + try { + w->write(); + } catch (QPDFExc const& e) { + std::cerr << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << e.what() << std::endl; + } +} + +void +FuzzHelper::testWrite() +{ + // Write in various ways to exercise QPDFWriter + + std::shared_ptr q; + std::shared_ptr w; + + q = getQpdf(); + w = getWriter(q); + w->setStaticID(true); + w->setObjectStreamMode(qpdf_o_disable); + w->setR3EncryptionParametersInsecure( + "u", "o", true, true, true, true, true, true, qpdf_r3p_full); + doWrite(w); +} + +void +FuzzHelper::doChecks() +{ + // Limit the memory used to decompress JPEG files during fuzzing. Excessive memory use during + // fuzzing is due to corrupt JPEG data which sometimes cannot be detected before + // jpeg_start_decompress is called. During normal use of qpdf very large JPEGs can occasionally + // occur legitimately and therefore must be allowed during normal operations. + Pl_DCT::setMemoryLimit(100'000'000); + Pl_DCT::setScanLimit(50); + + Pl_PNGFilter::setMemoryLimit(1'000'000); + Pl_TIFFPredictor::setMemoryLimit(1'000'000); + Pl_Flate::setMemoryLimit(1'000'000); + + // Do not decompress corrupt data. This may cause extended runtime within jpeglib without + // exercising additional code paths in qpdf, and potentially causing counterproductive timeouts. + Pl_DCT::setThrowOnCorruptData(true); + + // Get as much coverage as possible in parts of the library that + // might benefit from fuzzing. + std::cerr << "\ninfo: starting testWrite\n"; + testWrite(); +} + +void +FuzzHelper::run() +{ + // The goal here is that you should be able to throw anything at + // libqpdf and it will respond without any memory errors and never + // do anything worse than throwing a QPDFExc or + // std::runtime_error. Throwing any other kind of exception, + // segfaulting, or having a memory error (when built with + // appropriate sanitizers) will all cause abnormal exit. + try { + doChecks(); + } catch (QPDFExc const& e) { + std::cerr << "QPDFExc: " << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << "runtime_error: " << e.what() << std::endl; + } +} + +extern "C" int +LLVMFuzzerTestOneInput(unsigned char const* data, size_t size) +{ +#ifndef _WIN32 + // Used by jpeg library to work around false positives in memory + // sanitizer. + setenv("JSIMD_FORCENONE", "1", 1); +#endif + FuzzHelper f(data, size); + f.run(); + return 0; +} diff --git a/fuzz/qpdf_crypt_insecure_fuzzer.options b/fuzz/qpdf_crypt_insecure_fuzzer.options new file mode 100644 index 00000000..a15a7745 --- /dev/null +++ b/fuzz/qpdf_crypt_insecure_fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +dict = pdf.dict diff --git a/fuzz/qpdf_fuzzer.cc b/fuzz/qpdf_fuzzer.cc index 45d924d3..490caea4 100644 --- a/fuzz/qpdf_fuzzer.cc +++ b/fuzz/qpdf_fuzzer.cc @@ -6,10 +6,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -40,8 +36,6 @@ class FuzzHelper std::shared_ptr getWriter(std::shared_ptr); void doWrite(std::shared_ptr w); void testWrite(); - void testPages(); - void testOutlines(); void doChecks(); Buffer input_buffer; @@ -106,71 +100,6 @@ FuzzHelper::testWrite() w->setLinearization(true); w->setR6EncryptionParameters("u", "o", true, true, true, true, true, true, qpdf_r3p_full, true); doWrite(w); - - q = getQpdf(); - w = getWriter(q); - w->setStaticID(true); - w->setObjectStreamMode(qpdf_o_disable); - w->setR3EncryptionParametersInsecure( - "u", "o", true, true, true, true, true, true, qpdf_r3p_full); - doWrite(w); - - q = getQpdf(); - w = getWriter(q); - w->setDeterministicID(true); - w->setObjectStreamMode(qpdf_o_generate); - w->setLinearization(true); - doWrite(w); -} - -void -FuzzHelper::testPages() -{ - // Parse all content streams, and exercise some helpers that - // operate on pages. - std::shared_ptr q = getQpdf(); - QPDFPageDocumentHelper pdh(*q); - QPDFPageLabelDocumentHelper pldh(*q); - QPDFOutlineDocumentHelper odh(*q); - QPDFAcroFormDocumentHelper afdh(*q); - afdh.generateAppearancesIfNeeded(); - pdh.flattenAnnotations(); - DiscardContents discard_contents; - int pageno = 0; - for (auto& page: pdh.getAllPages()) { - ++pageno; - try { - page.coalesceContentStreams(); - page.parseContents(&discard_contents); - page.getImages(); - pldh.getLabelForPage(pageno); - QPDFObjectHandle page_obj(page.getObjectHandle()); - page_obj.getJSON(JSON::LATEST, true).unparse(); - odh.getOutlinesForPage(page_obj.getObjGen()); - - for (auto& aoh: afdh.getWidgetAnnotationsForPage(page)) { - afdh.getFieldForAnnotation(aoh); - } - } catch (QPDFExc& e) { - std::cerr << "page " << pageno << ": " << e.what() << std::endl; - } - } -} - -void -FuzzHelper::testOutlines() -{ - std::shared_ptr q = getQpdf(); - std::list> queue; - QPDFOutlineDocumentHelper odh(*q); - queue.push_back(odh.getTopLevelOutlines()); - while (!queue.empty()) { - for (auto& ol: *(queue.begin())) { - ol.getDestPage(); - queue.push_back(ol.getKids()); - } - queue.pop_front(); - } } void @@ -195,10 +124,6 @@ FuzzHelper::doChecks() // might benefit from fuzzing. std::cerr << "\ninfo: starting testWrite\n"; testWrite(); - std::cerr << "\ninfo: starting testPages\n"; - testPages(); - std::cerr << "\ninfo: starting testOutlines\n"; - testOutlines(); } void diff --git a/fuzz/qpdf_lin_fuzzer.cc b/fuzz/qpdf_lin_fuzzer.cc new file mode 100644 index 00000000..eede5b74 --- /dev/null +++ b/fuzz/qpdf_lin_fuzzer.cc @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DiscardContents: public QPDFObjectHandle::ParserCallbacks +{ + public: + ~DiscardContents() override = default; + void + handleObject(QPDFObjectHandle) override + { + } + void + handleEOF() override + { + } +}; + +class FuzzHelper +{ + public: + FuzzHelper(unsigned char const* data, size_t size); + void run(); + + private: + std::shared_ptr getQpdf(); + std::shared_ptr getWriter(std::shared_ptr); + void doWrite(std::shared_ptr w); + void testWrite(); + void doChecks(); + + Buffer input_buffer; + Pl_Discard discard; +}; + +FuzzHelper::FuzzHelper(unsigned char const* data, size_t size) : + // We do not modify data, so it is safe to remove the const for Buffer + input_buffer(const_cast(data), size) +{ +} + +std::shared_ptr +FuzzHelper::getQpdf() +{ + auto is = + std::shared_ptr(new BufferInputSource("fuzz input", &this->input_buffer)); + auto qpdf = QPDF::create(); + qpdf->setMaxWarnings(200); + qpdf->processInputSource(is); + return qpdf; +} + +std::shared_ptr +FuzzHelper::getWriter(std::shared_ptr qpdf) +{ + auto w = std::make_shared(*qpdf); + w->setOutputPipeline(&this->discard); + w->setDecodeLevel(qpdf_dl_all); + return w; +} + +void +FuzzHelper::doWrite(std::shared_ptr w) +{ + try { + w->write(); + } catch (QPDFExc const& e) { + std::cerr << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << e.what() << std::endl; + } +} + +void +FuzzHelper::testWrite() +{ + // Write in various ways to exercise QPDFWriter + + std::shared_ptr q; + std::shared_ptr w; + + q = getQpdf(); + w = getWriter(q); + w->setDeterministicID(true); + w->setObjectStreamMode(qpdf_o_generate); + w->setLinearization(true); + doWrite(w); +} + +void +FuzzHelper::doChecks() +{ + // Limit the memory used to decompress JPEG files during fuzzing. Excessive memory use during + // fuzzing is due to corrupt JPEG data which sometimes cannot be detected before + // jpeg_start_decompress is called. During normal use of qpdf very large JPEGs can occasionally + // occur legitimately and therefore must be allowed during normal operations. + Pl_DCT::setMemoryLimit(100'000'000); + Pl_DCT::setScanLimit(50); + + Pl_PNGFilter::setMemoryLimit(1'000'000); + Pl_TIFFPredictor::setMemoryLimit(1'000'000); + Pl_Flate::setMemoryLimit(1'000'000); + + // Do not decompress corrupt data. This may cause extended runtime within jpeglib without + // exercising additional code paths in qpdf, and potentially causing counterproductive timeouts. + Pl_DCT::setThrowOnCorruptData(true); + + // Get as much coverage as possible in parts of the library that + // might benefit from fuzzing. + std::cerr << "\ninfo: starting testWrite\n"; + testWrite(); +} + +void +FuzzHelper::run() +{ + // The goal here is that you should be able to throw anything at + // libqpdf and it will respond without any memory errors and never + // do anything worse than throwing a QPDFExc or + // std::runtime_error. Throwing any other kind of exception, + // segfaulting, or having a memory error (when built with + // appropriate sanitizers) will all cause abnormal exit. + try { + doChecks(); + } catch (QPDFExc const& e) { + std::cerr << "QPDFExc: " << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << "runtime_error: " << e.what() << std::endl; + } +} + +extern "C" int +LLVMFuzzerTestOneInput(unsigned char const* data, size_t size) +{ +#ifndef _WIN32 + // Used by jpeg library to work around false positives in memory + // sanitizer. + setenv("JSIMD_FORCENONE", "1", 1); +#endif + FuzzHelper f(data, size); + f.run(); + return 0; +} diff --git a/fuzz/qpdf_lin_fuzzer.options b/fuzz/qpdf_lin_fuzzer.options new file mode 100644 index 00000000..a15a7745 --- /dev/null +++ b/fuzz/qpdf_lin_fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +dict = pdf.dict diff --git a/fuzz/qpdf_outlines_fuzzer.cc b/fuzz/qpdf_outlines_fuzzer.cc new file mode 100644 index 00000000..db2a87c2 --- /dev/null +++ b/fuzz/qpdf_outlines_fuzzer.cc @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DiscardContents: public QPDFObjectHandle::ParserCallbacks +{ + public: + ~DiscardContents() override = default; + void + handleObject(QPDFObjectHandle) override + { + } + void + handleEOF() override + { + } +}; + +class FuzzHelper +{ + public: + FuzzHelper(unsigned char const* data, size_t size); + void run(); + + private: + std::shared_ptr getQpdf(); + void testOutlines(); + void doChecks(); + + Buffer input_buffer; + Pl_Discard discard; +}; + +FuzzHelper::FuzzHelper(unsigned char const* data, size_t size) : + // We do not modify data, so it is safe to remove the const for Buffer + input_buffer(const_cast(data), size) +{ +} + +std::shared_ptr +FuzzHelper::getQpdf() +{ + auto is = + std::shared_ptr(new BufferInputSource("fuzz input", &this->input_buffer)); + auto qpdf = QPDF::create(); + qpdf->setMaxWarnings(200); + qpdf->processInputSource(is); + return qpdf; +} + +void +FuzzHelper::testOutlines() +{ + std::shared_ptr q = getQpdf(); + std::list> queue; + QPDFOutlineDocumentHelper odh(*q); + queue.push_back(odh.getTopLevelOutlines()); + while (!queue.empty()) { + for (auto& ol: *(queue.begin())) { + ol.getDestPage(); + queue.push_back(ol.getKids()); + } + queue.pop_front(); + } +} + +void +FuzzHelper::doChecks() +{ + // Limit the memory used to decompress JPEG files during fuzzing. Excessive memory use during + // fuzzing is due to corrupt JPEG data which sometimes cannot be detected before + // jpeg_start_decompress is called. During normal use of qpdf very large JPEGs can occasionally + // occur legitimately and therefore must be allowed during normal operations. + Pl_DCT::setMemoryLimit(100'000'000); + Pl_DCT::setScanLimit(50); + + Pl_PNGFilter::setMemoryLimit(1'000'000); + Pl_TIFFPredictor::setMemoryLimit(1'000'000); + Pl_Flate::setMemoryLimit(1'000'000); + + // Do not decompress corrupt data. This may cause extended runtime within jpeglib without + // exercising additional code paths in qpdf, and potentially causing counterproductive timeouts. + Pl_DCT::setThrowOnCorruptData(true); + + // Get as much coverage as possible in parts of the library that + // might benefit from fuzzing. + std::cerr << "\ninfo: starting testOutlines\n"; + testOutlines(); +} + +void +FuzzHelper::run() +{ + // The goal here is that you should be able to throw anything at + // libqpdf and it will respond without any memory errors and never + // do anything worse than throwing a QPDFExc or + // std::runtime_error. Throwing any other kind of exception, + // segfaulting, or having a memory error (when built with + // appropriate sanitizers) will all cause abnormal exit. + try { + doChecks(); + } catch (QPDFExc const& e) { + std::cerr << "QPDFExc: " << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << "runtime_error: " << e.what() << std::endl; + } +} + +extern "C" int +LLVMFuzzerTestOneInput(unsigned char const* data, size_t size) +{ +#ifndef _WIN32 + // Used by jpeg library to work around false positives in memory + // sanitizer. + setenv("JSIMD_FORCENONE", "1", 1); +#endif + FuzzHelper f(data, size); + f.run(); + return 0; +} diff --git a/fuzz/qpdf_outlines_fuzzer.options b/fuzz/qpdf_outlines_fuzzer.options new file mode 100644 index 00000000..a15a7745 --- /dev/null +++ b/fuzz/qpdf_outlines_fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +dict = pdf.dict diff --git a/fuzz/qpdf_pages_fuzzer.cc b/fuzz/qpdf_pages_fuzzer.cc new file mode 100644 index 00000000..4e799f85 --- /dev/null +++ b/fuzz/qpdf_pages_fuzzer.cc @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DiscardContents: public QPDFObjectHandle::ParserCallbacks +{ + public: + ~DiscardContents() override = default; + void + handleObject(QPDFObjectHandle) override + { + } + void + handleEOF() override + { + } +}; + +class FuzzHelper +{ + public: + FuzzHelper(unsigned char const* data, size_t size); + void run(); + + private: + std::shared_ptr getQpdf(); + void testPages(); + void doChecks(); + + Buffer input_buffer; + Pl_Discard discard; +}; + +FuzzHelper::FuzzHelper(unsigned char const* data, size_t size) : + // We do not modify data, so it is safe to remove the const for Buffer + input_buffer(const_cast(data), size) +{ +} + +std::shared_ptr +FuzzHelper::getQpdf() +{ + auto is = + std::shared_ptr(new BufferInputSource("fuzz input", &this->input_buffer)); + auto qpdf = QPDF::create(); + qpdf->setMaxWarnings(200); + qpdf->processInputSource(is); + return qpdf; +} + +void +FuzzHelper::testPages() +{ + // Parse all content streams, and exercise some helpers that + // operate on pages. + std::shared_ptr q = getQpdf(); + QPDFPageDocumentHelper pdh(*q); + QPDFPageLabelDocumentHelper pldh(*q); + QPDFOutlineDocumentHelper odh(*q); + QPDFAcroFormDocumentHelper afdh(*q); + afdh.generateAppearancesIfNeeded(); + pdh.flattenAnnotations(); + DiscardContents discard_contents; + int pageno = 0; + for (auto& page: pdh.getAllPages()) { + ++pageno; + try { + page.coalesceContentStreams(); + page.parseContents(&discard_contents); + page.getImages(); + pldh.getLabelForPage(pageno); + QPDFObjectHandle page_obj(page.getObjectHandle()); + page_obj.getJSON(JSON::LATEST, true).unparse(); + odh.getOutlinesForPage(page_obj.getObjGen()); + + for (auto& aoh: afdh.getWidgetAnnotationsForPage(page)) { + afdh.getFieldForAnnotation(aoh); + } + } catch (QPDFExc& e) { + std::cerr << "page " << pageno << ": " << e.what() << std::endl; + } + } +} + +void +FuzzHelper::doChecks() +{ + // Limit the memory used to decompress JPEG files during fuzzing. Excessive memory use during + // fuzzing is due to corrupt JPEG data which sometimes cannot be detected before + // jpeg_start_decompress is called. During normal use of qpdf very large JPEGs can occasionally + // occur legitimately and therefore must be allowed during normal operations. + Pl_DCT::setMemoryLimit(100'000'000); + Pl_DCT::setScanLimit(50); + + Pl_PNGFilter::setMemoryLimit(1'000'000); + Pl_TIFFPredictor::setMemoryLimit(1'000'000); + Pl_Flate::setMemoryLimit(1'000'000); + + // Do not decompress corrupt data. This may cause extended runtime within jpeglib without + // exercising additional code paths in qpdf, and potentially causing counterproductive timeouts. + Pl_DCT::setThrowOnCorruptData(true); + + // Get as much coverage as possible in parts of the library that + // might benefit from fuzzing. + std::cerr << "\ninfo: starting testPages\n"; + testPages(); +} + +void +FuzzHelper::run() +{ + // The goal here is that you should be able to throw anything at + // libqpdf and it will respond without any memory errors and never + // do anything worse than throwing a QPDFExc or + // std::runtime_error. Throwing any other kind of exception, + // segfaulting, or having a memory error (when built with + // appropriate sanitizers) will all cause abnormal exit. + try { + doChecks(); + } catch (QPDFExc const& e) { + std::cerr << "QPDFExc: " << e.what() << std::endl; + } catch (std::runtime_error const& e) { + std::cerr << "runtime_error: " << e.what() << std::endl; + } +} + +extern "C" int +LLVMFuzzerTestOneInput(unsigned char const* data, size_t size) +{ +#ifndef _WIN32 + // Used by jpeg library to work around false positives in memory + // sanitizer. + setenv("JSIMD_FORCENONE", "1", 1); +#endif + FuzzHelper f(data, size); + f.run(); + return 0; +} diff --git a/fuzz/qpdf_pages_fuzzer.options b/fuzz/qpdf_pages_fuzzer.options new file mode 100644 index 00000000..a15a7745 --- /dev/null +++ b/fuzz/qpdf_pages_fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +dict = pdf.dict diff --git a/fuzz/qtest/fuzz.test b/fuzz/qtest/fuzz.test index f1cb79be..aad72e1b 100644 --- a/fuzz/qtest/fuzz.test +++ b/fuzz/qtest/fuzz.test @@ -11,6 +11,8 @@ my $td = new TestDriver('fuzz'); my $qpdf_corpus = $ENV{'QPDF_FUZZ_CORPUS'} || die "must set QPDF_FUZZ_CORPUS"; +my $n_qpdf_files = 77; # increment when adding new files + my @fuzzers = ( ['ascii85' => 1], ['dct' => 4], @@ -21,7 +23,12 @@ my @fuzzers = ( ['pngpredictor' => 1], ['runlength' => 6], ['tiffpredictor' => 2], - ['qpdf' => 77], # increment when adding new files + ['qpdf' => $n_qpdf_files], + ['qpdf_crypt' => $n_qpdf_files], + ['qpdf_crypt_insecure' => $n_qpdf_files], + ['qpdf_lin' => $n_qpdf_files], + ['qpdf_pages' => $n_qpdf_files], + ['qpdf_outlines' => $n_qpdf_files], ); my $n_tests = 0;