Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions fuzz/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ private static void Seed()
File.WriteAllBytes(Path.Combine(image, "minimal.png"), MinimalPng());
File.WriteAllBytes(Path.Combine(image, "gif-header"), "GIF89a"u8.ToArray());
File.WriteAllBytes(Path.Combine(image, "bmp-header"), "BM"u8.ToArray());
File.WriteAllBytes(Path.Combine(image, "png-preset-dictionary"), PresetDictionaryPng());
File.WriteAllBytes(Path.Combine(image, "empty"), []);

var font = Path.Combine(fixtures, "Fonts", "n8PDFProbe.ttf");
Expand Down Expand Up @@ -252,6 +253,14 @@ private static string RepositoryRoot()
"could not find the repository root (a directory holding tests/n8PDF.Tests/Fixtures)");
}

/// <summary>
/// libFuzzer's minimised unit for #296, kept as a seed so a fresh corpus carries the
/// regression: a PNG whose IDAT sets the zlib header's FDICT bit, which zlib answers by
/// asking for a preset dictionary and .NET reports as a ZLibException — once an escaper.
/// </summary>
private static byte[] PresetDictionaryPng() => Convert.FromBase64String(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAQAAAgAAAAAAAAADElEQVR4P5xjsmAcAAAARAAB//8dCFMA");

private static byte[] MinimalPng()
{
var png = new List<byte> { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
Expand Down
10 changes: 8 additions & 2 deletions src/n8PDF/Images/ImageReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,21 @@ public static ImageData Read(
}
catch (Exception e) when (e is ImageFormatException
or IndexOutOfRangeException or ArgumentException or OverflowException
or DivideByZeroException or InvalidDataException)
or DivideByZeroException or InvalidDataException or IOException)
{
// A malformed image should cost its own placement, not the whole conversion — and
// that sentence has to hold for the files the decoders did not think to refuse, not
// only the ones they did (#48). The types here are exactly what the audit reproduced
// escaping from crafted files of a few dozen bytes; each hole is filed as its own
// issue with its own validation fix, tested at the decoder level where this net
// cannot swallow the evidence, and this is the defence in depth behind those fixes
// rather than a substitute for any of them. OutOfMemoryException is deliberately not
// rather than a substitute for any of them. IOException joined the list when the fuzz
// job found ZLibException escaping a PNG whose zlib header asks for a preset
// dictionary (#296): it derives from IOException rather than InvalidDataException, so
// it sat outside every clause here, and it cannot be named — it is public in
// System.IO.Compression but absent from the net10.0 reference assembly. The width
// costs nothing, because every decoder behind this reads a byte array already in
// memory and none of them opens a file. OutOfMemoryException is deliberately not
// here: that one means the process is in trouble, and hiding it helps nobody.
_ = e;
return null;
Expand Down
10 changes: 9 additions & 1 deletion src/n8PDF/Images/PngDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,17 @@ private static byte[] Inflate(byte[] compressed, long maxBytes)
output.Write(buffer, 0, read);
}
}
catch (InvalidDataException)
catch (Exception e) when (e is InvalidDataException or IOException)
{
// A corrupt zlib stream is a picture this cannot read, not a fault in the reader (#7).
// IOException is the other half of that sentence: a header whose FDICT bit is set asks
// for a preset dictionary, which leaves zlib wanting one rather than calling the
// stream corrupt, and .NET raises ZLibException for it — outside
// InvalidDataException's family, and so outside the catch this once was (#296).
// ZLibException cannot be named: it is public in System.IO.Compression but absent
// from the net10.0 reference assembly, so its public base is what a catch can say.
// Nothing is lost by the width, because the only stream here is a MemoryStream over a
// byte array in hand — it performs no I/O, so an IOException can only be the inflater.
throw new ImageFormatException("PNG image data is not a valid zlib stream.");
}

Expand Down
5 changes: 4 additions & 1 deletion src/n8PDF/Images/TiffDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,12 @@ private static byte[] Inflate(byte[] data, int offset, int length, int expected)
output.Write(buffer, 0, read);
}
}
catch (InvalidDataException)
catch (Exception e) when (e is InvalidDataException or IOException)
{
// A corrupt Deflate strip is a picture this cannot read, not a fault in the reader (#35).
// A strip whose zlib header sets FDICT asks for a preset dictionary instead of being
// corrupt, and .NET raises ZLibException for that — the same hole as in PngDecoder,
// caught the same way and for the same reasons, which are written out there (#296).
throw new ImageFormatException("TIFF strip is not a valid Deflate stream.");
}

