diff --git a/README-maintainer b/README-maintainer index 2a6c4645..dc3e1d5b 100644 --- a/README-maintainer +++ b/README-maintainer @@ -19,14 +19,15 @@ Memory checks: GOOGLE OSS-FUZZ -* https://github.com/google/oss-fuzz/tree/master/projects/qpdf -* To test locally, see https://github.com/google/oss-fuzz/tree/master/docs/, - especially new_project_guide.md +* qpdf project: https://github.com/google/oss-fuzz/tree/master/projects/qpdf -Clone the oss-fuzz project. From the root directory of the repository: +* To test locally, see https://github.com/google/oss-fuzz/tree/master/docs/, + especially new_project_guide.md. Summary: + + Clone the oss-fuzz project. From the root directory of the repository: Add `-e GITHUB_FORK=fork -e GITHUB_BRANCH=branch` to build_fuzzers - to work off a fork/branch rather than qpdf/master. + from a qpdf fork/branch rather than qpdf/master. python infra/helper.py build_image --pull qpdf python infra/helper.py build_fuzzers qpdf @@ -34,6 +35,24 @@ Clone the oss-fuzz project. From the root directory of the repository: python infra/helper.py build_fuzzers --sanitizer coverage qpdf python infra/helper.py coverage qpdf + The fuzzer is in build/out/qpdf. It can be run with a directory as + an argument to run against files in a directory. You can use + + qpdf_fuzzer -merge=1 cur new >& /dev/null& + + to add any files from new into cur if they increase coverage. You + need to do this with the coverage build (the one with + --sanitizer coverage) + +* General documentation: http://libfuzzer.info + +* Build status: https://oss-fuzz-build-logs.storage.googleapis.com/index.html + +* Project status: https://oss-fuzz.com/ (private -- log in with Google account) + +* Latest corpus: + gs://qpdf-backup.clusterfuzz-external.appspot.com/corpus/libFuzzer/qpdf_fuzzer/latest.zip + CODING RULES * Avoid atoi. Use QUtil::string_to_int instead. It does diff --git a/fuzz/build.mk b/fuzz/build.mk index 5d7ab903..1ecdac9b 100644 --- a/fuzz/build.mk +++ b/fuzz/build.mk @@ -2,6 +2,7 @@ # https://github.com/google/oss-fuzz/tree/master/projects/qpdf FUZZERS = \ + qpdf_fuzzer \ qpdf_read_memory_fuzzer DEFAULT_FUZZ_RUNNER := standalone_fuzz_target_runner diff --git a/fuzz/qpdf_fuzzer.cc b/fuzz/qpdf_fuzzer.cc new file mode 100644 index 00000000..3fb33109 --- /dev/null +++ b/fuzz/qpdf_fuzzer.cc @@ -0,0 +1,212 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DiscardContents: public QPDFObjectHandle::ParserCallbacks +{ + public: + virtual ~DiscardContents() {} + virtual void handleObject(QPDFObjectHandle) {} + virtual void handleEOF() {} +}; + +class FuzzHelper +{ + public: + FuzzHelper(unsigned char const* data, size_t size); + void run(); + + private: + PointerHolder getQpdf(); + PointerHolder getWriter(PointerHolder); + void doWrite(PointerHolder w); + void testWrite(); + void testPages(); + 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) +{ +} + +PointerHolder +FuzzHelper::getQpdf() +{ + PointerHolder is = + new BufferInputSource("fuzz input", &this->input_buffer); + PointerHolder qpdf = new QPDF(); + qpdf->processInputSource(is); + return qpdf; +} + +PointerHolder +FuzzHelper::getWriter(PointerHolder qpdf) +{ + PointerHolder w = new QPDFWriter(*qpdf); + w->setOutputPipeline(&this->discard); + w->setDeterministicID(true); + w->setDecodeLevel(qpdf_dl_all); + w->setCompressStreams(false); + return w; +} + +void +FuzzHelper::doWrite(PointerHolder w) +{ + try + { + w->write(); + } + catch (QPDFExc const& e) + { + std::cerr << e.what() << std::endl; + } +} + +void +FuzzHelper::testWrite() +{ + // Write in various ways to exercise QPDFWriter + + PointerHolder q; + PointerHolder w; + + q = getQpdf(); + w = getWriter(q); + doWrite(w); + + q = getQpdf(); + w = getWriter(q); + w->setLinearization(true); + doWrite(w); + + q = getQpdf(); + w = getWriter(q); + w->setObjectStreamMode(qpdf_o_disable); + doWrite(w); + + q = getQpdf(); + w = getWriter(q); + w->setObjectStreamMode(qpdf_o_generate); + doWrite(w); +} + +void +FuzzHelper::testPages() +{ + // Parse all content streams, and exercise some helpers that + // operate on pages. + PointerHolder q = getQpdf(); + QPDFPageDocumentHelper pdh(*q); + QPDFPageLabelDocumentHelper pldh(*q); + QPDFOutlineDocumentHelper odh(*q); + QPDFAcroFormDocumentHelper afdh(*q); + std::vector pages = pdh.getAllPages(); + DiscardContents discard_contents; + int pageno = 0; + for (std::vector::iterator iter = + pages.begin(); + iter != pages.end(); ++iter) + { + QPDFPageObjectHelper& page(*iter); + ++pageno; + try + { + page.parsePageContents(&discard_contents); + page.getPageImages(); + pldh.getLabelForPage(pageno); + odh.getOutlinesForPage(page.getObjectHandle().getObjGen()); + + std::vector annotations = + afdh.getWidgetAnnotationsForPage(page); + for (std::vector::iterator annot_iter = + annotations.begin(); + annot_iter != annotations.end(); ++annot_iter) + { + QPDFAnnotationObjectHelper& aoh = *annot_iter; + afdh.getFieldForAnnotation(aoh); + } + } + catch (QPDFExc& e) + { + std::cerr << "page " << pageno << ": " + << e.what() << std::endl; + } + } +} + +void +FuzzHelper::testOutlines() +{ + PointerHolder q = getQpdf(); + std::list > queue; + QPDFOutlineDocumentHelper odh(*q); + queue.push_back(odh.getTopLevelOutlines()); + while (! queue.empty()) + { + std::list& outlines = *(queue.begin()); + for (std::list::iterator iter = + outlines.begin(); + iter != outlines.end(); ++iter) + { + QPDFOutlineObjectHelper& ol = *iter; + ol.getDestPage(); + queue.push_back(ol.getKids()); + } + queue.pop_front(); + } +} + +void +FuzzHelper::doChecks() +{ + // Get as much coverage as possible in parts of the library that + // might benefit from fuzzing. + testWrite(); + testPages(); + 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) +{ + FuzzHelper f(data, size); + f.run(); + return 0; +} diff --git a/fuzz/standalone_fuzz_target_runner.cc b/fuzz/standalone_fuzz_target_runner.cc index 7038f188..9881d0fb 100644 --- a/fuzz/standalone_fuzz_target_runner.cc +++ b/fuzz/standalone_fuzz_target_runner.cc @@ -20,8 +20,7 @@ int main(int argc, char **argv) in.seekg(0, in.end); size_t length = in.tellg(); in.seekg (0, in.beg); - std::cout << "Reading " << length << " bytes from " << argv[i] - << std::endl; + std::cout << "checking " << argv[i] << std::endl; // Allocate exactly length bytes so that we reliably catch // buffer overflows. std::vector bytes(length); @@ -30,7 +29,7 @@ int main(int argc, char **argv) LLVMFuzzerTestOneInput( reinterpret_cast(bytes.data()), bytes.size()); - std::cout << "Execution successful" << std::endl; + std::cout << argv[i] << " successful" << std::endl; } return 0; }