fix: enforce a shared PNG decompression budget - #6
Merged
Merged
Conversation
Text metadata was budgeted at 8 MiB per file and ICC profiles at 16 MiB, but nothing counted the two together. A 23 KiB PNG carrying a 15 MiB profile and 7.5 MiB of compressed text therefore unpacked to about 22.5 MiB, was accepted by inspection, and cleaned and verified without complaint. Reproduced against 71f320b before touching anything. One DecompressionBudget per file now carries both: 16 MiB in total, the 8 MiB text budget and the 1 MiB single-chunk ceiling unchanged inside it, the profile ceiling unchanged at 16 MiB but no longer additive. Bytes are charged as the reader hands them over, so a stream that dies late has still spent what it delivered, and the reader is cancelled when the budget refuses. Text-only bombs keep the sentence they had; a combined overflow says metadata, because saying text would be untrue. The chunk that breaks a per-item ceiling is deliberately not charged, which is only sound while every caller turns an overflow into a refusal. That invariant is now written down where it has to hold. Original fix by Antigravity. Independently reviewed here: the budget itself stands, the profile budget became a required argument rather than an optional one, and the tests were rewritten - nine of the ten proposed cases passed against the unfixed code, and the reported attack was not among them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 6, 2026
This was referenced Sep 6, 2026
FHoffarth
added a commit
that referenced
this pull request
Sep 6, 2026
…NG one up to date Head e5d4f49, both gates green on that head. Documentation only, one file. Carries the v0.1 half of the decision in #10: the PDF resource limit is written down rather than implied away, scoped to what was measured, and the PNG paragraph is brought up to the shared budget that landed in #6. Hardening remains deferred to #12.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Provenance
Antigravity performed the resource-budget audit, found the gap and wrote the original
fix. Claude reviewed it independently against the code and the diff, and rewrote the
test suite. This split is part of the evidence, not a courtesy: the review was only worth
something because it did not start from the assumption that the fix was right — and it
was not right about everything.
The defect, reproduced before touching anything
Text metadata was budgeted at 8 MiB per file. ICC profiles had their own 16 MiB ceiling.
Nothing counted the two together. Measured against
71f320bwith the fix reverted:A 23 KiB file unpacked to about 22.5 MiB, passed inspection, and was cleaned and verified
without complaint.
profileBytes()had no budget parameter at all.Same inputs with the fix in place:
Both orderings behave identically. The budget is one counter, not a sequence of them.
The fix
One
DecompressionBudgetper file carries both paths: 16 MiB in total, with the 8 MiBtext budget and the 1 MiB single-chunk ceiling unchanged inside it, and the 16 MiB profile
ceiling unchanged but no longer additive. Bytes are charged as the reader hands them over,
so a stream that dies late has still spent what it delivered; the reader is cancelled when
the budget refuses. Text-only bombs keep the sentence they had — a combined overflow says
metadata, because saying text would be untrue.
The chunk that breaks a per-item ceiling is deliberately not charged. That is only sound
while every caller turns an overflow into a refusal, and the invariant is now written down
where it has to hold.
What the review changed
Nine of the ten proposed test cases passed against the unfixed code. Only one detected
the defect, and the reported 15 MiB + 7.5 MiB attack was not among them. After the rewrite,
five of fourteen fail without the fix.
profileBytes(rest, budget?)→ requiredrunBytes, asserting no verdict and no downloadcleanAndVerifyimport removedThe removed case claimed
cleanFileandrunBytesenforce the shared budget. It used a16 MiB + 100 KiB profile, which trips the pre-existing per-profile ceiling, so it was
green without the fix;
runByteswas never called; and it built an artificial reportobject, the anti-pattern already removed from
resource.test.tsonce.One claim the review disproved
clean()receives a budget, but that budget cannot change any outcome there. Thecleaner never inflates text — it drops those chunks whole — so only the profile is charged,
and
MAX_PROFILE_BYTESequals the total budget. A test asserting thatcleanFilerejectsthe 22.5 MiB file failed: the cleaner accepts it. Harmless, because
clean()is onlyreached after inspection has already refused, but it is future-proofing rather than a second
enforced gate, and the tests now say so instead of claiming protection that is not there.
Verified independently
jpeg.tsorexif.tsEvidence
Windows,
core.autocrlf=true:tsc --noEmitvite buildnpm audit --audit-level=highgit diff --checkgit diff audit/git status --porcelainafter a full audit run on the committed treenpm ciagain hit a Windows EPERM lock on@rolldown/binding-win32-x64-msvcin the workingcheckout. No unattributable processes were killed; the lockfile was verified by running
npm ciagainst a copy in a scratch directory — 125 packages, 0 vulnerabilities. CI is theauthoritative clean-runner gate.
Scope
No JPEG, PDF, verification semantics, dependencies or build-system changes. The only new
product copy is the metadata overflow message, which is required because the text message
would be a false statement in that case.
Follow-ups, deliberately not in this PR
reader.cancel()can in principle reject and thereby mask the original budget error,which would downgrade a refusal to "unreadable" and let the file through. Pre-existing
pattern, unlikely, tracked separately.
pdf-libandpdfjs-dist, outside this PNG budget.Real, but its own scope.
No tag, release or deploy proposed.
🤖 Generated with Claude Code