Expand Down
21 changes: 21 additions & 0 deletions tests/n8PDF.Tests/ImageDecoderHostileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,27 @@ public void Tiff_deflate_strip_is_bounded_against_its_rows() // #30
Timed("TIFF deflate bomb", () => ImageReader.TryRead(tiff));
}

[Fact]
public void Tiff_deflate_strip_asking_for_a_preset_dictionary_fails_cleanly() // #296
{
// The same hole as the PNG one: a strip whose zlib header sets FDICT leaves zlib wanting
// a dictionary, which .NET reports as ZLibException rather than a data error, and which
// #35's catch therefore let past. Asserted at the decoder, where the net cannot hide it.
byte[] strip = [0x78, 0x3f, 0x00, 0x00, 0x00, 0x01, 0x63, 0x00, 0x00, 0x00, 0x00];
var b = new TiffBuilder().Tags(8);
var stripAt = b.Blob(strip);
var tiff = b
.Tag(256, 3, 1, 8).Tag(257, 3, 1, 8) // 8x8
.Tag(258, 3, 1, 8).Tag(277, 3, 1, 1)
.Tag(259, 3, 1, 8) // Deflate
.Tag(273, 4, 1, stripAt).Tag(279, 4, 1, strip.Length)
.Build();
_output.WriteLine($"{tiff.Length}-byte TIFF whose Deflate strip asks for a preset dictionary");

Assert.IsType<ImageFormatException>(Record.Exception(() => TiffDecoder.Decode(tiff)));
Assert.Null(ImageReader.TryRead(tiff));
}

[Fact]
public void Ccitt_zero_run_strip_does_not_spin() // #46
{
Expand Down
42 changes: 42 additions & 0 deletions tests/n8PDF.Tests/PngHostileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ private static byte[] ZlibOfZeros(int count)
return raw.ToArray();
}

/// <summary>A zlib stream whose header asks for a preset dictionary it cannot be given.</summary>
private static byte[] PresetDictionaryZlib()
{
// CMF 0x78 is deflate over a 32K window; FLG 0x3f sets FDICT (bit 5) and carries an
// FCHECK that makes the pair a multiple of 31, so the header passes its own check and
// zlib gets as far as asking. What follows stands in for the DICTID and the data.
return [0x78, 0x3f, 0x00, 0x00, 0x00, 0x01, 0x63, 0x00, 0x00, 0x00, 0x00];
}

[Fact]
public void A_decompression_bomb_is_refused_against_the_declared_size()
{
Expand Down Expand Up @@ -121,6 +130,39 @@ public void A_corrupt_idat_fails_cleanly() // #7
Assert.Null(ImageReader.TryRead(png.ToArray()));
}

[Fact]
public void An_idat_asking_for_a_preset_dictionary_fails_cleanly() // #296
{
// FDICT is the one malformation zlib answers with Z_NEED_DICT rather than a data error,
// and .NET raises ZLibException for it — an IOException, outside the family #7's catch
// named. Two bytes of a PNG therefore used to take the whole conversion down.
var png = new List<byte> { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
var ihdr = new List<byte>();
Be32(ihdr, 2); Be32(ihdr, 2);
ihdr.Add(8); ihdr.Add(6); ihdr.Add(0); ihdr.Add(0); ihdr.Add(0);
Chunk(png, "IHDR", ihdr.ToArray());
Chunk(png, "IDAT", PresetDictionaryZlib());
Chunk(png, "IEND", []);

var ex = Record.Exception(() => PngDecoder.Decode(png.ToArray()));
Assert.IsType<ImageFormatException>(ex);
Assert.Null(ImageReader.TryRead(png.ToArray()));
}

[Fact]
public void The_minimised_fuzz_unit_from_the_image_target_is_read_as_null() // #296
{
// libFuzzer's own reduction of the crash, kept verbatim: the input the scheduled job
// wrote out. The corpus itself is not in git — it lives in the Actions cache and is
// rebuilt by `dotnet run -- seed` — so fuzz/Program.cs seeds this same unit, and this
// holds the assertion where the suite can see it.
var unit = Convert.FromBase64String(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAQAAAgAAAAAAAAADElEQVR4P5xjsmAcAAAARAAB//8dCFMA");
_output.WriteLine($"the minimised unit is {unit.Length} bytes");

Assert.Null(ImageReader.TryRead(unit));
}

[Fact]
public void A_chunk_length_near_int_max_does_not_pass_the_bounds_check() // #3
{
Expand Down
Loading