diff --git a/include/qpdf/Constants.h b/include/qpdf/Constants.h index 4f62fad..e6f7b3e 100644 --- a/include/qpdf/Constants.h +++ b/include/qpdf/Constants.h @@ -273,12 +273,15 @@ enum qpdf_result_e { * options or global limits.For the meaning of individual parameters see `qpdf/global.cc` */ enum qpdf_param_e { + /* global state */ + qpdf_p_limit_errors = 0x10020, + /* global options */ - qpdf_p_default_limits = 0x10100, + qpdf_p_default_limits = 0x11100, /* global limits */ /* object - parser limits */ - qpdf_p_objects_max_nesting = 0x11000, + qpdf_p_objects_max_nesting = 0x12000, qpdf_p_objects_max_errors, qpdf_p_objects_max_container_size, qpdf_p_objects_max_container_size_damaged, diff --git a/include/qpdf/global.hh b/include/qpdf/global.hh index 042e979..3ad2550 100644 --- a/include/qpdf/global.hh +++ b/include/qpdf/global.hh @@ -54,6 +54,18 @@ namespace qpdf::global handle_result(qpdf_global_set_uint32(param, value)); } + /// @brief Retrieves the number of limit errors. + /// + /// Returns the number a global limit was exceeded. This item is reaf only. + /// + /// @return The number of limit errors. + /// + /// @since 12.3 + uint32_t inline limit_errors() + { + return get_uint32(qpdf_p_limit_errors); + } + namespace options { /// @brief Retrieves whether default limits are enabled. diff --git a/libqpdf/QPDFParser.cc b/libqpdf/QPDFParser.cc index 7b6d855..00dc52b 100644 --- a/libqpdf/QPDFParser.cc +++ b/libqpdf/QPDFParser.cc @@ -437,7 +437,8 @@ QPDFParser::parseRemainder(bool content_stream) case QPDFTokenizer::tt_array_open: case QPDFTokenizer::tt_dict_open: if (stack.size() > max_nesting) { - warn("ignoring excessively deeply nested data structure"); + global::Limits::error(); + warn("limits error: ignoring excessively deeply nested data structure"); return {}; } else { b_contents = false; @@ -647,8 +648,10 @@ QPDFParser::check_too_many_bad_tokens() auto limit = Limits::objects_max_container_size(bad_count || sanity_checks); if (frame->olist.size() > limit || frame->dict.size() > limit) { if (bad_count) { + Limits::error(); warn( - "encountered errors while parsing an array or dictionary with more than " + + "limits error: encountered errors while parsing an array or dictionary with more " + "than " + std::to_string(limit) + " elements; giving up on reading object"); throw Error(); } diff --git a/libqpdf/global.cc b/libqpdf/global.cc index 6bca03b..ae49eb0 100644 --- a/libqpdf/global.cc +++ b/libqpdf/global.cc @@ -38,6 +38,9 @@ qpdf_global_get_uint32(qpdf_param_e param, uint32_t* value) case qpdf_p_default_limits: *value = Options::default_limits(); return qpdf_r_ok; + case qpdf_p_limit_errors: + *value = Limits::errors(); + return qpdf_r_ok; case qpdf_p_objects_max_nesting: *value = Limits::objects_max_nesting(); return qpdf_r_ok; diff --git a/libqpdf/qpdf/global_private.hh b/libqpdf/qpdf/global_private.hh index 3a81256..93e7522 100644 --- a/libqpdf/qpdf/global_private.hh +++ b/libqpdf/qpdf/global_private.hh @@ -48,6 +48,21 @@ namespace qpdf::global static void objects_max_container_size(bool damaged, uint32_t value); + /// Record a limit error. + static void + error() + { + if (l.errors_ < std::numeric_limits::max()) { + ++l.errors_; + } + } + + static uint32_t const& + errors() + { + return l.errors_; + } + static void disable_defaults(); private: @@ -56,6 +71,8 @@ namespace qpdf::global static Limits l; + uint32_t errors_{0}; + uint32_t objects_max_nesting_{499}; uint32_t objects_max_errors_{15}; bool objects_max_errors_set_{false}; diff --git a/libtests/objects.cc b/libtests/objects.cc index 60d80b4..a23c004 100644 --- a/libtests/objects.cc +++ b/libtests/objects.cc @@ -221,6 +221,22 @@ test_2(QPDF& pdf, char const* arg2) thrown = true; } assert(thrown); + + /* Test limit errors */ + assert(qpdf::global::limit_errors() == 0); + QPDFObjectHandle::parse("[[[[]]]]"); + assert(qpdf::global::limit_errors() == 0); + objects_max_nesting(3); + try { + QPDFObjectHandle::parse("[[[[[]]]]]"); + } catch (std::exception&) { + } + assert(qpdf::global::limit_errors() == 1); + try { + QPDFObjectHandle::parse("[[[[[]]]]]"); + } catch (std::exception&) { + } + assert(qpdf::global::limit_errors() == 2); } void diff --git a/qpdf/qtest/qpdf/issue-146.out b/qpdf/qtest/qpdf/issue-146.out index 40fbe4c..96dcede 100644 --- a/qpdf/qtest/qpdf/issue-146.out +++ b/qpdf/qtest/qpdf/issue-146.out @@ -1,7 +1,7 @@ WARNING: issue-146.pdf: file is damaged WARNING: issue-146.pdf: can't find startxref WARNING: issue-146.pdf: Attempting to reconstruct cross-reference table -WARNING: issue-146.pdf (trailer, offset 695): ignoring excessively deeply nested data structure +WARNING: issue-146.pdf (trailer, offset 695): limits error: ignoring excessively deeply nested data structure WARNING: issue-146.pdf (object 1 0, offset 92): expected endobj WARNING: issue-146.pdf (object 7 0, offset 146): unknown token while reading object; treating as null WARNING: issue-146.pdf (object 7 0, offset 168): expected endobj diff --git a/qpdf/qtest/qpdf/issue-202.out b/qpdf/qtest/qpdf/issue-202.out index 913c379..e62f736 100644 --- a/qpdf/qtest/qpdf/issue-202.out +++ b/qpdf/qtest/qpdf/issue-202.out @@ -1,8 +1,8 @@ -WARNING: issue-202.pdf (trailer, offset 55770): ignoring excessively deeply nested data structure +WARNING: issue-202.pdf (trailer, offset 55770): limits error: ignoring excessively deeply nested data structure WARNING: issue-202.pdf: file is damaged WARNING: issue-202.pdf (offset 54769): expected trailer dictionary WARNING: issue-202.pdf: Attempting to reconstruct cross-reference table -WARNING: issue-202.pdf (trailer, offset 55770): ignoring excessively deeply nested data structure +WARNING: issue-202.pdf (trailer, offset 55770): limits error: ignoring excessively deeply nested data structure WARNING: issue-202.pdf (object 222 0, offset 50101): dictionary has duplicated key /Creator; last occurrence overrides earlier ones WARNING: issue-202.pdf (object 222 0, offset 50101): dictionary has duplicated key /Producer; last occurrence overrides earlier ones WARNING: issue-202.pdf: unable to find trailer dictionary while recovering damaged file