You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ZLibException escapes the image decoders on a preset-dictionary (FDICT) zlib stream #296
A zlib stream whose header sets the FDICT bit (a preset-dictionary stream) makes System.IO.Compression throw ZLibException, which neither PngDecoder.Inflate nor TiffDecoder.Inflate catches, and which the ImageReader.TryRead net (#48) does not list. It
escapes Converter and takes the whole conversion down.
Found by the scheduled fuzz job: run 33303663302, job Fuzz image,
exit 77.
src/n8PDF/Images/TiffDecoder.cs:593-607 — the same catch, for Deflate strips (compression 8 / 32946).
src/n8PDF/Images/ImageReader.cs:58-60 — the TryRead net does not list ZLibException either.
Pdf/PdfFilters.FlateDecode shares the shape but only ever reads bytes we wrote ourselves, so it
is not on the attack surface.
Why it matters
A hostile .docx gets a hard failure of the entire conversion out of two bytes in an image
part. The contract the decoders and TryRead exist to keep is that a malformed picture costs its
own placement and nothing more; here it costs the document. No allocation and no memory unsafety —
hence sev:medium rather than higher — but it is a denial of the conversion from an input that is
trivially cheap to author, and it is exactly the class of hole #48 was opened to close.
The reason it slipped the net is that ZLibException derives from IOException, not from InvalidDataException, so it is outside every catch clause in the image path.
Reproduction
libFuzzer's minimised unit (a 1x4 8-bit greyscale-alpha PNG whose IDAT begins 78 3f):
ImageReader.TryRead(Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAQAAAgAAAAAAAAADElEQVR4P5xjsmAcAAAARAAB//8dCFMA"));// System.IO.Compression.ZLibException: The underlying compression routine returned an unexpected error code.// at System.IO.Compression.Inflater.Inflate(FlushCode flushCode)// at n8PDF.Images.PngDecoder.Inflate(Byte[] compressed, Int64 maxBytes)// at n8PDF.Images.PngDecoder.Decode(Byte[] data, Int64 maximumPixels)// at n8PDF.Images.ImageReader.Read(Byte[] data, Int64 maximumPixels, Int32 nesting)// at n8PDF.Images.ImageReader.TryRead(Byte[] data, Int64 maximumPixels, Int32 nesting)
Reproduced locally on macOS, .NET 10, Release.
FDICT is the whole of the trigger. Feeding ZLibStream a corrupt body under a range of headers,
only the one with bit 5 of FLG set leaves the handled family — zlib returns Z_NEED_DICT and
.NET maps that to ZLibException while mapping everything else to InvalidDataException:
What
A zlib stream whose header sets the
FDICTbit (a preset-dictionary stream) makesSystem.IO.CompressionthrowZLibException, which neitherPngDecoder.InflatenorTiffDecoder.Inflatecatches, and which theImageReader.TryReadnet (#48) does not list. Itescapes
Converterand takes the whole conversion down.Found by the scheduled fuzz job: run
33303663302, job
Fuzz image,exit 77.
Where
src/n8PDF/Images/PngDecoder.cs:127-145—catch (InvalidDataException)only.src/n8PDF/Images/TiffDecoder.cs:593-607— the same catch, for Deflate strips (compression 8 / 32946).src/n8PDF/Images/ImageReader.cs:58-60— theTryReadnet does not listZLibExceptioneither.Pdf/PdfFilters.FlateDecodeshares the shape but only ever reads bytes we wrote ourselves, so itis not on the attack surface.
Why it matters
A hostile
.docxgets a hard failure of the entire conversion out of two bytes in an imagepart. The contract the decoders and
TryReadexist to keep is that a malformed picture costs itsown placement and nothing more; here it costs the document. No allocation and no memory unsafety —
hence sev:medium rather than higher — but it is a denial of the conversion from an input that is
trivially cheap to author, and it is exactly the class of hole #48 was opened to close.
The reason it slipped the net is that
ZLibExceptionderives fromIOException, not fromInvalidDataException, so it is outside every catch clause in the image path.Reproduction
libFuzzer's minimised unit (a 1x4 8-bit greyscale-alpha PNG whose IDAT begins
78 3f):Reproduced locally on macOS, .NET 10, Release.
FDICTis the whole of the trigger. FeedingZLibStreama corrupt body under a range of headers,only the one with bit 5 of
FLGset leaves the handled family — zlib returnsZ_NEED_DICTand.NET maps that to
ZLibExceptionwhile mapping everything else toInvalidDataException:Suggested fix
ZLibExceptionalongsideInvalidDataExceptioninPngDecoder.InflateandTiffDecoder.Inflate, translating it to theImageFormatExceptioneach already raises. Apreset-dictionary stream is a picture we cannot read, not a fault in the reader — the same
sentence Images/PngDecoder: InvalidDataException from ZLibStream on a corrupt IDAT escapes uncaught and aborts the conversion #7 and Images/TiffDecoder: malformed Deflate strip data throws InvalidDataException straight out of the decoder #35 already settled for a corrupt one.
ZLibExceptionto theTryReadnet as defence in depth behind those, not instead of them.FDICT— so the assertion landswhere the net cannot swallow it, per Images/PngDecoder: IDAT is inflated into an unbounded MemoryStream with no cap against the header's expected size #2/Images/PngDecoder: InvalidDataException from ZLibStream on a corrupt IDAT escapes uncaught and aborts the conversion #7/Images/TiffDecoder: malformed Deflate strip data throws InvalidDataException straight out of the decoder #35.
fuzz/corpus/image/soreplaykeeps it.Acceptance criteria
PngDecoder.DecodethrowsImageFormatExceptionfor anFDICTIDAT.TiffDecoder.DecodethrowsImageFormatExceptionfor anFDICTDeflate strip.ImageReader.TryReadreturns null for the minimised unit above.FUZZ_TARGET=image ... replaypasses.