diff -Nru qpdf-11.5.0/debian/changelog qpdf-11.5.0/debian/changelog --- qpdf-11.5.0/debian/changelog 2023-07-09 14:42:20.000000000 +0000 +++ qpdf-11.5.0/debian/changelog 2024-03-20 14:40:27.000000000 +0000 @@ -1,3 +1,25 @@ +qpdf (11.5.0-1ubuntu1.1) mantic-security; urgency=medium + + * SECURITY UPDATE: heap overflow via std::__shared_count() + - debian/patches/CVE-2024-24246.patch: handle parse error stream data + in libqpdf/QPDF_json.cc, qpdf/qpdf.testcov, qpdf/qtest/*. + - CVE-2024-24246 + + -- Marc Deslauriers Wed, 20 Mar 2024 10:40:27 -0400 + +qpdf (11.5.0-1ubuntu1) mantic; urgency=medium + + * Fix data loss bug introduced in 11.0.0 and fixed in 11.6.3. The bug + causes the qpdf tokenizer to discard the character after a one-digit + or two-digit quoted octal string. Most writers don't create these, and + they are rare outside of content streams. By default, qpdf doesn't + parse content streams. The most common place for this to occur would + be in a document's /ID string, but in the worst case, this bug could + cause silent damage to some strings in a PDF file's metadata, such as + bookmark names or form field values. (LP: #2039804) + + -- Jay Berkenbilt Thu, 19 Oct 2023 07:20:25 -0400 + qpdf (11.5.0-1) unstable; urgency=medium * New upstream release. diff -Nru qpdf-11.5.0/debian/control qpdf-11.5.0/debian/control --- qpdf-11.5.0/debian/control 2023-07-09 14:42:20.000000000 +0000 +++ qpdf-11.5.0/debian/control 2023-10-19 11:20:25.000000000 +0000 @@ -2,7 +2,8 @@ Section: libs Priority: optional Build-Depends: cmake (>= 3.16~), debhelper (>> 10.3~), libjpeg-dev, zlib1g-dev, libgnutls28-dev -Maintainer: Jay Berkenbilt +Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Jay Berkenbilt Standards-Version: 4.6.2 Homepage: https://qpdf.sourceforge.io diff -Nru qpdf-11.5.0/debian/patches/CVE-2024-24246.patch qpdf-11.5.0/debian/patches/CVE-2024-24246.patch --- qpdf-11.5.0/debian/patches/CVE-2024-24246.patch 1970-01-01 00:00:00.000000000 +0000 +++ qpdf-11.5.0/debian/patches/CVE-2024-24246.patch 2024-03-20 14:39:21.000000000 +0000 @@ -0,0 +1,842 @@ +Backport of: + +From cb0f390cc1f98a8e82b27259f8f3cd5f162992eb Mon Sep 17 00:00:00 2001 +From: Jay Berkenbilt +Date: Sun, 4 Feb 2024 15:59:18 -0500 +Subject: [PATCH] Handle parse error stream data (fixes #1123) + +A parse error in stream data in which stream data contained a nested +object would cause a crash because qpdf was not correctly updating its +internal state. Rework the QPDF json reactor to not be sensitive to +parse errors in this way. +--- + libqpdf/QPDF_json.cc | 287 +++++++++--------- + qpdf/qpdf.testcov | 4 +- + qpdf/qtest/qpdf-json.test | 2 + + qpdf/qtest/qpdf/qjson-bad-data2.json | 71 +++++ + qpdf/qtest/qpdf/qjson-bad-data2.out | 2 + + qpdf/qtest/qpdf/qjson-bad-datafile2.json | 71 +++++ + qpdf/qtest/qpdf/qjson-bad-datafile2.out | 2 + + qpdf/qtest/qpdf/qjson-bad-pdf-version1.out | 4 +- + qpdf/qtest/qpdf/qjson-bad-pdf-version2.out | 4 +- + qpdf/qtest/qpdf/qjson-obj-key-errors.out | 6 +- + .../qtest/qpdf/qjson-stream-dict-not-dict.out | 3 +- + qpdf/qtest/qpdf/qjson-stream-not-dict.out | 1 + + qpdf/qtest/qpdf/qjson-trailer-stream.out | 1 + + qpdf/qtest/qpdf/update-from-json-errors.out | 4 +- + 14 files changed, 311 insertions(+), 151 deletions(-) + create mode 100644 qpdf/qtest/qpdf/qjson-bad-data2.json + create mode 100644 qpdf/qtest/qpdf/qjson-bad-data2.out + create mode 100644 qpdf/qtest/qpdf/qjson-bad-datafile2.json + create mode 100644 qpdf/qtest/qpdf/qjson-bad-datafile2.out + +--- a/libqpdf/QPDF_json.cc ++++ b/libqpdf/QPDF_json.cc +@@ -14,7 +14,7 @@ + + // This chart shows an example of the state transitions that would occur in parsing a minimal file. + +-// | st_initial ++// | + // { | -> st_top + // "qpdf": [ | -> st_qpdf + // { | -> st_qpdf_meta +@@ -47,7 +47,7 @@ + // } | <- st_objects + // } | <- st_qpdf + // ] | <- st_top +-// } | <- st_initial ++// } | + + static char const* JSON_PDF = ( + // force line break +@@ -99,7 +99,7 @@ is_indirect_object(std::string const& v, + } + obj = QUtil::string_to_int(o_str.c_str()); + gen = QUtil::string_to_int(g_str.c_str()); +- return true; ++ return obj > 0; + } + + static bool +@@ -250,7 +250,6 @@ class QPDF::JSONReactor: public JSON::Re + + private: + enum state_e { +- st_initial, + st_top, + st_qpdf, + st_qpdf_meta, +@@ -262,28 +261,35 @@ class QPDF::JSONReactor: public JSON::Re + st_ignore, + }; + ++ struct StackFrame ++ { ++ StackFrame(state_e state) : ++ state(state){}; ++ StackFrame(state_e state, QPDFObjectHandle&& object) : ++ state(state), ++ object(object){}; ++ state_e state; ++ QPDFObjectHandle object; ++ }; ++ + void containerStart(); +- void nestedState(std::string const& key, JSON const& value, state_e); ++ bool setNextStateIfDictionary(std::string const& key, JSON const& value, state_e); + void setObjectDescription(QPDFObjectHandle& oh, JSON const& value); + QPDFObjectHandle makeObject(JSON const& value); + void error(qpdf_offset_t offset, std::string const& message); +- void +- replaceObject(QPDFObjectHandle to_replace, QPDFObjectHandle replacement, JSON const& value); ++ void replaceObject(QPDFObjectHandle&& replacement, JSON const& value); + + QPDF& pdf; + std::shared_ptr is; + bool must_be_complete{true}; + std::shared_ptr descr; + bool errors{false}; +- bool parse_error{false}; + bool saw_qpdf{false}; + bool saw_qpdf_meta{false}; + bool saw_objects{false}; + bool saw_json_version{false}; + bool saw_pdf_version{false}; + bool saw_trailer{false}; +- state_e state{st_initial}; +- state_e next_state{st_top}; + std::string cur_object; + bool saw_value{false}; + bool saw_stream{false}; +@@ -291,9 +297,10 @@ class QPDF::JSONReactor: public JSON::Re + bool saw_data{false}; + bool saw_datafile{false}; + bool this_stream_needs_data{false}; +- std::vector state_stack{st_initial}; +- std::vector object_stack; + std::set reserved; ++ std::vector stack; ++ QPDFObjectHandle next_obj; ++ state_e next_state{st_top}; + }; + + void +@@ -316,8 +323,12 @@ QPDF::JSONReactor::anyErrors() const + void + QPDF::JSONReactor::containerStart() + { +- state_stack.push_back(state); +- state = next_state; ++ if (next_obj.isInitialized()) { ++ stack.emplace_back(next_state, std::move(next_obj)); ++ next_obj = QPDFObjectHandle(); ++ } else { ++ stack.emplace_back(next_state); ++ } + } + + void +@@ -329,20 +340,19 @@ QPDF::JSONReactor::dictionaryStart() + void + QPDF::JSONReactor::arrayStart() + { +- containerStart(); +- if (state == st_top) { ++ if (stack.empty()) { + QTC::TC("qpdf", "QPDF_json top-level array"); + throw std::runtime_error("QPDF JSON must be a dictionary"); + } ++ containerStart(); + } + + void + QPDF::JSONReactor::containerEnd(JSON const& value) + { +- auto from_state = state; +- state = state_stack.back(); +- state_stack.pop_back(); +- if (state == st_initial) { ++ auto from_state = stack.back().state; ++ stack.pop_back(); ++ if (stack.empty()) { + if (!this->saw_qpdf) { + QTC::TC("qpdf", "QPDF_json missing qpdf"); + error(0, "\"qpdf\" object was not seen"); +@@ -365,26 +375,16 @@ QPDF::JSONReactor::containerEnd(JSON con + } + } + } +- } else if (state == st_objects) { +- if (parse_error) { +- QTC::TC("qpdf", "QPDF_json don't check object after parse error"); +- } else if (cur_object == "trailer") { +- if (!saw_value) { +- QTC::TC("qpdf", "QPDF_json trailer no value"); +- error(value.getStart(), "\"trailer\" is missing \"value\""); +- } +- } else if (saw_value == saw_stream) { ++ } else if (from_state == st_trailer) { ++ if (!saw_value) { ++ QTC::TC("qpdf", "QPDF_json trailer no value"); ++ error(value.getStart(), "\"trailer\" is missing \"value\""); ++ } ++ } else if (from_state == st_object_top) { ++ if (saw_value == saw_stream) { + QTC::TC("qpdf", "QPDF_json value stream both or neither"); + error(value.getStart(), "object must have exactly one of \"value\" or \"stream\""); + } +- object_stack.clear(); +- this->cur_object = ""; +- this->saw_dict = false; +- this->saw_data = false; +- this->saw_datafile = false; +- this->saw_value = false; +- this->saw_stream = false; +- } else if (state == st_object_top) { + if (saw_stream) { + if (!saw_dict) { + QTC::TC("qpdf", "QPDF_json stream no dict"); +@@ -408,11 +408,7 @@ QPDF::JSONReactor::containerEnd(JSON con + } + } + } +- } else if ((state == st_stream) || (state == st_object)) { +- if (!parse_error) { +- object_stack.pop_back(); +- } +- } else if ((state == st_top) && (from_state == st_qpdf)) { ++ } else if (from_state == st_qpdf) { + // Handle dangling indirect object references which the PDF spec says to treat as nulls. + // It's tempting to make this an error, but that would be wrong since valid input files may + // have these. +@@ -423,16 +419,27 @@ QPDF::JSONReactor::containerEnd(JSON con + } + } + } ++ if (!stack.empty()) { ++ auto state = stack.back().state; ++ if (state == st_objects) { ++ this->cur_object = ""; ++ this->saw_dict = false; ++ this->saw_data = false; ++ this->saw_datafile = false; ++ this->saw_value = false; ++ this->saw_stream = false; ++ } ++ } + } + + void +-QPDF::JSONReactor::replaceObject( +- QPDFObjectHandle to_replace, QPDFObjectHandle replacement, JSON const& value) ++QPDF::JSONReactor::replaceObject(QPDFObjectHandle&& replacement, JSON const& value) + { +- auto og = to_replace.getObjGen(); ++ auto& tos = stack.back(); ++ auto og = tos.object.getObjGen(); + this->pdf.replaceObject(og, replacement); +- auto oh = pdf.getObject(og); +- setObjectDescription(oh, value); ++ next_obj = pdf.getObject(og); ++ setObjectDescription(tos.object, value); + } + + void +@@ -442,22 +449,26 @@ QPDF::JSONReactor::topLevelScalar() + throw std::runtime_error("QPDF JSON must be a dictionary"); + } + +-void +-QPDF::JSONReactor::nestedState(std::string const& key, JSON const& value, state_e next) ++bool ++QPDF::JSONReactor::setNextStateIfDictionary(std::string const& key, JSON const& value, state_e next) + { + // Use this method when the next state is for processing a nested dictionary. + if (value.isDictionary()) { + this->next_state = next; +- } else { +- error(value.getStart(), "\"" + key + "\" must be a dictionary"); +- this->next_state = st_ignore; +- this->parse_error = true; ++ return true; + } ++ error(value.getStart(), "\"" + key + "\" must be a dictionary"); ++ return false; + } + + bool + QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value) + { ++ if (stack.empty()) { ++ throw std::logic_error("stack is empty in dictionaryItem"); ++ } ++ next_state = st_ignore; ++ auto state = stack.back().state; + if (state == st_ignore) { + QTC::TC("qpdf", "QPDF_json ignoring in st_ignore"); + // ignore +@@ -467,51 +478,48 @@ QPDF::JSONReactor::dictionaryItem(std::s + if (!value.isArray()) { + QTC::TC("qpdf", "QPDF_json qpdf not array"); + error(value.getStart(), "\"qpdf\" must be an array"); +- next_state = st_ignore; +- parse_error = true; + } else { + next_state = st_qpdf; + } + } else { + // Ignore all other fields. + QTC::TC("qpdf", "QPDF_json ignoring unknown top-level key"); +- next_state = st_ignore; + } + } else if (state == st_qpdf_meta) { + if (key == "pdfversion") { + this->saw_pdf_version = true; +- bool version_okay = false; + std::string v; ++ bool okay = false; + if (value.getString(v)) { + std::string version; + char const* p = v.c_str(); + if (QPDF::validatePDFVersion(p, version) && (*p == '\0')) { +- version_okay = true; + this->pdf.m->pdf_version = version; ++ okay = true; + } + } +- if (!version_okay) { ++ if (!okay) { + QTC::TC("qpdf", "QPDF_json bad pdf version"); +- error(value.getStart(), "invalid PDF version (must be x.y)"); ++ error(value.getStart(), "invalid PDF version (must be \"x.y\")"); + } + } else if (key == "jsonversion") { + this->saw_json_version = true; +- bool version_okay = false; + std::string v; ++ bool okay = false; + if (value.getNumber(v)) { + std::string version; + if (QUtil::string_to_int(v.c_str()) == 2) { +- version_okay = true; ++ okay = true; + } + } +- if (!version_okay) { ++ if (!okay) { + QTC::TC("qpdf", "QPDF_json bad json version"); +- error(value.getStart(), "invalid JSON version (must be 2)"); ++ error(value.getStart(), "invalid JSON version (must be numeric value 2)"); + } + } else if (key == "pushedinheritedpageresources") { + bool v; + if (value.getBool(v)) { +- if ((!this->must_be_complete) && v) { ++ if (!this->must_be_complete && v) { + this->pdf.pushInheritedAttributesToPage(); + } + } else { +@@ -521,7 +529,7 @@ QPDF::JSONReactor::dictionaryItem(std::s + } else if (key == "calledgetallpages") { + bool v; + if (value.getBool(v)) { +- if ((!this->must_be_complete) && v) { ++ if (!this->must_be_complete && v) { + this->pdf.getAllPages(); + } + } else { +@@ -532,103 +540,95 @@ QPDF::JSONReactor::dictionaryItem(std::s + // ignore unknown keys for forward compatibility and to skip keys we don't care about + // like "maxobjectid". + QTC::TC("qpdf", "QPDF_json ignore second-level key"); +- next_state = st_ignore; + } + } else if (state == st_objects) { + int obj = 0; + int gen = 0; + if (key == "trailer") { + this->saw_trailer = true; +- nestedState(key, value, st_trailer); + this->cur_object = "trailer"; ++ setNextStateIfDictionary(key, value, st_trailer); + } else if (is_obj_key(key, obj, gen)) { + this->cur_object = key; +- auto oh = pdf.reserveObjectIfNotExists(QPDFObjGen(obj, gen)); +- object_stack.push_back(oh); +- nestedState(key, value, st_object_top); ++ if (setNextStateIfDictionary(key, value, st_object_top)) { ++ next_obj = pdf.reserveObjectIfNotExists(QPDFObjGen(obj, gen)); ++ } + } else { + QTC::TC("qpdf", "QPDF_json bad object key"); + error(value.getStart(), "object key should be \"trailer\" or \"obj:n n R\""); +- next_state = st_ignore; +- parse_error = true; + } + } else if (state == st_object_top) { +- if (object_stack.size() == 0) { +- throw std::logic_error("no object on stack in st_object_top"); ++ if (stack.empty()) { ++ throw std::logic_error("stack empty in st_object_top"); ++ } ++ auto& tos = stack.back(); ++ if (!tos.object.isInitialized()) { ++ throw std::logic_error("current object uninitialized in st_object_top"); + } +- auto tos = object_stack.back(); +- QPDFObjectHandle replacement; + if (key == "value") { +- // Don't use nestedState since this can have any type. ++ // Don't use setNextStateIfDictionary since this can have any type. + this->saw_value = true; ++ replaceObject(makeObject(value), value); + next_state = st_object; +- replacement = makeObject(value); +- replaceObject(tos, replacement, value); + } else if (key == "stream") { + this->saw_stream = true; +- nestedState(key, value, st_stream); +- this->this_stream_needs_data = false; +- if (tos.isStream()) { +- QTC::TC("qpdf", "QPDF_json updating existing stream"); ++ if (setNextStateIfDictionary(key, value, st_stream)) { ++ this->this_stream_needs_data = false; ++ if (tos.object.isStream()) { ++ QTC::TC("qpdf", "QPDF_json updating existing stream"); ++ } else { ++ this->this_stream_needs_data = true; ++ replaceObject(pdf.reserveStream(tos.object.getObjGen()), value); ++ } ++ next_obj = tos.object; + } else { +- this->this_stream_needs_data = true; +- replacement = pdf.reserveStream(tos.getObjGen()); +- replaceObject(tos, replacement, value); ++ // Error message already given above ++ QTC::TC("qpdf", "QPDF_json stream not a dictionary"); + } + } else { + // Ignore unknown keys for forward compatibility + QTC::TC("qpdf", "QPDF_json ignore unknown key in object_top"); +- next_state = st_ignore; +- } +- if (replacement.isInitialized()) { +- object_stack.pop_back(); +- object_stack.push_back(replacement); + } + } else if (state == st_trailer) { + if (key == "value") { + this->saw_value = true; +- // The trailer must be a dictionary, so we can use nestedState. +- nestedState("trailer.value", value, st_object); +- this->pdf.m->trailer = makeObject(value); +- setObjectDescription(this->pdf.m->trailer, value); ++ // The trailer must be a dictionary, so we can use setNextStateIfDictionary. ++ if (setNextStateIfDictionary("trailer.value", value, st_object)) { ++ this->pdf.m->trailer = makeObject(value); ++ setObjectDescription(this->pdf.m->trailer, value); ++ } + } else if (key == "stream") { + // Don't need to set saw_stream here since there's already an error. + QTC::TC("qpdf", "QPDF_json trailer stream"); + error(value.getStart(), "the trailer may not be a stream"); +- next_state = st_ignore; +- parse_error = true; + } else { + // Ignore unknown keys for forward compatibility + QTC::TC("qpdf", "QPDF_json ignore unknown key in trailer"); +- next_state = st_ignore; + } + } else if (state == st_stream) { +- if (object_stack.size() == 0) { +- throw std::logic_error("no object on stack in st_stream"); ++ if (stack.empty()) { ++ throw std::logic_error("stack empty in st_stream"); + } +- auto tos = object_stack.back(); +- if (!tos.isStream()) { +- throw std::logic_error("top of stack is not stream in st_stream"); ++ auto& tos = stack.back(); ++ if (!tos.object.isStream()) { ++ throw std::logic_error("current object is not stream in st_stream"); + } + auto uninitialized = QPDFObjectHandle(); + if (key == "dict") { + this->saw_dict = true; +- // Since a stream dictionary must be a dictionary, we can use nestedState to transition +- // to st_value. +- nestedState("stream.dict", value, st_object); +- auto dict = makeObject(value); +- if (dict.isDictionary()) { +- tos.replaceDict(dict); ++ if (setNextStateIfDictionary("stream.dict", value, st_object)) { ++ tos.object.replaceDict(makeObject(value)); + } else { +- // An error had already been given by nestedState ++ // An error had already been given by setNextStateIfDictionary + QTC::TC("qpdf", "QPDF_json stream dict not dict"); +- parse_error = true; + } + } else if (key == "data") { + this->saw_data = true; + std::string v; + if (!value.getString(v)) { ++ QTC::TC("qpdf", "QPDF_json stream data not string"); + error(value.getStart(), "\"stream.data\" must be a string"); ++ tos.object.replaceStreamData("", uninitialized, uninitialized); + } else { + // The range includes the quotes. + auto start = value.getStart() + 1; +@@ -636,32 +636,40 @@ QPDF::JSONReactor::dictionaryItem(std::s + if (end < start) { + throw std::logic_error("QPDF_json: JSON string length < 0"); + } +- tos.replaceStreamData(provide_data(is, start, end), uninitialized, uninitialized); ++ tos.object.replaceStreamData( ++ provide_data(is, start, end), uninitialized, uninitialized); + } + } else if (key == "datafile") { + this->saw_datafile = true; + std::string filename; +- if (value.getString(filename)) { +- tos.replaceStreamData(QUtil::file_provider(filename), uninitialized, uninitialized); +- } else { ++ if (!value.getString(filename)) { ++ QTC::TC("qpdf", "QPDF_json stream datafile not string"); + error( + value.getStart(), +- "\"stream.datafile\" must be a string containing a file " +- "name"); ++ "\"stream.datafile\" must be a string containing a file name"); ++ tos.object.replaceStreamData("", uninitialized, uninitialized); ++ } else { ++ tos.object.replaceStreamData( ++ QUtil::file_provider(filename), uninitialized, uninitialized); + } + } else { + // Ignore unknown keys for forward compatibility. + QTC::TC("qpdf", "QPDF_json ignore unknown key in stream"); +- next_state = st_ignore; + } + } else if (state == st_object) { +- if (!parse_error) { +- auto dict = object_stack.back(); +- if (dict.isStream()) { +- dict = dict.getDict(); +- } +- dict.replaceKey(key, makeObject(value)); ++ if (stack.empty()) { ++ throw std::logic_error("stack empty in st_object"); ++ } ++ auto& tos = stack.back(); ++ auto dict = tos.object; ++ if (dict.isStream()) { ++ dict = dict.getDict(); ++ } ++ if (!dict.isDictionary()) { ++ throw std::logic_error( ++ "current object is not stream or dictionary in st_object dictionary item"); + } ++ dict.replaceKey(key, makeObject(value)); + } else { + throw std::logic_error("QPDF_json: unknown state " + std::to_string(state)); + } +@@ -671,25 +679,24 @@ QPDF::JSONReactor::dictionaryItem(std::s + bool + QPDF::JSONReactor::arrayItem(JSON const& value) + { ++ if (stack.empty()) { ++ throw std::logic_error("stack is empty in arrayItem"); ++ } ++ next_state = st_ignore; ++ auto state = stack.back().state; + if (state == st_qpdf) { + if (!this->saw_qpdf_meta) { + this->saw_qpdf_meta = true; +- nestedState("qpdf[0]", value, st_qpdf_meta); ++ setNextStateIfDictionary("qpdf[0]", value, st_qpdf_meta); + } else if (!this->saw_objects) { + this->saw_objects = true; +- nestedState("qpdf[1]", value, st_objects); ++ setNextStateIfDictionary("qpdf[1]", value, st_objects); + } else { + QTC::TC("qpdf", "QPDF_json more than two qpdf elements"); + error(value.getStart(), "\"qpdf\" must have two elements"); +- next_state = st_ignore; +- parse_error = true; +- } +- } +- if (state == st_object) { +- if (!parse_error) { +- auto tos = object_stack.back(); +- tos.appendItem(makeObject(value)); + } ++ } else if (state == st_object) { ++ stack.back().object.appendItem(makeObject(value)); + } + return true; + } +@@ -714,10 +721,12 @@ QPDF::JSONReactor::makeObject(JSON const + bool bool_v = false; + if (value.isDictionary()) { + result = QPDFObjectHandle::newDictionary(); +- object_stack.push_back(result); ++ next_obj = result; ++ next_state = st_object; + } else if (value.isArray()) { + result = QPDFObjectHandle::newArray(); +- object_stack.push_back(result); ++ next_obj = result; ++ next_state = st_object; + } else if (value.isNull()) { + result = QPDFObjectHandle::newNull(); + } else if (value.getBool(bool_v)) { +--- a/qpdf/qpdf.testcov ++++ b/qpdf/qpdf.testcov +@@ -660,7 +660,6 @@ QPDF_json value stream both or neither 0 + QPDFJob need json-stream-prefix for stdout 0 + QPDFJob write json to stdout 0 + QPDFJob write json to file 0 +-QPDF_json don't check object after parse error 0 + QPDF_json ignoring unknown top-level key 0 + QPDF_json ignore second-level key 0 + QPDF_json ignore unknown key in object_top 0 +@@ -681,3 +680,6 @@ QPDFPageObjectHelper used fallback witho + QPDF skipping cache for known unchecked object 0 + QPDF fix dangling triggered xref reconstruction 0 + QPDFPageDocumentHelper flatten resources missing or invalid 0 ++QPDF_json stream data not string 0 ++QPDF_json stream datafile not string 0 ++QPDF_json stream not a dictionary 0 +--- a/qpdf/qtest/qpdf-json.test ++++ b/qpdf/qtest/qpdf-json.test +@@ -37,6 +37,8 @@ my @badfiles = ( + 'obj-key-errors', + 'bad-data', + 'bad-datafile', ++ 'bad-data2', ++ 'bad-datafile2', + ); + + $n_tests += scalar(@badfiles); +--- /dev/null ++++ b/qpdf/qtest/qpdf/qjson-bad-data2.json +@@ -0,0 +1,71 @@ ++{ ++ "qpdf": [ ++ { ++ "jsonversion": 2, ++ "pdfversion": "1.3", ++ "maxobjectid": 6 ++ }, ++ { ++ "obj:1 0 R": { ++ "value": { ++ "/Pages": "2 0 R", ++ "/Type": "/Catalog" ++ } ++ }, ++ "obj:2 0 R": { ++ "value": { ++ "/Count": 1, ++ "/Kids": [ ++ "3 0 R" ++ ], ++ "/Type": "/Pages" ++ } ++ }, ++ "obj:3 0 R": { ++ "value": { ++ "/Contents": ["4 0 R", "7 0 R"], ++ "/MediaBox": [ ++ 0, ++ 0, ++ 612, ++ 792 ++ ], ++ "/Parent": "2 0 R", ++ "/Resources": { ++ "/Font": { ++ "/F1": "6 0 R" ++ }, ++ "/ProcSet": "5 0 R" ++ }, ++ "/Type": "/Page" ++ } ++ }, ++ "obj:4 0 R": { ++ "stream": { ++ "data": [[]], ++ "dict": {} ++ } ++ }, ++ "obj:5 0 R": { ++ "value": [ ++ "/PDF", ++ "/Text" ++ ] ++ }, ++ "obj:6 0 R": { ++ "value": { ++ "/BaseFont": "/Helvetica", ++ "/Encoding": "/WinAnsiEncoding", ++ "/Subtype": "/Type1", ++ "/Type": "/Font" ++ } ++ }, ++ "trailer": { ++ "value": { ++ "/Root": "1 0 R", ++ "/Size": 7 ++ } ++ } ++ } ++ ] ++} +--- /dev/null ++++ b/qpdf/qtest/qpdf/qjson-bad-data2.out +@@ -0,0 +1,2 @@ ++WARNING: qjson-bad-data2.json (obj:4 0 R, offset 846): "stream.data" must be a string ++qpdf: qjson-bad-data2.json: errors found in JSON +--- /dev/null ++++ b/qpdf/qtest/qpdf/qjson-bad-datafile2.json +@@ -0,0 +1,71 @@ ++{ ++ "qpdf": [ ++ { ++ "jsonversion": 2, ++ "pdfversion": "1.3", ++ "maxobjectid": 6 ++ }, ++ { ++ "obj:1 0 R": { ++ "value": { ++ "/Pages": "2 0 R", ++ "/Type": "/Catalog" ++ } ++ }, ++ "obj:2 0 R": { ++ "value": { ++ "/Count": 1, ++ "/Kids": [ ++ "3 0 R" ++ ], ++ "/Type": "/Pages" ++ } ++ }, ++ "obj:3 0 R": { ++ "value": { ++ "/Contents": ["4 0 R", "7 0 R"], ++ "/MediaBox": [ ++ 0, ++ 0, ++ 612, ++ 792 ++ ], ++ "/Parent": "2 0 R", ++ "/Resources": { ++ "/Font": { ++ "/F1": "6 0 R" ++ }, ++ "/ProcSet": "5 0 R" ++ }, ++ "/Type": "/Page" ++ } ++ }, ++ "obj:4 0 R": { ++ "stream": { ++ "datafile": [[]], ++ "dict": {} ++ } ++ }, ++ "obj:5 0 R": { ++ "value": [ ++ "/PDF", ++ "/Text" ++ ] ++ }, ++ "obj:6 0 R": { ++ "value": { ++ "/BaseFont": "/Helvetica", ++ "/Encoding": "/WinAnsiEncoding", ++ "/Subtype": "/Type1", ++ "/Type": "/Font" ++ } ++ }, ++ "trailer": { ++ "value": { ++ "/Root": "1 0 R", ++ "/Size": 7 ++ } ++ } ++ } ++ ] ++} +--- /dev/null ++++ b/qpdf/qtest/qpdf/qjson-bad-datafile2.out +@@ -0,0 +1,2 @@ ++WARNING: qjson-bad-datafile2.json (obj:4 0 R, offset 850): "stream.datafile" must be a string containing a file name ++qpdf: qjson-bad-datafile2.json: errors found in JSON +--- a/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out ++++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out +@@ -1,3 +1,3 @@ +-WARNING: qjson-bad-pdf-version1.json (offset 41): invalid JSON version (must be 2) +-WARNING: qjson-bad-pdf-version1.json (offset 70): invalid PDF version (must be x.y) ++WARNING: qjson-bad-pdf-version1.json (offset 41): invalid JSON version (must be numeric value 2) ++WARNING: qjson-bad-pdf-version1.json (offset 70): invalid PDF version (must be "x.y") + qpdf: qjson-bad-pdf-version1.json: errors found in JSON +--- a/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out ++++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out +@@ -1,5 +1,5 @@ +-WARNING: qjson-bad-pdf-version2.json (offset 41): invalid JSON version (must be 2) +-WARNING: qjson-bad-pdf-version2.json (offset 66): invalid PDF version (must be x.y) ++WARNING: qjson-bad-pdf-version2.json (offset 41): invalid JSON version (must be numeric value 2) ++WARNING: qjson-bad-pdf-version2.json (offset 66): invalid PDF version (must be "x.y") + WARNING: qjson-bad-pdf-version2.json (offset 97): calledgetallpages must be a boolean + WARNING: qjson-bad-pdf-version2.json (offset 138): pushedinheritedpageresources must be a boolean + qpdf: qjson-bad-pdf-version2.json: errors found in JSON +--- a/qpdf/qtest/qpdf/qjson-obj-key-errors.out ++++ b/qpdf/qtest/qpdf/qjson-obj-key-errors.out +@@ -1,7 +1,7 @@ + WARNING: qjson-obj-key-errors.json (obj:2 0 R, offset 244): object must have exactly one of "value" or "stream" + WARNING: qjson-obj-key-errors.json (obj:3 0 R, offset 542): object must have exactly one of "value" or "stream" +-WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 710): "stream" is missing "dict" +-WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 710): new "stream" must have exactly one of "data" or "datafile" +-WARNING: qjson-obj-key-errors.json (obj:5 0 R, offset 800): new "stream" must have exactly one of "data" or "datafile" ++WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 690): "stream" is missing "dict" ++WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 690): new "stream" must have exactly one of "data" or "datafile" ++WARNING: qjson-obj-key-errors.json (obj:5 0 R, offset 780): new "stream" must have exactly one of "data" or "datafile" + WARNING: qjson-obj-key-errors.json (trailer, offset 1178): "trailer" is missing "value" + qpdf: qjson-obj-key-errors.json: errors found in JSON +--- a/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out ++++ b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out +@@ -1,5 +1,4 @@ + WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 142): "stream.dict" must be a dictionary +-WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 142): unrecognized string value +-WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 122): new "stream" must have exactly one of "data" or "datafile" ++WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 102): new "stream" must have exactly one of "data" or "datafile" + WARNING: qjson-stream-dict-not-dict.json: "qpdf[1].trailer" was not seen + qpdf: qjson-stream-dict-not-dict.json: errors found in JSON +--- a/qpdf/qtest/qpdf/qjson-stream-not-dict.out ++++ b/qpdf/qtest/qpdf/qjson-stream-not-dict.out +@@ -1,3 +1,4 @@ + WARNING: qjson-stream-not-dict.json (obj:1 0 R, offset 122): "stream" must be a dictionary ++WARNING: qjson-stream-not-dict.json (obj:1 0 R, offset 102): "stream" is missing "dict" + WARNING: qjson-stream-not-dict.json: "qpdf[1].trailer" was not seen + qpdf: qjson-stream-not-dict.json: errors found in JSON +--- a/qpdf/qtest/qpdf/qjson-trailer-stream.out ++++ b/qpdf/qtest/qpdf/qjson-trailer-stream.out +@@ -1,2 +1,3 @@ + WARNING: qjson-trailer-stream.json (trailer, offset 1269): the trailer may not be a stream ++WARNING: qjson-trailer-stream.json (trailer, offset 1249): "trailer" is missing "value" + qpdf: qjson-trailer-stream.json: errors found in JSON +--- a/qpdf/qtest/qpdf/update-from-json-errors.out ++++ b/qpdf/qtest/qpdf/update-from-json-errors.out +@@ -1,4 +1,4 @@ +-WARNING: good13.pdf (obj:4 0 R from qpdf-json-update-errors.json, offset 95): existing "stream" may at most one of "data" or "datafile" ++WARNING: good13.pdf (obj:4 0 R from qpdf-json-update-errors.json, offset 75): existing "stream" may at most one of "data" or "datafile" + WARNING: good13.pdf (obj:20 0 R from qpdf-json-update-errors.json, offset 335): unrecognized string value +-WARNING: good13.pdf (obj:20 0 R from qpdf-json-update-errors.json, offset 293): new "stream" must have exactly one of "data" or "datafile" ++WARNING: good13.pdf (obj:20 0 R from qpdf-json-update-errors.json, offset 273): new "stream" must have exactly one of "data" or "datafile" + qpdf: qpdf-json-update-errors.json: errors found in JSON diff -Nru qpdf-11.5.0/debian/patches/series qpdf-11.5.0/debian/patches/series --- qpdf-11.5.0/debian/patches/series 2023-07-09 14:42:20.000000000 +0000 +++ qpdf-11.5.0/debian/patches/series 2024-03-20 14:33:12.000000000 +0000 @@ -0,0 +1,2 @@ +tokenizer-1ecc6bb2 -p1 +CVE-2024-24246.patch diff -Nru qpdf-11.5.0/debian/patches/tokenizer-1ecc6bb2 qpdf-11.5.0/debian/patches/tokenizer-1ecc6bb2 --- qpdf-11.5.0/debian/patches/tokenizer-1ecc6bb2 1970-01-01 00:00:00.000000000 +0000 +++ qpdf-11.5.0/debian/patches/tokenizer-1ecc6bb2 2023-10-19 11:20:25.000000000 +0000 @@ -0,0 +1,34 @@ +commit 1ecc6bb29e24a4f89470ff91b2682b46e0576ad4 +Author: Jay Berkenbilt +Date: Sat Oct 14 17:04:58 2023 -0400 + + Don't lose character after \d or \dd parsing string (fixes #1050) + +diff --git a/libqpdf/QPDFTokenizer.cc b/libqpdf/QPDFTokenizer.cc +index d98af8a9..ca09708a 100644 +--- a/libqpdf/QPDFTokenizer.cc ++++ b/libqpdf/QPDFTokenizer.cc +@@ -692,16 +691,21 @@ QPDFTokenizer::inHexstring2nd(char ch) + void + QPDFTokenizer::inCharCode(char ch) + { ++ bool handled = false; + if (('0' <= ch) && (ch <= '7')) { + this->char_code = 8 * this->char_code + (int(ch) - int('0')); + if (++(this->digit_count) < 3) { + return; + } +- // We've accumulated \ddd. PDF Spec says to ignore high-order overflow. ++ handled = true; + } ++ // We've accumulated \ddd or we have \d or \dd followed by other than an octal digit. The PDF ++ // Spec says to ignore high-order overflow. + this->val += char(this->char_code % 256); + this->state = st_in_string; +- return; ++ if (!handled) { ++ inString(ch); ++ } + } + + void