pdf: forms read again are bounded per document

A Form XObject that drew the next one many times, level under level,
was read (draws ^ depth) times: a few kilobytes asked for an unbounded
reading, and the page text grew with every leaf (D2 of the document
layer security review). The forms' text cannot be read faithfully; it is
exponential in the file.

The first reading of each form in a document stays whole. Reading one
again (the same content drawn elsewhere: a logo, a header on every page)
is counted in decoded bytes; beyond 256 MiB of such re-readings the
document reads no form again, and each skip is counted
(form_rereads_cut), as a decoded stream above 512 MiB already is. Real
documents re-read small forms: the 749-PDF dev corpus extracts
byte-identically as before (720 of 720 documents, 1,492 of 1,492 pages;
flattened copies too). Reading each form only once per page was tried
first and changed one real page, so it was not taken.

Test: pdf_extract_form_ladder_bounded: seven levels of forms drawing the
next four times each, 64 KiB per form: re-readings are cut and the page
keeps its glyphs bounded (with the bound reverted nothing is cut).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-09 02:55:23 +02:00
1 parent 2af58b0a33
commit 24593cdb9d
4 files changed
+89

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ typedef struct {
int stream_length_recovered; /* a stream's /Length was wrong */
int objstm_nested; /* opening an object stream needed an object of one */
int xref_streams; /* cross-reference streams decoded */
int form_rereads_cut; /* a form not read again: re-readings past 256 MiB */
int flate_recovered; /* a damaged Flate stream decoded in part */
int stream_guard_hits; /* a decoded stream cut at 512 MiB */
int filters_unsupported; /* a stream with a filter this reader does not decode */
+7
View File
@@ -71,6 +71,13 @@ enum {
* no text layer, and real documents stay far below it. */
#define PDF_MAX_STREAM_OUT ((size_t)512 << 20)
/* A Form XObject's first reading in a document is always whole. Reading one
* again (the same content drawn elsewhere: a logo, a header) is counted, and
* beyond this many decoded bytes of such re-readings the document reads no
* form again (counted): forms drawing the next one many times, level under
* level, asked for (draws ^ depth) readings from a few kilobytes. */
#define PDF_MAX_FORM_REREAD ((size_t)256 << 20)
/* ── Integers from the file: saturated, never undefined ──────────── */
static inline int64_t pdf_sat_add(int64_t a, int64_t b) {
+14
View File
@@ -359,6 +359,8 @@ typedef struct {
pdf_out_t out; /* the page being read */
pdf_formkey_t forms[PDF_MAX_FORM_DEPTH + 2];
int nforms;
pdf_map_t forms_read; /* the forms the document has read once */
size_t reread; /* decoded bytes of forms read again */
pdf_glyph_out_t *gbuf;
size_t gcap;
} pdf_interp_t;
@@ -484,6 +486,17 @@ static bool do_xobject(pdf_interp_t *it, pdf_val_t *res, const pdf_val_t *name,
if (!data || !dlen) {
return true;
}
uint64_t once = key.key << 1 | (key.is_ref ? 1U : 0U);
if (pdf_map_get(&it->forms_read, once) >= 0) {
if (dlen > PDF_MAX_FORM_REREAD - it->reread) {
d->counts->form_rereads_cut++;
return true;
}
it->reread += dlen;
} else if (!pdf_map_put(&it->forms_read, once, 1)) {
d->nomem = true;
return false;
}
pdf_val_t *mat = pdf_dget(d, xo->stream->dict, "Matrix");
double m[6];
memcpy(m, IDENT, sizeof(m));
@@ -1025,6 +1038,7 @@ bool cbm_pdf_extract(const unsigned char *data, size_t len, CBMArena *out, cbm_p
res->npages = i + 1;
}
cbm_free(CBM_MEM_CLASS_EXTRACT, it.gbuf);
pdf_map_free(&it.forms_read);
cbm_free(CBM_MEM_CLASS_EXTRACT, pl.v);
bool nomem = d.nomem;
pdf_doc_close(&d);
+67
View File
@@ -707,6 +707,72 @@ static unsigned char *tp_xrefstm_sections(int sections, size_t *out_len) {
return b.p;
}
/* A page drawing form 1, form k drawing form k+1 `fan` times, `depth` forms,
* each padded with a 64 KiB comment; the last one shows "x". */
static unsigned char *tp_form_ladder(int depth, int fan, size_t *out_len) {
int n = 5 + depth;
tp_obj_t *o = (tp_obj_t *)calloc((size_t)n, sizeof(tp_obj_t));
char **dicts = (char **)calloc((size_t)n, sizeof(char *));
tp_buf_t *bodies = (tp_buf_t *)calloc((size_t)n, sizeof(tp_buf_t));
o[0] = (tp_obj_t){TP_CATALOG, NULL, 0};
o[1] = (tp_obj_t){"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", NULL, 0};
o[2] = (tp_obj_t){"<< /Type /Page /Parent 2 0 R /Resources << /XObject << /F 6 0 R >> >> "
"/Contents 5 0 R >>",
NULL, 0};
o[3] = (tp_obj_t){TP_HELV, NULL, 0};
o[4] = (tp_obj_t){"", "q /F Do Q", 9};
char *pad = (char *)malloc(64 * 1024 + 3);
pad[0] = '%';
memset(pad + 1, 'x', 64 * 1024);
pad[64 * 1024 + 1] = '\n';
pad[64 * 1024 + 2] = 0;
for (int k = 0; k < depth; k++) {
int i = 5 + k; /* object i + 1 */
dicts[i] = (char *)malloc(256);
snprintf(dicts[i], 256,
"/Type /XObject /Subtype /Form /BBox [0 0 612 792] /Resources << /XObject << /F "
"%d 0 R >> /Font << /F1 4 0 R >> >>",
i + 2);
tp_puts(&bodies[i], pad);
if (k + 1 < depth) {
for (int f = 0; f < fan; f++) {
tp_puts(&bodies[i], "q /F Do Q\n");
}
} else {
tp_puts(&bodies[i], "BT /F1 12 Tf 72 700 Td (x) Tj ET");
}
o[i] = (tp_obj_t){dicts[i], bodies[i].p, bodies[i].n};
}
unsigned char *pdf = tp_pdf(o, n, NULL, out_len);
for (int i = 0; i < n; i++) {
free(dicts[i]);
free(bodies[i].p);
}
free(pad);
free(bodies);
free(dicts);
free(o);
return pdf;
}
/* Forms drawing the next one four times, seven levels: the first reading of
* each form is whole, readings again stop at PDF_MAX_FORM_REREAD (about 1.3
* GB of form content read again before, and draws ^ depth in general). */
TEST(pdf_extract_form_ladder_bounded) {
size_t len;
tp_text_t t;
unsigned char *pdf = tp_form_ladder(7, 4, &len);
ASSERT_TRUE(tp_extract(pdf, len, &t));
ASSERT_EQ(t.r.status, CBM_PDF_OK);
ASSERT_EQ(t.r.npages, 1);
ASSERT_TRUE(t.r.counts.form_rereads_cut > 0);
ASSERT_TRUE(t.r.pages[0].glyphs >= 1);
ASSERT_TRUE((size_t)t.r.pages[0].glyphs <= (256U << 20) / (64U * 1024U) + 1);
cbm_arena_destroy(&t.arena);
free(pdf);
PASS();
}
/* tp_simple's page, then `n` objects that each open a string and never close
* it, and no cross-reference: the reader rebuilds it from the object headers. */
static unsigned char *tp_open_strings(int n, size_t *out_len) {
@@ -1117,6 +1183,7 @@ SUITE(doc_links_pdf) {
RUN_TEST(pdf_extract_object_chains);
RUN_TEST(pdf_extract_xrefstm_once);
RUN_TEST(pdf_extract_open_strings_linear);
RUN_TEST(pdf_extract_form_ladder_bounded);
RUN_TEST(pdf_scan_mentions);
RUN_TEST(pdf_links_pipeline);
RUN_TEST(pdf_snippet_is_page_text);