Commit 0bfe9024893ebb1f62108fe6c24410e6ba589c3e

Authored by Jay Berkenbilt
1 parent 10bceb55

Security: avoid pre-allocating vectors based on file data

In places where std::vector<T>(size_t) was used, either validate that
the size parameter is sane or refactor code to avoid the need to
pre-allocate the vector.
ChangeLog
1 1 2013-10-05 Jay Berkenbilt <ejb@ql.org>
2 2  
  3 + * Security fix: In places where std::vector<T>(size_t) was used,
  4 + either validate that the size parameter is sane or refactor code
  5 + to avoid the need to pre-allocate the vector. This reduces the
  6 + likelihood of allocating a lot of memory in response to invalid
  7 + data in linearization hint streams.
  8 +
3 9 * Security fix: sanitize /W array in cross reference stream to
4 10 avoid a potential integer overflow in a multiplication. It is
5 11 unlikely that any exploits were possible from this bug as
... ...
libqpdf/QPDF_linearization.cc
... ... @@ -23,13 +23,22 @@ static void
23 23 load_vector_int(BitStream& bit_stream, int nitems, std::vector<T>& vec,
24 24 int bits_wanted, int_type T::*field)
25 25 {
  26 + bool append = vec.empty();
26 27 // nitems times, read bits_wanted from the given bit stream,
27 28 // storing results in the ith vector entry.
28 29  
29 30 for (int i = 0; i < nitems; ++i)
30 31 {
  32 + if (append)
  33 + {
  34 + vec.push_back(T());
  35 + }
31 36 vec[i].*field = bit_stream.getBits(bits_wanted);
32 37 }
  38 + if (static_cast<int>(vec.size()) != nitems)
  39 + {
  40 + throw std::logic_error("vector has wrong size in load_vector_int");
  41 + }
33 42 // The PDF spec says that each hint table starts at a byte
34 43 // boundary. Each "row" actually must start on a byte boundary.
35 44 bit_stream.skipToNextByte();
... ... @@ -255,6 +264,17 @@ QPDF::readLinearizationData()
255 264  
256 265 // Store linearization parameter data
257 266  
  267 + // Various places in the code use linp.npages, which is
  268 + // initialized from N, to pre-allocate memory, so make sure it's
  269 + // accurate and bail right now if it's not.
  270 + if (N.getIntValue() != static_cast<long long>(getAllPages().size()))
  271 + {
  272 + throw QPDFExc(qpdf_e_damaged_pdf, this->file->getName(),
  273 + "linearization hint table",
  274 + this->file->getLastOffset(),
  275 + "/N does not match number of pages");
  276 + }
  277 +
258 278 // file_size initialized by isLinearized()
259 279 this->linp.first_page_object = O.getIntValue();
260 280 this->linp.first_page_end = E.getIntValue();
... ... @@ -396,10 +416,9 @@ QPDF::readHPageOffset(BitStream h)
396 416 t.nbits_shared_numerator = h.getBits(16); // 12
397 417 t.shared_denominator = h.getBits(16); // 13
398 418  
399   - unsigned int nitems = this->linp.npages;
400 419 std::vector<HPageOffsetEntry>& entries = t.entries;
401   - entries = std::vector<HPageOffsetEntry>(nitems);
402   -
  420 + entries.clear();
  421 + unsigned int nitems = this->linp.npages;
403 422 load_vector_int(h, nitems, entries,
404 423 t.nbits_delta_nobjects,
405 424 &HPageOffsetEntry::delta_nobjects);
... ... @@ -441,10 +460,9 @@ QPDF::readHSharedObject(BitStream h)
441 460 QTC::TC("qpdf", "QPDF lin nshared_total > nshared_first_page",
442 461 (t.nshared_total > t.nshared_first_page) ? 1 : 0);
443 462  
444   - int nitems = t.nshared_total;
445 463 std::vector<HSharedObjectEntry>& entries = t.entries;
446   - entries = std::vector<HSharedObjectEntry>(nitems);
447   -
  464 + entries.clear();
  465 + int nitems = t.nshared_total;
448 466 load_vector_int(h, nitems, entries,
449 467 t.nbits_delta_group_length,
450 468 &HSharedObjectEntry::delta_group_length);
... ... @@ -1466,8 +1484,11 @@ QPDF::calculateLinearizationData(std::map&lt;int, int&gt; const&amp; object_stream_data)
1466 1484 // validation code can compute them relatively easily given the
1467 1485 // rest of the information.
1468 1486  
  1487 + // npages is the size of the existing pages vector, which has been
  1488 + // created by traversing the pages tree, and as such is a
  1489 + // reasonable size.
1469 1490 this->c_linp.npages = npages;
1470   - this->c_page_offset_data.entries = std::vector<CHPageOffsetEntry>(npages);
  1491 + this->c_page_offset_data.entries = std::vector<CHPageOffsetEntry>(npages);
1471 1492  
1472 1493 // Part 4: open document objects. We don't care about the order.
1473 1494  
... ... @@ -1861,6 +1882,7 @@ QPDF::calculateHPageOffset(
1861 1882  
1862 1883 HPageOffset& ph = this->page_offset_hints;
1863 1884 std::vector<HPageOffsetEntry>& phe = ph.entries;
  1885 + // npages is the size of the existing pages array.
1864 1886 phe = std::vector<HPageOffsetEntry>(npages);
1865 1887  
1866 1888 for (unsigned int i = 0; i < npages; ++i)
... ... @@ -1935,7 +1957,7 @@ QPDF::calculateHSharedObject(
1935 1957 std::vector<CHSharedObjectEntry>& csoe = cso.entries;
1936 1958 HSharedObject& so = this->shared_object_hints;
1937 1959 std::vector<HSharedObjectEntry>& soe = so.entries;
1938   - soe = std::vector<HSharedObjectEntry>(cso.nshared_total);
  1960 + soe.clear();
1939 1961  
1940 1962 int min_length = outputLengthNextN(
1941 1963 csoe[0].object, 1, lengths, obj_renumber);
... ... @@ -1948,8 +1970,13 @@ QPDF::calculateHSharedObject(
1948 1970 csoe[i].object, 1, lengths, obj_renumber);
1949 1971 min_length = std::min(min_length, length);
1950 1972 max_length = std::max(max_length, length);
  1973 + soe.push_back(HSharedObjectEntry());
1951 1974 soe[i].delta_group_length = length;
1952 1975 }
  1976 + if (soe.size() != static_cast<size_t>(cso.nshared_total))
  1977 + {
  1978 + throw std::logic_error("soe has wrong size after initialization");
  1979 + }
1953 1980  
1954 1981 so.nshared_total = cso.nshared_total;
1955 1982 so.nshared_first_page = cso.nshared_first_page;
... ...
qpdf/qtest/qpdf.test
... ... @@ -199,7 +199,7 @@ $td-&gt;runtest(&quot;remove page we don&#39;t have&quot;,
199 199 show_ntests();
200 200 # ----------
201 201 $td->notify("--- Miscellaneous Tests ---");
202   -$n_tests += 69;
  202 +$n_tests += 70;
203 203  
204 204 $td->runtest("qpdf version",
205 205 {$td->COMMAND => "qpdf --version"},
... ... @@ -537,6 +537,13 @@ $td-&gt;runtest(&quot;bounds check linearization data 2&quot;,
537 537 {$td->FILE => "linearization-bounds-2.out",
538 538 $td->EXIT_STATUS => 2},
539 539 $td->NORMALIZE_NEWLINES);
  540 +# Throws logic error, not bad_alloc
  541 +$td->runtest("sanity check array size",
  542 + {$td->COMMAND =>
  543 + "qpdf --check linearization-large-vector-alloc.pdf"},
  544 + {$td->FILE => "linearization-large-vector-alloc.out",
  545 + $td->EXIT_STATUS => 2},
  546 + $td->NORMALIZE_NEWLINES);
540 547  
541 548 show_ntests();
542 549 # ----------
... ...
qpdf/qtest/qpdf/linearization-large-vector-alloc.out 0 → 100644
  1 +checking linearization-large-vector-alloc.pdf
  2 +PDF Version: 1.3
  3 +File is not encrypted
  4 +File is linearized
  5 +WARNING: linearization-large-vector-alloc.pdf (linearization hint stream: object 62 0, file position 1183): attempting to recover stream length
  6 +overflow reading bit stream
... ...
qpdf/qtest/qpdf/linearization-large-vector-alloc.pdf 0 → 100644
No preview for this file type