Skip to content

Cap ObjStm header prealloc to bound /N memory amplification#13

Merged
pgundlach merged 1 commit into
mainfrom
claude/objstm-prealloc-dos
Jun 23, 2026
Merged

Cap ObjStm header prealloc to bound /N memory amplification#13
pgundlach merged 1 commit into
mainfrom
claude/objstm-prealloc-dos

Conversation

@fank

@fank fank commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Bug

readCompressedObject preallocated its (objNum, offset) slice with the ObjStm's /N count:

pairs := make([]pair, 0, n)

/N is attacker-controlled and bounded only by the decoded stream length. With the default 16 MiB stream cap, a few-KB PDF — an ObjStm that FlateDecode-inflates to 16 MiB of zeros and declares /N = 16777216 — forces a ~256 MiB allocation (16M × sizeof(pair)) before a single header entry is parsed. The header parse then fails, but the memory is already committed. Memory-amplification DoS, directly in the untrusted-input threat model.

Fix

Cap the prealloc to a small constant; append grows pairs to the real entry count (which the header-parse loop already bounds by bytes actually present).

Tests (at the Open()/Resolve() surface)

  • TestObjStmHugeNDoesNotAmplifyAllocation — red before the fix (304 MiB measured), green after (bounded under a 128 MiB ceiling via runtime.MemStats TotalAlloc delta).
  • TestObjStmManyObjectsResolvePastPrealloc — a legitimate ObjStm with 5000 entries (> the cap) still resolves an object past the cap to its exact value, proving append growth doesn't truncate.

Also adds xref-recovery coverage found while auditing this path:

  • TestRecoverXrefViaCatalogScan — no startxref / no trailer keyword; recovery scans rebuilt objects for /Type /Catalog.
  • TestPrevChainNewestSectionWins/Prev incremental-update chain; newest section wins and the older trailer's keys survive the merge.

Coverage (main package): 73.7% → 76.1%; recoverXref 75% → 91%.

readCompressedObject sized its (objNum, offset) slice from the ObjStm's
/N entry, which is attacker-controlled and bounded only by the decoded
stream length. A few-KB PDF whose ObjStm inflates to 16 MiB of zeros and
declares /N = 16M forced a ~256 MiB allocation before a single header
entry was parsed. Cap the prealloc; append grows it to the real count.

Adds regression tests at the Open()/Resolve() surface: an allocation
bound for the hostile case and a >cap ObjStm that still resolves
correctly, plus xref-recovery coverage (catalog-scan recovery and a
/Prev incremental-update merge).
@fank fank force-pushed the claude/objstm-prealloc-dos branch from 26d039d to 5ef8326 Compare June 22, 2026 21:20
@fank fank marked this pull request as ready for review June 22, 2026 22:06
@fank fank requested a review from pgundlach June 22, 2026 22:06
@pgundlach pgundlach merged commit ad483ed into main Jun 23, 2026
1 check passed
@pgundlach pgundlach deleted the claude/objstm-prealloc-dos branch June 23, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants