Skip to content

ZLibException escapes the image decoders on a preset-dictionary (FDICT) zlib stream #296

Description

@nathanpond

What

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.

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 — 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):

Base64: iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAQAAAgAAAAAAAAADElEQVR4P5xjsmAcAAAARAAB//8dCFMA
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:

78 9c (no FDICT, corrupt body)  -> InvalidDataException
78 3f (FDICT set)               -> ZLibException          <-- escapes
78 01 (no FDICT)                -> InvalidDataException
08 1d (window 256, FDICT clear) -> InvalidDataException
00 00 (bad CM)                  -> InvalidDataException

Suggested fix

  1. Catch ZLibException alongside InvalidDataException in PngDecoder.Inflate and
    TiffDecoder.Inflate, translating it to the ImageFormatException each already raises. A
    preset-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.
  2. Add ZLibException to the TryRead net as defence in depth behind those, not instead of them.
  3. Tests at the decoder — a PNG and a TIFF whose stream sets FDICT — so the assertion lands
    where 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.
  4. Commit the minimised unit to fuzz/corpus/image/ so replay keeps it.

Acceptance criteria

  • PngDecoder.Decode throws ImageFormatException for an FDICT IDAT.
  • TiffDecoder.Decode throws ImageFormatException for an FDICT Deflate strip.
  • ImageReader.TryRead returns null for the minimised unit above.
  • The unit is in the image corpus and FUZZ_TARGET=image ... replay passes.
  • Build warning-clean; suite green with qpdf, fontTools and FriBidi on.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditFiled by an audit runsecurityReachable from untrusted inputsev:mediumWrong output where a clean failure was owed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions