Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Parse iteratively to avoid stack overflow (fixes #146)
  • Loading branch information
jberkenbilt committed Aug 26, 2017
1 parent 85f05cc commit ad527a6
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 108 deletions.
3 changes: 3 additions & 0 deletions ChangeLog
@@ -1,5 +1,8 @@
2017-08-25 Jay Berkenbilt <ejb@ql.org>

* Re-implement parser iteratively to avoid stack overflow on very
deeply nested arrays and dictionaries. Fixes #146.

* Detect infinite loop while finding additional xref tables. Fixes
#149.

Expand Down
1 change: 0 additions & 1 deletion include/qpdf/QPDFObjectHandle.hh
Expand Up @@ -667,7 +667,6 @@ class QPDFObjectHandle
std::string const& object_description,
QPDFTokenizer& tokenizer, bool& empty,
StringDecrypter* decrypter, QPDF* context,
bool in_array, bool in_dictionary,
bool content_stream);
static void parseContentStream_internal(
PointerHolder<Buffer> stream_data,
Expand Down
248 changes: 141 additions & 107 deletions libqpdf/QPDFObjectHandle.cc
Expand Up @@ -883,8 +883,7 @@ QPDFObjectHandle::parseContentStream_internal(PointerHolder<Buffer> stream_data,
while (static_cast<size_t>(input->tell()) < length)
{
QPDFObjectHandle obj =
parseInternal(input, "content", tokenizer, empty,
0, 0, false, false, true);
parseInternal(input, "content", tokenizer, empty, 0, 0, true);
if (! obj.isInitialized())
{
// EOF
Expand Down Expand Up @@ -945,15 +944,14 @@ QPDFObjectHandle::parse(PointerHolder<InputSource> input,
StringDecrypter* decrypter, QPDF* context)
{
return parseInternal(input, object_description, tokenizer, empty,
decrypter, context, false, false, false);
decrypter, context, false);
}

QPDFObjectHandle
QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
std::string const& object_description,
QPDFTokenizer& tokenizer, bool& empty,
StringDecrypter* decrypter, QPDF* context,
bool in_array, bool in_dictionary,
bool content_stream)
{
// This method must take care not to resolve any objects. Don't
Expand All @@ -962,22 +960,23 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
// of reading the object and changing the file pointer.

empty = false;
if (in_dictionary && in_array)
{
// Although dictionaries and arrays arbitrarily nest, these
// variables indicate what is at the top of the stack right
// now, so they can, by definition, never both be true.
throw std::logic_error(
"INTERNAL ERROR: parseInternal: in_dict && in_array");
}

QPDFObjectHandle object;

qpdf_offset_t offset = input->tell();
std::vector<QPDFObjectHandle> olist;
std::vector<std::vector<QPDFObjectHandle> > olist_stack;
olist_stack.push_back(std::vector<QPDFObjectHandle>());
enum state_e { st_top, st_start, st_stop, st_eof, st_dictionary, st_array };
std::vector<state_e> state_stack;
state_stack.push_back(st_top);
std::vector<qpdf_offset_t> offset_stack;
offset_stack.push_back(input->tell());
bool done = false;
while (! done)
{
std::vector<QPDFObjectHandle>& olist = olist_stack.back();
state_e state = state_stack.back();
qpdf_offset_t offset = offset_stack.back();

object = QPDFObjectHandle();

QPDFTokenizer::Token token =
Expand All @@ -988,8 +987,7 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
case QPDFTokenizer::tt_eof:
if (content_stream)
{
// Return uninitialized object to indicate EOF
return object;
state = st_eof;
}
else
{
Expand All @@ -1012,9 +1010,9 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
break;

case QPDFTokenizer::tt_array_close:
if (in_array)
if (state == st_array)
{
done = true;
state = st_stop;
}
else
{
Expand All @@ -1029,9 +1027,9 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
break;

case QPDFTokenizer::tt_dict_close:
if (in_dictionary)
if (state == st_dictionary)
{
done = true;
state = st_stop;
}
else
{
Expand All @@ -1046,15 +1044,13 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
break;

case QPDFTokenizer::tt_array_open:
object = parseInternal(
input, object_description, tokenizer, empty,
decrypter, context, true, false, content_stream);
break;

case QPDFTokenizer::tt_dict_open:
object = parseInternal(
input, object_description, tokenizer, empty,
decrypter, context, false, true, content_stream);
olist_stack.push_back(std::vector<QPDFObjectHandle>());
state = st_start;
offset_stack.push_back(input->tell());
state_stack.push_back(
(token.getType() == QPDFTokenizer::tt_array_open) ?
st_array : st_dictionary);
break;

case QPDFTokenizer::tt_bool:
Expand Down Expand Up @@ -1084,12 +1080,12 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
{
object = QPDFObjectHandle::newOperator(value);
}
else if ((value == "R") && (in_array || in_dictionary) &&
(olist.size() >= 2) &&
(! olist.at(olist.size() - 1).isIndirect()) &&
(olist.at(olist.size() - 1).isInteger()) &&
(! olist.at(olist.size() - 2).isIndirect()) &&
(olist.at(olist.size() - 2).isInteger()))
else if ((value == "R") && (state != st_top) &&
(olist.size() >= 2) &&
(! olist.at(olist.size() - 1).isIndirect()) &&
(olist.at(olist.size() - 1).isInteger()) &&
(! olist.at(olist.size() - 2).isIndirect()) &&
(olist.at(olist.size() - 2).isInteger()))
{
if (context == 0)
{
Expand All @@ -1106,8 +1102,7 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
olist.pop_back();
olist.pop_back();
}
else if ((value == "endobj") &&
(! (in_array || in_dictionary)))
else if ((value == "endobj") && (state == st_top))
{
// We just saw endobj without having read
// anything. Treat this as a null and do not move
Expand Down Expand Up @@ -1153,93 +1148,132 @@ QPDFObjectHandle::parseInternal(PointerHolder<InputSource> input,
break;
}

if (in_dictionary || in_array)
{
if (! done)
{
olist.push_back(object);
}
}
else if (! object.isInitialized())
{
warn(context,
QPDFExc(qpdf_e_damaged_pdf, input->getName(),
object_description,
input->getLastOffset(),
"parse error while reading object"));
if ((! object.isInitialized()) &&
(! ((state == st_start) ||
(state == st_stop) ||
(state == st_eof))))
{
throw std::logic_error(
"QPDFObjectHandle::parseInternal: "
"unexpected uninitialized object");
object = newNull();
}
else
{
done = true;
}
}
}

if (in_array)
{
object = newArray(olist);
}
else if (in_dictionary)
{
// Convert list to map. Alternating elements are keys. Attempt
// to recover more or less gracefully from invalid
// dictionaries.
std::set<std::string> names;
for (std::vector<QPDFObjectHandle>::iterator iter = olist.begin();
iter != olist.end(); ++iter)
switch (state)
{
if ((! (*iter).isIndirect()) && (*iter).isName())
case st_eof:
if (state_stack.size() > 1)
{
names.insert((*iter).getName());
warn(context,
QPDFExc(qpdf_e_damaged_pdf, input->getName(),
object_description,
input->getLastOffset(),
"parse error while reading object"));
}
}
done = true;
// Leave object uninitialized to indicate EOF
break;

std::map<std::string, QPDFObjectHandle> dict;
int next_fake_key = 1;
for (unsigned int i = 0; i < olist.size(); ++i)
{
QPDFObjectHandle key_obj = olist.at(i);
QPDFObjectHandle val;
if (key_obj.isIndirect() || (! key_obj.isName()))
case st_dictionary:
case st_array:
olist.push_back(object);
break;

case st_top:
done = true;
break;

case st_start:
break;

case st_stop:
if ((state_stack.size() < 2) || (olist_stack.size() < 2))
{
throw std::logic_error(
"QPDFObjectHandle::parseInternal: st_stop encountered"
" with insufficient elements in stack");
}
state_e old_state = state_stack.back();
state_stack.pop_back();
if (old_state == st_array)
{
bool found_fake = false;
std::string candidate;
while (! found_fake)
object = newArray(olist);
}
else if (old_state == st_dictionary)
{
// Convert list to map. Alternating elements are keys.
// Attempt to recover more or less gracefully from
// invalid dictionaries.
std::set<std::string> names;
for (std::vector<QPDFObjectHandle>::iterator iter =
olist.begin();
iter != olist.end(); ++iter)
{
if ((! (*iter).isIndirect()) && (*iter).isName())
{
names.insert((*iter).getName());
}
}

std::map<std::string, QPDFObjectHandle> dict;
int next_fake_key = 1;
for (unsigned int i = 0; i < olist.size(); ++i)
{
candidate =
"/QPDFFake" + QUtil::int_to_string(next_fake_key++);
found_fake = (names.count(candidate) == 0);
QTC::TC("qpdf", "QPDFObjectHandle found fake",
(found_fake ? 0 : 1));
QPDFObjectHandle key_obj = olist.at(i);
QPDFObjectHandle val;
if (key_obj.isIndirect() || (! key_obj.isName()))
{
bool found_fake = false;
std::string candidate;
while (! found_fake)
{
candidate =
"/QPDFFake" +
QUtil::int_to_string(next_fake_key++);
found_fake = (names.count(candidate) == 0);
QTC::TC("qpdf", "QPDFObjectHandle found fake",
(found_fake ? 0 : 1));
}
warn(context,
QPDFExc(
qpdf_e_damaged_pdf,
input->getName(), object_description, offset,
"expected dictionary key but found"
" non-name object; inserting key " +
candidate));
val = key_obj;
key_obj = newName(candidate);
}
else if (i + 1 >= olist.size())
{
QTC::TC("qpdf", "QPDFObjectHandle no val for last key");
warn(context,
QPDFExc(
qpdf_e_damaged_pdf,
input->getName(), object_description, offset,
"dictionary ended prematurely; "
"using null as value for last key"));
val = newNull();
}
else
{
val = olist.at(++i);
}
dict[key_obj.getName()] = val;
}
warn(context,
QPDFExc(
qpdf_e_damaged_pdf,
input->getName(), object_description, offset,
"expected dictionary key but found"
" non-name object; inserting key " +
candidate));
val = key_obj;
key_obj = newName(candidate);
object = newDictionary(dict);
}
else if (i + 1 >= olist.size())
olist_stack.pop_back();
offset_stack.pop_back();
if (state_stack.back() == st_top)
{
QTC::TC("qpdf", "QPDFObjectHandle no val for last key");
warn(context,
QPDFExc(
qpdf_e_damaged_pdf,
input->getName(), object_description, offset,
"dictionary ended prematurely; using null as value"
" for last key"));
val = newNull();
done = true;
}
else
{
val = olist.at(++i);
olist_stack.back().push_back(object);
}
dict[key_obj.getName()] = val;
}
object = newDictionary(dict);
}

return object;
Expand Down
1 change: 1 addition & 0 deletions qpdf/qtest/qpdf.test
Expand Up @@ -221,6 +221,7 @@ my @bug_tests = (
["141a", "/W entry size 0", 2],
["141b", "/W entry size 0", 2],
["143", "self-referential ostream", 3],
["146", "very deeply nested array", 2],
["149", "xref prev pointer loop", 3],
);
$n_tests += scalar(@bug_tests);
Expand Down
5 changes: 5 additions & 0 deletions qpdf/qtest/qpdf/issue-146.out
@@ -0,0 +1,5 @@
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, file position 20728): unknown token while reading object; treating as string
issue-146.pdf (trailer, file position 20732): EOF while reading token
Binary file added qpdf/qtest/qpdf/issue-146.pdf
Binary file not shown.

0 comments on commit ad527a6

Please sign in to comment.