Skip to content

GIF decoder throws IndexOutOfRangeException or ArgumentOutOfRangeException for LZW codes past the code tables and frames that extend past the logical screen #3236

Open

Description

@jafin

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 DEBUG and RELEASE mode
  • 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.ReadFrameColors clamps the target row to image.Height. That row is out of range, so DangerousGetRowSpan throws ArgumentOutOfRangeException. 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 ArgumentOutOfRangeException when 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: Identify returns a 4x4 ImageInfo, and Load stops decoding the frame at the invalid code instead of throwing.
  • Cases 2a and 2b: Load returns 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

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions