Repository navigation
GIF decoder throws IndexOutOfRangeException or ArgumentOutOfRangeException for LZW codes past the code tables and frames that extend past the logical screen #3236
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)
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
Some corrupt or non-conforming GIFs make the decoder throw IndexOutOfRangeException or ArgumentOutOfRangeException. Expected: the decoder decodes what it can, or throws InvalidImageContentException.
1. LZW code past the code tables
With an LZW minimum code size of 12, the clear code is 4096. The first code after a clear code (or at the start of the data) can then be the next available code, 4098. That code is past the end of the 4096-entry prefix/suffix tables. LzwDecoder.DecodePixelRow (used by Image.Load) and LzwDecoder.SkipIndices (used by Image.Identify) index the tables with it and throw IndexOutOfRangeException.
2. Frames that extend past the logical screen
- For an interlaced frame whose lines fall below the logical screen,
GifDecoderCore.ReadFrameColorsclamps the target row toimage.Height. That row is out of range, soDangerousGetRowSpanthrowsArgumentOutOfRangeException. Examples are a 20x20 interlaced frame on a 10x10 screen, or a 10x10 interlaced frame at top 5. The row loop is also limited by the screen height. For an interlaced frame, that limit cuts off lines of later passes that are still on the screen. - An interlaced frame shorter than 5 lines has interlace passes with no lines. The decoder enters such a pass and writes the frame's lines to the wrong rows. For example, a 2-line frame writes its second line to row 4 instead of row 1.
- A frame whose left edge is at or past the right edge of the screen throws
ArgumentOutOfRangeExceptionwhen the row is sliced.
Expected: lines and columns outside the logical screen are dropped, and the lines inside the screen are written to the correct rows (GIF89a section 20 and Appendix E describe the interlace passes).
A fix is proposed in #3224.
Steps to Reproduce
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
// GIF89a with a 4-entry global color table (red, green, blue, white) and one image descriptor.
// imageData runs from the LZW minimum code size byte to the block terminator.
static byte[] CreateGif(int screenWidth, int screenHeight, int left, int top, int width, int height, bool interlaced, byte[] imageData)
{
using MemoryStream s = new();
s.Write("GIF89a"u8);
s.Write([(byte)screenWidth, (byte)(screenWidth >> 8), (byte)screenHeight, (byte)(screenHeight >> 8), 0x81, 0x00, 0x00]);
s.Write([255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]);
s.Write([0x2C, (byte)left, (byte)(left >> 8), (byte)top, (byte)(top >> 8),
(byte)width, (byte)(width >> 8), (byte)height, (byte)(height >> 8), (byte)(interlaced ? 0x40 : 0x00)]);
s.Write(imageData);
s.WriteByte(0x3B);
return s.ToArray();
}
// 1. Minimum code size 12, one sub-block holding the single 13-bit code 4098.
byte[] lzwPastTables = CreateGif(4, 4, 0, 0, 4, 4, false, [12, 0x02, 0x02, 0x10, 0x00]);
Image.Identify(lzwPastTables); // IndexOutOfRangeException
using (Image<Rgba32> image = Image.Load<Rgba32>(lzwPastTables)) { } // IndexOutOfRangeException
// Minimum code size 2: a clear code followed by the end of information code (no pixel data).
byte[] noPixels = [0x02, 0x01, 0x2C, 0x00];
// 2a. 20x20 interlaced frame on a 10x10 screen, and 10x10 interlaced frame at top 5.
using (Image<Rgba32> image = Image.Load<Rgba32>(CreateGif(10, 10, 0, 0, 20, 20, true, noPixels))) { } // ArgumentOutOfRangeException
using (Image<Rgba32> image = Image.Load<Rgba32>(CreateGif(10, 10, 0, 5, 10, 10, true, noPixels))) { } // ArgumentOutOfRangeException
// 2b. 4x4 frame at left 12 on a 10x10 screen.
using (Image<Rgba32> image = Image.Load<Rgba32>(CreateGif(10, 10, 12, 0, 4, 4, false, noPixels))) { } // ArgumentOutOfRangeException
// 2c. 1x2 interlaced frame on a 1x10 screen. Frame row 0 is index 1 (green), frame row 1 is index 2 (blue).
// LZW codes (3 bits): clear, 1, 2, end of information.
using (Image<Rgba32> image = Image.Load<Rgba32>(CreateGif(1, 10, 0, 0, 1, 2, true, [0x02, 0x02, 0x8C, 0x0A, 0x00])))
{
for (int y = 0; y < image.Height; y++)
{
Console.WriteLine($"row {y}: {image[0, y]}");
}
}Actual result:
1. Load:
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at SixLabors.ImageSharp.Formats.Gif.LzwDecoder.DecodePixelRow(Span`1 indices)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrameColors[TPixel](...)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrame[TPixel](...)
1. Identify:
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at SixLabors.ImageSharp.Formats.Gif.LzwDecoder.SkipIndices(Int32 length)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrameMetadata(...)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.Identify(...)
2a (both cases):
System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'DangerousGetRowSpan(10). Y was out of range. Height=10')
at SixLabors.ImageSharp.Memory.Buffer2D`1.ThrowYOutOfRangeException(Int32 y)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrameColors[TPixel](...)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrame[TPixel](...)
2b:
System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrameColors[TPixel](...)
at SixLabors.ImageSharp.Formats.Gif.GifDecoderCore.ReadFrame[TPixel](...)
2c:
row 0: Rgba32(0, 255, 0, 255)
row 1: Rgba32(0, 0, 0, 0)
row 2: Rgba32(0, 0, 0, 0)
row 3: Rgba32(0, 0, 0, 0)
row 4: Rgba32(0, 0, 255, 255)
...
Expected result:
- Case 1:
Identifyreturns a 4x4ImageInfo, andLoadstops decoding the frame at the invalid code instead of throwing. - Cases 2a and 2b:
Loadreturns a 10x10 image. Frame lines and columns that fall outside the logical screen are dropped, and an interlaced frame keeps the lines of later passes that are on the screen. - Case 2c: row 1 is blue (
Rgba32(0, 0, 255, 255)) and row 4 is untouched.
Images
No images needed; the repro builds the input in memory.
Activity