Repository navigation
PNG: empty IDAT/fdAT chunks break decoding, a 1-2 byte PLTE reads outside the palette, and 16-bit tRNS uses the wrong byte order #3240
Description
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp
- I have verified if the problem exist in both
DEBUGandRELEASEmode - I have searched open and closed issues to ensure it has not already been reported
ImageSharp version
main @ a73c43d (also present in v4.1.3, same code)
Other ImageSharp packages and versions
None
Environment (Operating system, version and so on)
Windows 11 Pro 10.0.22631, x64
.NET Framework version
.NET 10 (net10.0)
Description
There are three separate defects in the PNG decoder and encoder.
1. Zero-length IDAT / fdAT chunks end the compressed stream early
The PNG specification defines the compressed image data as the concatenation of the contents of all consecutive IDAT chunks. A chunk length of zero is valid, and the data may be split at any byte, including inside the 2-byte zlib header.
PngDecoderCore.ReadNextDataChunk() returns the length of the next data chunk, and for an empty chunk that length is 0. ChunkedReadStream treats 0 as "no more data". The result depends on where the empty chunk is:
- An empty chunk between the first byte and the rest of the stream (
[1 byte][empty][rest]) throwsImageFormatException: Bad method for ZLIB header. - Two empty chunks before the data decode without an error, but all pixels are 0.
ReadNextFrameDataChunk() has the same problem with an fdAT chunk that holds only its sequence number.
2. A PLTE chunk shorter than one entry reads outside the color table
A PLTE chunk of 1 or 2 bytes gives an empty color table. AssignColorPalette only returns early for palette.Length == 0. The palette scanline processor clamps each index to palette.Length - 1, which is -1 here, so it reads memory before the table. In my test this threw System.EntryPointNotFoundException for both non-interlaced and Adam7 images. The expected result is the InvalidImageContentException that the decoder already throws for an indexed image with no palette.
3. 16-bit tRNS values are read and written little-endian
The PNG specification stores all multi-byte integers in network byte order (most significant byte first). This includes the 2-byte gray and RGB samples in tRNS. AssignTransparentMarkers reads them with ReadUInt16LittleEndian, and WriteTransparencyChunk writes them with WriteUInt16LittleEndian.
As a result, a file that marks 0x0102 as transparent makes 0x0201 transparent instead. Also, a file that ImageSharp writes with transparent color 0x0102 stores the bytes 02 01, so other readers make the wrong color transparent. The 8-bit path is correct. The existing 16-bit tRNS test images use FFFF, which is the same in both byte orders.
A fix is proposed in #3228.
Steps to Reproduce
The helpers below write chunks with a valid CRC and compress filtered scanlines.
using System.IO.Compression;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
static byte[] Compress(byte[] data)
{
using MemoryStream ms = new();
using (ZLibStream z = new(ms, CompressionLevel.Optimal, true)) { z.Write(data); }
return ms.ToArray();
}
static void WriteChunk(Stream s, string type, byte[] data)
{
byte[] typeAndData = [.. Encoding.ASCII.GetBytes(type), .. data];
uint crc = 0xFFFFFFFF;
foreach (byte b in typeAndData)
{
crc ^= b;
for (int k = 0; k < 8; k++) { crc = (crc >> 1) ^ (0xEDB88320 & (uint)-(int)(crc & 1)); }
}
crc = ~crc;
s.Write([(byte)(data.Length >> 24), (byte)(data.Length >> 16), (byte)(data.Length >> 8), (byte)data.Length]);
s.Write(typeAndData);
s.Write([(byte)(crc >> 24), (byte)(crc >> 16), (byte)(crc >> 8), (byte)crc]);
}
static MemoryStream StartPng(byte[] ihdr)
{
MemoryStream png = new();
png.Write([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
WriteChunk(png, "IHDR", ihdr);
return png;
}1. Empty IDAT chunk
// 2x1 8-bit grayscale. One scanline: filter 0, pixels 10 and 20.
byte[] zlib = Compress([0, 10, 20]);
byte[] ihdr = [0, 0, 0, 2, 0, 0, 0, 1, 8, 0, 0, 0, 0];
// a) Empty chunk inside the zlib header.
using MemoryStream a = StartPng(ihdr);
WriteChunk(a, "IDAT", zlib[..1]);
WriteChunk(a, "IDAT", []);
WriteChunk(a, "IDAT", zlib[1..]);
WriteChunk(a, "IEND", []);
using Image<L8> imageA = Image.Load<L8>(a.ToArray()); // throws
// b) Two empty chunks before the data.
using MemoryStream b = StartPng(ihdr);
WriteChunk(b, "IDAT", []);
WriteChunk(b, "IDAT", []);
WriteChunk(b, "IDAT", zlib);
WriteChunk(b, "IEND", []);
using Image<L8> imageB = Image.Load<L8>(b.ToArray());
Console.WriteLine($"{imageB[0, 0].PackedValue} {imageB[1, 0].PackedValue}"); // prints "0 0"Actual result for (a):
SixLabors.ImageSharp.ImageFormatException: Bad method for ZLIB header: cmf=156
at SixLabors.ImageSharp.Compression.Zlib.ZlibInflateReader.InitializeInflateStream(Boolean isCriticalChunk)
at SixLabors.ImageSharp.Compression.Zlib.ZlibInflateReader.AllocateNewBytes(Int32 bytes, Boolean isCriticalChunk)
at SixLabors.ImageSharp.Formats.Png.PngDecoderCore.ReadScanlines[TPixel](...)
at SixLabors.ImageSharp.Formats.Png.PngDecoderCore.Decode[TPixel](BufferedReadStream stream, CancellationToken cancellationToken)
Actual result for (b): 0 0.
Expected result: both images decode to pixels 10 20.
2. PLTE chunk shorter than one entry
// 9x9, 8-bit, indexed (color type 3), not interlaced. Every scanline is filter 0, index 0.
using MemoryStream png = StartPng([0, 0, 0, 9, 0, 0, 0, 9, 8, 3, 0, 0, 0]);
WriteChunk(png, "PLTE", [10, 20]); // 2 bytes, less than one 3-byte entry
WriteChunk(png, "IDAT", Compress(new byte[9 * 10]));
WriteChunk(png, "IEND", []);
using Image<Rgba32> image = Image.Load<Rgba32>(png.ToArray());Actual result (the same with interlace byte 1, Adam7, and 100 bytes of scanline data):
System.EntryPointNotFoundException: Entry point was not found.
at SixLabors.ImageSharp.PixelFormats.IPixel.ToScaledVector4()
at SixLabors.ImageSharp.Formats.Png.PngScanlineProcessor.ProcessInterlacedPaletteScanline[TPixel](...)
at SixLabors.ImageSharp.Formats.Png.PngScanlineProcessor.ProcessPaletteScanline[TPixel](...)
at SixLabors.ImageSharp.Formats.Png.PngDecoderCore.ProcessDefilteredScanline[TPixel](...)
This reads memory outside the color table, so the exact failure can differ.
Expected result: InvalidImageContentException, the same as for an indexed image with no PLTE chunk.
3. 16-bit tRNS byte order
// Decoder: 2x1 16-bit grayscale. tRNS marks 0x0102 transparent. Pixel 0 is 0x0102, pixel 1 is 0x0201.
using MemoryStream png = StartPng([0, 0, 0, 2, 0, 0, 0, 1, 16, 0, 0, 0, 0]);
WriteChunk(png, "tRNS", [0x01, 0x02]);
WriteChunk(png, "IDAT", Compress([0, 0x01, 0x02, 0x02, 0x01]));
WriteChunk(png, "IEND", []);
PngMetadata meta = Image.Identify(png.ToArray()).Metadata.GetPngMetadata();
Console.WriteLine($"0x{meta.TransparentColor?.ToPixel<L16>().PackedValue:X4}"); // 0x0201
using Image<Rgba64> image = Image.Load<Rgba64>(png.ToArray());
Console.WriteLine($"{image[0, 0].A} {image[1, 0].A}"); // 65535 0
// Encoder: write transparent color 0x0102 and look at the tRNS data.
using Image<Rgba64> input = new(2, 1);
PngMetadata outMeta = input.Metadata.GetPngMetadata();
outMeta.ColorType = PngColorType.Grayscale;
outMeta.BitDepth = PngBitDepth.Bit16;
outMeta.TransparentColor = Color.FromPixel(new L16(0x0102));
using MemoryStream saved = new();
input.Save(saved, new PngEncoder { ColorType = PngColorType.Grayscale, BitDepth = PngBitDepth.Bit16 });
byte[] bytes = saved.ToArray();
int i = bytes.AsSpan().IndexOf("tRNS"u8);
Console.WriteLine(Convert.ToHexString(bytes, i + 4, 2)); // 0201Actual result: the transparent color is read as 0x0201. Pixel 0 (0x0102) is opaque and pixel 1 (0x0201) is transparent. The encoder writes the tRNS data as 0201.
Expected result: the transparent color is 0x0102, pixel 0 has alpha 0, pixel 1 has alpha 65535, and the encoder writes 0102. A 16-bit RGB image with tRNS 01 02 03 04 05 06 has the same problem.
Images
No images needed; the repro builds the input in memory.
Activity