Skip to content

Turn a corrupt PNG's decode crash into a catchable error - #1788

Closed
afonsojanu wants to merge 1 commit into
foliojs:masterfrom
afonsojanu:fix/corrupt-png-catchable-error
Closed

Turn a corrupt PNG's decode crash into a catchable error#1788
afonsojanu wants to merge 1 commit into
foliojs:masterfrom
afonsojanu:fix/corrupt-png-catchable-error

Conversation

@afonsojanu

Copy link
Copy Markdown

Fixes #1747.

png-js decodes IDAT pixel data through Node's async zlib.inflate and rethrows any inflate error from inside that callback. That happens on a later tick, so it can't be caught by a try/catch around document.image(), and it takes the whole process down instead.

I added an inflateSync pass on the same compressed bytes right before handing them to png-js's decodePixels, at all three places pdfkit calls it (splitAlphaChannel, loadIndexedAlphaChannel, decodeData). That runs synchronously inside embed(), which is itself called synchronously from document.image(), so a bad IDAT stream now throws in the same spot PDFImage.open() already throws for other malformed input, rather than crashing later from somewhere nothing can catch.

The #zlib abstraction only exposed deflateSync before this (used for re-encoding decoded pixels), so I extended both implementations with inflateSync too: Node's zlib.inflateSync for the node build, and fflate's unzlibSync for the browser build, which is the inverse of the zlibSync it already uses for deflateSync.

Added a regression test reproducing the exact corrupted PNG from the issue and confirming document.image() now throws synchronously instead of crashing the process. Confirmed it fails on the unmodified code (surfaces as an uncaught exception, matching the reported bug) and passes with the fix. Also added direct round-trip and corrupt-data tests for the new inflateSync on both zlib implementations.

Full suite (unit + visual, 494 tests) and eslint both pass.

png-js decodes IDAT pixel data through Node's async zlib.inflate and
rethrows any inflate error from inside that callback. That happens on
a later tick, so it can't be caught by a try/catch around
document.image(), and it takes the whole process down (issue foliojs#1747).

Added an inflateSync pass on the same compressed bytes right before
handing them to png-js's decodePixels, in all three places pdfkit
calls it (splitAlphaChannel, loadIndexedAlphaChannel, decodeData).
Since it runs synchronously inside embed(), which is itself called
synchronously from document.image(), a bad IDAT stream now throws in
the same spot PDFImage.open() already throws for other malformed
input, instead of crashing later from a spot nothing can catch.

The #zlib abstraction only had deflateSync before this (used for
re-encoding decoded pixels), so extended both implementations with an
inflateSync as well: Node's zlib.inflateSync for the node build, and
fflate's unzlibSync for the browser build, since that's the inverse of
the zlibSync it already uses for deflateSync.

Added a regression test reproducing the exact corrupted PNG from the
issue and confirming document.image() now throws synchronously.
Confirmed it fails on the unmodified code (as an uncaught exception,
matching the reported crash) and passes with the fix. Also added
direct round-trip and corrupt-data tests for the new inflateSync on
both zlib implementations. Full suite (unit + visual, 494 tests) and
eslint both pass.
@blikblum

blikblum commented Sep 4, 2026

Copy link
Copy Markdown
Member

png.js is being reworked to improve the error handling and allow sync decode foliojs/png.js#100

we will update png.js once a new release lands

@blikblum blikblum closed this Sep 4, 2026
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.

Corrupt PNG throws uncatchable error

2 participants