SixLabors ImageSharp Allows Excessive Memory Allocation in Gif Decoder
Description
ImageSharp is a 2D graphics API. A vulnerability discovered in the ImageSharp library, where the processing of specially crafted files can lead to excessive memory usage in the Gif decoder. The vulnerability is triggered when ImageSharp attempts to process image files that are designed to exploit this flaw. All users are advised to upgrade to v3.1.5 or v2.1.9.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Specially crafted GIF files can cause excessive memory usage in ImageSharp's Gif decoder via a flaw in image descriptor processing.
Vulnerability
Description CVE-2024-41132 is a memory consumption issue in the ImageSharp library's GIF decoder. The root cause lies in how the ReadFrame method handles image descriptors: prior to the fix, the code allocated a Buffer2D for pixel indices unconditionally, even when a local color table is absent [1]. This memory allocation, combined with crafted file attributes, can lead to excessive memory usage.
Attack
Vector An attacker can trigger this vulnerability by supplying a specially crafted GIF file that exploits the decoder's allocation behavior during image descriptor processing. No authentication is required; the vulnerability is triggered simply when ImageSharp processes the malformed image [2][3]. The attack surface is file upload or image processing pipelines that use the vulnerable library versions.
Impact
Successful exploitation results in uncontrolled memory consumption, potentially leading to denial of service by exhausting system memory. The CVSS score is not publicly available, but the official advisory recommends upgrading to patched versions [4].
Mitigation
Users are advised to update to ImageSharp v3.1.5 or v2.1.9, which contain the fix that defers buffer allocation until necessary [2][3]. No workaround is provided; upgrading is the recommended action.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
SixLabors.ImageSharpNuGet | < 2.1.9 | 2.1.9 |
SixLabors.ImageSharpNuGet | >= 3.0.0, < 3.1.5 | 3.1.5 |
Affected products
2- SixLabors/ImageSharpv5Range: < 2.1.9
Patches
265bd9b5ccdecMerge pull request #2775 from SixLabors/js-fix-2771
4 files changed · +13 −8
src/ImageSharp/Formats/Png/Filters/AverageFilter.cs+2 −2 modified@@ -169,7 +169,7 @@ public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previo Vector256<int> sumAccumulator = Vector256<int>.Zero; Vector256<byte> allBitsSet = Avx2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); - for (nuint xLeft = x - bytesPerPixel; x <= (uint)(scanline.Length - Vector256<byte>.Count); xLeft += (uint)Vector256<byte>.Count) + for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector256<byte>.Count; xLeft += (uint)Vector256<byte>.Count) { Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x)); Vector256<byte> left = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft)); @@ -192,7 +192,7 @@ public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previo Vector128<int> sumAccumulator = Vector128<int>.Zero; Vector128<byte> allBitsSet = Sse2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); - for (nuint xLeft = x - bytesPerPixel; x <= (uint)(scanline.Length - Vector128<byte>.Count); xLeft += (uint)Vector128<byte>.Count) + for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector128<byte>.Count; xLeft += (uint)Vector128<byte>.Count) { Vector128<byte> scan = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, x)); Vector128<byte> left = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
src/ImageSharp/Formats/Png/Filters/SubFilter.cs+4 −3 modified@@ -136,7 +136,7 @@ public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> result Vector256<byte> zero = Vector256<byte>.Zero; Vector256<int> sumAccumulator = Vector256<int>.Zero; - for (nuint xLeft = x - (uint)bytesPerPixel; x <= (uint)(scanline.Length - Vector256<byte>.Count); xLeft += (uint)Vector256<byte>.Count) + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector256<byte>.Count); xLeft += (uint)Vector256<byte>.Count) { Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x)); Vector256<byte> prev = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft)); @@ -150,11 +150,12 @@ public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> result sum += Numerics.EvenReduceSum(sumAccumulator); } - else if (Vector.IsHardwareAccelerated) + else + if (Vector.IsHardwareAccelerated) { Vector<uint> sumAccumulator = Vector<uint>.Zero; - for (nuint xLeft = x - (uint)bytesPerPixel; x <= (uint)(scanline.Length - Vector<byte>.Count); xLeft += (uint)Vector<byte>.Count) + for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector<byte>.Count); xLeft += (uint)Vector<byte>.Count) { Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x)); Vector<byte> prev = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, xLeft));
src/ImageSharp/Formats/Png/Filters/UpFilter.cs+2 −2 modified@@ -179,7 +179,7 @@ public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previo Vector256<byte> zero = Vector256<byte>.Zero; Vector256<int> sumAccumulator = Vector256<int>.Zero; - for (; x <= (uint)(scanline.Length - Vector256<byte>.Count);) + for (; (int)x <= scanline.Length - Vector256<byte>.Count;) { Vector256<byte> scan = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref scanBaseRef, x)); Vector256<byte> above = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, x)); @@ -197,7 +197,7 @@ public static void Encode(ReadOnlySpan<byte> scanline, ReadOnlySpan<byte> previo { Vector<uint> sumAccumulator = Vector<uint>.Zero; - for (; x <= (uint)(scanline.Length - Vector<byte>.Count);) + for (; (int)x <= scanline.Length - Vector<byte>.Count;) { Vector<byte> scan = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref scanBaseRef, x)); Vector<byte> above = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref prevBaseRef, x));
src/ImageSharp/Formats/Png/PngEncoderCore.cs+5 −1 modified@@ -1477,7 +1477,11 @@ private void SanitizeAndSetEncoderOptions<TPixel>( // Use options, then check metadata, if nothing set there then we suggest // a sensible default based upon the pixel format. this.colorType = encoder.ColorType ?? pngMetadata.ColorType ?? SuggestColorType<TPixel>(); - if (!encoder.FilterMethod.HasValue) + if (encoder.FilterMethod.HasValue) + { + this.filterMethod = encoder.FilterMethod.Value; + } + else { // Specification recommends default filter method None for paletted images and Paeth for others. this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth;
9816ca45016cMerge pull request #2770 from SixLabors/af/backport-2759-2.1.x
15 files changed · +564 −278
src/ImageSharp/Formats/Gif/GifDecoderCore.cs+284 −181 modified@@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,19 +23,24 @@ namespace SixLabors.ImageSharp.Formats.Gif internal sealed class GifDecoderCore : IImageDecoderInternals { /// <summary> - /// The temp buffer used to reduce allocations. + /// The temp buffer. /// </summary> - private readonly byte[] buffer = new byte[16]; + private byte[] buffer = new byte[16]; /// <summary> - /// The currently loaded stream. + /// The global color table. /// </summary> - private BufferedReadStream stream; + private IMemoryOwner<byte> globalColorTable; /// <summary> - /// The global color table. + /// The current local color table. /// </summary> - private IMemoryOwner<byte> globalColorTable; + private IMemoryOwner<byte> currentLocalColorTable; + + /// <summary> + /// Gets the size in bytes of the current local color table. + /// </summary> + private int currentLocalColorTableSize; /// <summary> /// The area to restore. @@ -56,6 +62,26 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// </summary> private GifImageDescriptor imageDescriptor; + /// <summary> + /// The global configuration. + /// </summary> + private readonly Configuration configuration; + + /// <summary> + /// Used for allocating memory during processing operations. + /// </summary> + private readonly MemoryAllocator memoryAllocator; + + /// <summary> + /// The maximum number of frames to decode. Inclusive. + /// </summary> + private readonly uint maxFrames; + + /// <summary> + /// Whether to skip metadata during decode. + /// </summary> + private readonly bool skipMetadata; + /// <summary> /// The abstract metadata. /// </summary> @@ -73,23 +99,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// <param name="options">The decoder options.</param> public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) { - this.IgnoreMetadata = options.IgnoreMetadata; - this.DecodingMode = options.DecodingMode; - this.Configuration = configuration ?? Configuration.Default; + this.skipMetadata = options.IgnoreMetadata; + this.configuration = configuration ?? Configuration.Default; + this.maxFrames = options.DecodingMode == FrameDecodingMode.All ? options.MaxFrames : 1; + this.memoryAllocator = this.configuration.MemoryAllocator; } /// <inheritdoc /> - public Configuration Configuration { get; } - - /// <summary> - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. - /// </summary> - public bool IgnoreMetadata { get; internal set; } - - /// <summary> - /// Gets the decoding mode for multi-frame images. - /// </summary> - public FrameDecodingMode DecodingMode { get; } + public Configuration Configuration => this.configuration; /// <summary> /// Gets the dimensions of the image. @@ -102,6 +119,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel<TPixel> { + uint frameCount = 0; Image<TPixel> image = null; ImageFrame<TPixel> previousFrame = null; try @@ -114,28 +132,32 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken { if (nextFlag == GifConstants.ImageLabel) { - if (previousFrame != null && this.DecodingMode == FrameDecodingMode.First) + if (previousFrame != null && ++frameCount == this.maxFrames) { break; } - this.ReadFrame(ref image, ref previousFrame); + this.ReadFrame(stream, ref image, ref previousFrame); + + // Reset per-frame state. + this.imageDescriptor = default; + this.graphicsControlExtension = default; } else if (nextFlag == GifConstants.ExtensionIntroducer) { switch (stream.ReadByte()) { case GifConstants.GraphicControlLabel: - this.ReadGraphicalControlExtension(); + this.ReadGraphicalControlExtension(stream); break; case GifConstants.CommentLabel: - this.ReadComments(); + this.ReadComments(stream); break; case GifConstants.ApplicationExtensionLabel: - this.ReadApplicationExtension(); + this.ReadApplicationExtension(stream); break; case GifConstants.PlainTextLabel: - this.SkipBlock(); // Not supported by any known decoder. + SkipBlock(stream); // Not supported by any known decoder. break; } } @@ -154,6 +176,12 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); + } + + if (image is null) + { + GifThrowHelper.ThrowInvalidImageContentException("No data"); } return image; @@ -162,6 +190,9 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken /// <inheritdoc /> public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + uint frameCount = 0; + ImageFrameMetadata? previousFrame = null; + List<ImageFrameMetadata> framesMetadata = new(); try { this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); @@ -172,23 +203,32 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella { if (nextFlag == GifConstants.ImageLabel) { - this.ReadImageDescriptor(); + if (previousFrame != null && ++frameCount == this.maxFrames) + { + break; + } + + this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame); + + // Reset per-frame state. + this.imageDescriptor = default; + this.graphicsControlExtension = default; } else if (nextFlag == GifConstants.ExtensionIntroducer) { switch (stream.ReadByte()) { case GifConstants.GraphicControlLabel: - this.SkipBlock(); // Skip graphic control extension block + this.ReadGraphicalControlExtension(stream); break; case GifConstants.CommentLabel: - this.ReadComments(); + this.ReadComments(stream); break; case GifConstants.ApplicationExtensionLabel: - this.ReadApplicationExtension(); + this.ReadApplicationExtension(stream); break; case GifConstants.PlainTextLabel: - this.SkipBlock(); // Not supported by any known decoder. + SkipBlock(stream); // Not supported by any known decoder. break; } } @@ -207,6 +247,12 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); + } + + if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) + { + GifThrowHelper.ThrowNoHeader(); } return new ImageInfo( @@ -219,9 +265,10 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella /// <summary> /// Reads the graphic control extension. /// </summary> - private void ReadGraphicalControlExtension() + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + private void ReadGraphicalControlExtension(BufferedReadStream stream) { - int bytesRead = this.stream.Read(this.buffer, 0, 6); + int bytesRead = stream.Read(this.buffer, 0, 6); if (bytesRead != 6) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); @@ -233,9 +280,10 @@ private void ReadGraphicalControlExtension() /// <summary> /// Reads the image descriptor. /// </summary> - private void ReadImageDescriptor() + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + private void ReadImageDescriptor(BufferedReadStream stream) { - int bytesRead = this.stream.Read(this.buffer, 0, 9); + int bytesRead = stream.Read(this.buffer, 0, 9); if (bytesRead != 9) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); @@ -251,9 +299,10 @@ private void ReadImageDescriptor() /// <summary> /// Reads the logical screen descriptor. /// </summary> - private void ReadLogicalScreenDescriptor() + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + private void ReadLogicalScreenDescriptor(BufferedReadStream stream) { - int bytesRead = this.stream.Read(this.buffer, 0, 7); + int bytesRead = stream.Read(this.buffer, 0, 7); if (bytesRead != 7) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); @@ -266,197 +315,188 @@ private void ReadLogicalScreenDescriptor() /// Reads the application extension block parsing any animation or XMP information /// if present. /// </summary> - private void ReadApplicationExtension() + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + private void ReadApplicationExtension(BufferedReadStream stream) { - int appLength = this.stream.ReadByte(); + int appLength = stream.ReadByte(); // If the length is 11 then it's a valid extension and most likely // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. + long position = stream.Position; if (appLength == GifConstants.ApplicationBlockSize) { - this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); + stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); - - if (isXmp && !this.IgnoreMetadata) + if (isXmp && !this.skipMetadata) { - var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator); + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); if (extension.Data.Length > 0) { - this.metadata.XmpProfile = new XmpProfile(extension.Data); + this.metadata!.XmpProfile = new XmpProfile(extension.Data); + } + else + { + // Reset the stream position and continue. + stream.Position = position; + SkipBlock(stream, appLength); } return; } - else - { - int subBlockSize = this.stream.ReadByte(); - // TODO: There's also a NETSCAPE buffer extension. - // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension - if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) - { - this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; - this.stream.Skip(1); // Skip the terminator. - return; - } + int subBlockSize = stream.ReadByte(); - // Could be something else not supported yet. - // Skip the subblock and terminator. - this.SkipBlock(subBlockSize); + // TODO: There's also a NETSCAPE buffer extension. + // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension + if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + { + stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; + stream.Skip(1); // Skip the terminator. + return; } + // Could be something else not supported yet. + // Skip the subblock and terminator. + SkipBlock(stream, subBlockSize); + return; } - this.SkipBlock(appLength); // Not supported by any known decoder. + SkipBlock(stream, appLength); // Not supported by any known decoder. } /// <summary> /// Skips over a block or reads its terminator. - /// <param name="blockSize">The length of the block to skip.</param> /// </summary> - private void SkipBlock(int blockSize = 0) + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + /// <param name="blockSize">The length of the block to skip.</param> + private static void SkipBlock(BufferedReadStream stream, int blockSize = 0) { if (blockSize > 0) { - this.stream.Skip(blockSize); + stream.Skip(blockSize); } int flag; - while ((flag = this.stream.ReadByte()) > 0) + while ((flag = stream.ReadByte()) > 0) { - this.stream.Skip(flag); + stream.Skip(flag); } } /// <summary> /// Reads the gif comments. /// </summary> - private void ReadComments() + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + private void ReadComments(BufferedReadStream stream) { int length; - var stringBuilder = new StringBuilder(); - while ((length = this.stream.ReadByte()) != 0) + StringBuilder stringBuilder = new(); + while ((length = stream.ReadByte()) != 0) { if (length > GifConstants.MaxCommentSubBlockLength) { GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); } - if (this.IgnoreMetadata) + if (this.skipMetadata) { - this.stream.Seek(length, SeekOrigin.Current); + stream.Seek(length, SeekOrigin.Current); continue; } - using IMemoryOwner<byte> commentsBuffer = this.MemoryAllocator.Allocate<byte>(length); + using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length); Span<byte> commentsSpan = commentsBuffer.GetSpan(); - this.stream.Read(commentsSpan); + stream.Read(commentsSpan); string commentPart = GifConstants.Encoding.GetString(commentsSpan); stringBuilder.Append(commentPart); } if (stringBuilder.Length > 0) { - this.gifMetadata.Comments.Add(stringBuilder.ToString()); + this.gifMetadata!.Comments.Add(stringBuilder.ToString()); } } /// <summary> /// Reads an individual gif frame. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> /// <param name="image">The image to decode the information to.</param> /// <param name="previousFrame">The previous frame.</param> - private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame) + private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame) where TPixel : unmanaged, IPixel<TPixel> { - this.ReadImageDescriptor(); + this.ReadImageDescriptor(stream); - IMemoryOwner<byte> localColorTable = null; - Buffer2D<byte> indices = null; - try - { - // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. - if (this.imageDescriptor.LocalColorTableFlag) - { - int length = this.imageDescriptor.LocalColorTableSize * 3; - localColorTable = this.Configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean); - this.stream.Read(localColorTable.GetSpan()); - } + // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. + bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag; - indices = this.Configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); - this.ReadFrameIndices(indices); - - Span<byte> rawColorTable = default; - if (localColorTable != null) - { - rawColorTable = localColorTable.GetSpan(); - } - else if (this.globalColorTable != null) - { - rawColorTable = this.globalColorTable.GetSpan(); - } - - ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable); - this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor); + if (hasLocalColorTable) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length)); + } - // Skip any remaining blocks - this.SkipBlock(); + Span<byte> rawColorTable = default; + if (hasLocalColorTable) + { + rawColorTable = this.currentLocalColorTable!.GetSpan().Slice(0, this.currentLocalColorTableSize); } - finally + else if (this.globalColorTable != null) { - localColorTable?.Dispose(); - indices?.Dispose(); + rawColorTable = this.globalColorTable.GetSpan(); } - } - /// <summary> - /// Reads the frame indices marking the color to use for each pixel. - /// </summary> - /// <param name="indices">The 2D pixel buffer to write to.</param> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReadFrameIndices(Buffer2D<byte> indices) - { - int minCodeSize = this.stream.ReadByte(); - using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream); - lzwDecoder.DecodePixels(minCodeSize, indices); + ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable); + this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor); + + // Skip any remaining blocks + SkipBlock(stream); } /// <summary> /// Reads the frames colors, mapping indices to colors. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> /// <param name="image">The image to decode the information to.</param> /// <param name="previousFrame">The previous frame.</param> - /// <param name="indices">The indexed pixels.</param> /// <param name="colorTable">The color table containing the available colors.</param> /// <param name="descriptor">The <see cref="GifImageDescriptor"/></param> - private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame, Buffer2D<byte> indices, ReadOnlySpan<Rgb24> colorTable, in GifImageDescriptor descriptor) + private void ReadFrameColors<TPixel>( + BufferedReadStream stream, + ref Image<TPixel>? image, + ref ImageFrame<TPixel>? previousFrame, + ReadOnlySpan<Rgb24> colorTable, + in GifImageDescriptor descriptor) where TPixel : unmanaged, IPixel<TPixel> { int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; bool transFlag = this.graphicsControlExtension.TransparencyFlag; - ImageFrame<TPixel> prevFrame = null; - ImageFrame<TPixel> currentFrame = null; + ImageFrame<TPixel>? prevFrame = null; + ImageFrame<TPixel>? currentFrame = null; ImageFrame<TPixel> imageFrame; if (previousFrame is null) { if (!transFlag) { - image = new Image<TPixel>(this.Configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata); + image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata); } else { // This initializes the image to become fully transparent because the alpha channel is zero. - image = new Image<TPixel>(this.Configuration, imageWidth, imageHeight, this.metadata); + image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata); } this.SetFrameMetadata(image.Frames.RootFrame.Metadata); @@ -470,7 +510,10 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi prevFrame = previousFrame; } - currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection + // We create a clone of the frame and add it. + // We will overpaint the difference of pixels on the current frame to create a complete image. + // This ensures that we have enough pixel data to process without distortion. #2450 + currentFrame = image!.Frames.AddFrame(previousFrame); this.SetFrameMetadata(currentFrame.Metadata); @@ -494,64 +537,79 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi byte transIndex = this.graphicsControlExtension.TransparencyIndex; int colorTableMaxIdx = colorTable.Length - 1; - for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++) + // For a properly encoded gif the descriptor dimensions will never exceed the logical screen dimensions. + // However we have images that exceed this that can be decoded by other libraries. #1530 + using IMemoryOwner<byte> indicesRowOwner = this.memoryAllocator.Allocate<byte>(descriptor.Width); + Span<byte> indicesRow = indicesRowOwner.Memory.Span; + ref byte indicesRowRef = ref MemoryMarshal.GetReference(indicesRow); + + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) { - ref byte indicesRowRef = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y - descriptorTop)); + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); - // Check if this image is interlaced. - int writeY; // the target y offset to write to - if (descriptor.InterlaceFlag) + for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++) { - // If so then we read lines at predetermined offsets. - // When an entire image height worth of offset lines has been read we consider this a pass. - // With each pass the number of offset lines changes and the starting line changes. - if (interlaceY >= descriptor.Height) + // Check if this image is interlaced. + int writeY; // the target y offset to write to + if (descriptor.InterlaceFlag) { - interlacePass++; - switch (interlacePass) + // If so then we read lines at predetermined offsets. + // When an entire image height worth of offset lines has been read we consider this a pass. + // With each pass the number of offset lines changes and the starting line changes. + if (interlaceY >= descriptor.Height) { - case 1: - interlaceY = 4; - break; - case 2: - interlaceY = 2; - interlaceIncrement = 4; - break; - case 3: - interlaceY = 1; - interlaceIncrement = 2; - break; + interlacePass++; + switch (interlacePass) + { + case 1: + interlaceY = 4; + break; + case 2: + interlaceY = 2; + interlaceIncrement = 4; + break; + case 3: + interlaceY = 1; + interlaceIncrement = 2; + break; + } } - } - writeY = interlaceY + descriptor.Top; - interlaceY += interlaceIncrement; - } - else - { - writeY = y; - } + writeY = Math.Min(interlaceY + descriptor.Top, image.Height); + interlaceY += interlaceIncrement; + } + else + { + writeY = y; + } - ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); + lzwDecoder.DecodePixelRow(indicesRow); + ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); - if (!transFlag) - { - // #403 The left + width value can be larger than the image width - for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + if (!transFlag) { - int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); - ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); - Rgb24 rgb = colorTable[index]; - pixel.FromRgb24(rgb); + // #403 The left + width value can be larger than the image width + for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + { + int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); + ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); + Rgb24 rgb = colorTable[index]; + pixel.FromRgb24(rgb); + } } - } - else - { - for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + else { - int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); - if (transIndex != index) + for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) { + int index = Unsafe.Add(ref indicesRowRef, x - descriptorLeft); + + // Treat any out of bounds values as transparent. + if (index > colorTableMaxIdx || index == transIndex) + { + continue; + } + ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); Rgb24 rgb = colorTable[index]; pixel.FromRgb24(rgb); @@ -574,6 +632,43 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi } } + /// <summary> + /// Reads the frames metadata. + /// </summary> + /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> + /// <param name="frameMetadata">The collection of frame metadata.</param> + /// <param name="previousFrame">The previous frame metadata.</param> + private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame) + { + this.ReadImageDescriptor(stream); + + // Skip the color table for this frame if local. + if (this.imageDescriptor.LocalColorTableFlag) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length)); + } + + // Skip the frame indices. Pixels length + mincode size. + // The gif format does not tell us the length of the compressed data beforehand. + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) + { + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); + lzwDecoder.SkipIndices(this.imageDescriptor.Width * this.imageDescriptor.Height); + } + + ImageFrameMetadata currentFrame = new(); + frameMetadata.Add(currentFrame); + this.SetFrameMetadata(currentFrame); + previousFrame = currentFrame; + + // Skip any remaining blocks + SkipBlock(stream); + } + /// <summary> /// Restores the current frame area to the background. /// </summary> @@ -587,15 +682,15 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame) return; } - var interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value); + Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value); Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest); pixelRegion.Clear(); this.restoreArea = null; } /// <summary> - /// Sets the frames metadata. + /// Sets the metadata for the image frame. /// </summary> /// <param name="meta">The metadata.</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -628,13 +723,11 @@ private void SetFrameMetadata(ImageFrameMetadata meta) /// <param name="stream">The stream containing image data. </param> private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream) { - this.stream = stream; - // Skip the identifier - this.stream.Skip(6); - this.ReadLogicalScreenDescriptor(); + stream.Skip(6); + this.ReadLogicalScreenDescriptor(stream); - var meta = new ImageMetadata(); + ImageMetadata meta = new(); // The Pixel Aspect Ratio is defined to be the quotient of the pixel's // width over its height. The value range in this field allows @@ -671,16 +764,26 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s if (this.logicalScreenDescriptor.GlobalColorTableFlag) { int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; - this.gifMetadata.GlobalColorTableLength = globalColorTableLength; - if (globalColorTableLength > 0) { - this.globalColorTable = this.MemoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean); + this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean); - // Read the global color table data from the stream - stream.Read(this.globalColorTable.GetSpan()); + // Read the global color table data from the stream and preserve it in the gif metadata + Span<byte> globalColorTableSpan = this.globalColorTable.GetSpan(); + stream.Read(globalColorTableSpan); + + //Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize]; + //ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(globalColorTableSpan); + //for (int i = 0; i < colorTable.Length; i++) + //{ + // colorTable[i] = new Color(rgbTable[i]); + //} + + //this.gifMetadata.GlobalColorTable = colorTable; } } + + //this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex; } } }
src/ImageSharp/Formats/Gif/GifDecoder.cs+5 −0 modified@@ -23,6 +23,11 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe /// </summary> public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All; + /// <summary> + /// Gets or sets the maximum number of gif frames. + /// </summary> + public uint MaxFrames { get; set; } = uint.MaxValue; + /// <inheritdoc/> public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel<TPixel>
src/ImageSharp/Formats/Gif/GifThrowHelper.cs+7 −0 modified@@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Gif @@ -24,5 +25,11 @@ public static void ThrowInvalidImageContentException(string errorMessage) /// if no inner exception is specified.</param> [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNoHeader() => throw new InvalidImageContentException("Gif image does not contain a Logical Screen Descriptor."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNoData() => throw new InvalidImageContentException("Unable to read Gif image data"); } }
src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs+5 −0 modified@@ -19,5 +19,10 @@ internal interface IGifDecoderOptions /// Gets the decoding mode for multi-frame images. /// </summary> FrameDecodingMode DecodingMode { get; } + + /// <summary> + /// Gets or sets the maximum number of gif frames. + /// </summary> + uint MaxFrames { get; set; } } }
src/ImageSharp/Formats/Gif/LzwDecoder.cs+207 −92 modified@@ -45,196 +45,310 @@ internal sealed class LzwDecoder : IDisposable /// </summary> private readonly IMemoryOwner<int> suffix; + /// <summary> + /// The scratch buffer for reading data blocks. + /// </summary> + private readonly IMemoryOwner<byte> scratchBuffer; + /// <summary> /// The pixel stack buffer. /// </summary> private readonly IMemoryOwner<int> pixelStack; + private readonly int minCodeSize; + private readonly int clearCode; + private readonly int endCode; + private int code; + private int codeSize; + private int codeMask; + private int availableCode; + private int oldCode = NullCode; + private int bits; + private int top; + private int count; + private int bufferIndex; + private int data; + private int first; /// <summary> /// Initializes a new instance of the <see cref="LzwDecoder"/> class /// and sets the stream, where the compressed data should be read from. /// </summary> /// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param> /// <param name="stream">The stream to read from.</param> + /// <param name="minCodeSize">The minimum code size.</param> /// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception> - public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream) + public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.prefix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean); this.suffix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean); this.pixelStack = memoryAllocator.Allocate<int>(MaxStackSize + 1, AllocationOptions.Clean); + this.scratchBuffer = memoryAllocator.Allocate<byte>(byte.MaxValue, AllocationOptions.None); + this.minCodeSize = minCodeSize; + + // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize + this.clearCode = 1 << minCodeSize; + this.codeSize = minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.endCode = this.clearCode + 1; + this.availableCode = this.clearCode + 2; + + ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); + for (this.code = 0; this.code < this.clearCode; this.code++) + { + Unsafe.Add(ref suffixRef, this.code) = (byte)this.code; + } } /// <summary> - /// Decodes and decompresses all pixel indices from the stream. + /// Gets a value indicating whether the minimum code size is valid. /// </summary> - /// <param name="minCodeSize">Minimum code size of the data.</param> - /// <param name="pixels">The pixel array to decode to.</param> - public void DecodePixels(int minCodeSize, Buffer2D<byte> pixels) + /// <param name="minCodeSize">The minimum code size.</param> + /// <returns> + /// <see langword="true"/> if the minimum code size is valid; otherwise, <see langword="false"/>. + /// </returns> + public static bool IsValidMinCodeSize(int minCodeSize) { - // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize - int clearCode = 1 << minCodeSize; - // It is possible to specify a larger LZW minimum code size than the palette length in bits // which may leave a gap in the codes where no colors are assigned. // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression + int clearCode = 1 << minCodeSize; if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize) { // Don't attempt to decode the frame indices. // Theoretically we could determine a min code size from the length of the provided // color palette but we won't bother since the image is most likely corrupted. - return; + return false; } - // The resulting index table length. - int width = pixels.Width; - int height = pixels.Height; - int length = width * height; + return true; + } - int codeSize = minCodeSize + 1; + /// <summary> + /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer. + /// </summary> + /// <param name="indices">The pixel indices array to decode to.</param> + public void DecodePixelRow(Span<byte> indices) + { + indices.Clear(); - // Calculate the end code - int endCode = clearCode + 1; + ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices); + ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); + ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); + ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); + Span<byte> buffer = this.scratchBuffer.GetSpan(); - // Calculate the available code. - int availableCode = clearCode + 2; + int x = 0; + int xyz = 0; + while (xyz < indices.Length) + { + if (this.top == 0) + { + if (this.bits < this.codeSize) + { + // Load bytes until there are enough bits for a code. + if (this.count == 0) + { + // Read a new data block. + this.count = this.ReadBlock(buffer); + if (this.count == 0) + { + break; + } - // Jillzhangs Code see: http://giflib.codeplex.com/ - // Adapted from John Cristy's ImageMagick. - int code; - int oldCode = NullCode; - int codeMask = (1 << codeSize) - 1; - int bits = 0; + this.bufferIndex = 0; + } - int top = 0; - int count = 0; - int bi = 0; - int xyz = 0; + this.data += buffer[this.bufferIndex] << this.bits; - int data = 0; - int first = 0; + this.bits += 8; + this.bufferIndex++; + this.count--; + continue; + } - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); + // Get the next code + this.code = this.data & this.codeMask; + this.data >>= this.codeSize; + this.bits -= this.codeSize; - for (code = 0; code < clearCode; code++) - { - Unsafe.Add(ref suffixRef, code) = (byte)code; + // Interpret the code + if (this.code > this.availableCode || this.code == this.endCode) + { + break; + } + + if (this.code == this.clearCode) + { + // Reset the decoder + this.codeSize = this.minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.availableCode = this.clearCode + 2; + this.oldCode = NullCode; + continue; + } + + if (this.oldCode == NullCode) + { + Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code); + this.oldCode = this.code; + this.first = this.code; + continue; + } + + int inCode = this.code; + if (this.code == this.availableCode) + { + Unsafe.Add(ref pixelStackRef, this.top++) = (byte)this.first; + + this.code = this.oldCode; + } + + while (this.code > this.clearCode) + { + Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code); + this.code = Unsafe.Add(ref prefixRef, this.code); + } + + int suffixCode = Unsafe.Add(ref suffixRef, this.code); + this.first = suffixCode; + Unsafe.Add(ref pixelStackRef, this.top++) = suffixCode; + + // Fix for Gifs that have "deferred clear code" as per here : + // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 + if (this.availableCode < MaxStackSize) + { + Unsafe.Add(ref prefixRef, this.availableCode) = this.oldCode; + Unsafe.Add(ref suffixRef, this.availableCode) = this.first; + this.availableCode++; + if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) + { + this.codeSize++; + this.codeMask = (1 << this.codeSize) - 1; + } + } + + this.oldCode = inCode; + } + + // Pop a pixel off the pixel stack. + this.top--; + + // Clear missing pixels + xyz++; + Unsafe.Add(ref pixelsRowRef, x++) = (byte)Unsafe.Add(ref pixelStackRef, this.top); } + } - Span<byte> buffer = stackalloc byte[byte.MaxValue]; + /// <summary> + /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data. + /// </summary> + /// <param name="length">The resulting index table length.</param> + public void SkipIndices(int length) + { + ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); + ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); + ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); + Span<byte> buffer = this.scratchBuffer.GetSpan(); - int y = 0; - int x = 0; - int rowMax = width; - ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y)); + int xyz = 0; while (xyz < length) { - // Reset row reference. - if (xyz == rowMax) - { - x = 0; - pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y)); - rowMax = (y * width) + width; - } - - if (top == 0) + if (this.top == 0) { - if (bits < codeSize) + if (this.bits < this.codeSize) { // Load bytes until there are enough bits for a code. - if (count == 0) + if (this.count == 0) { // Read a new data block. - count = this.ReadBlock(buffer); - if (count == 0) + this.count = this.ReadBlock(buffer); + if (this.count == 0) { break; } - bi = 0; + this.bufferIndex = 0; } - data += buffer[bi] << bits; + this.data += buffer[this.bufferIndex] << this.bits; - bits += 8; - bi++; - count--; + this.bits += 8; + this.bufferIndex++; + this.count--; continue; } // Get the next code - code = data & codeMask; - data >>= codeSize; - bits -= codeSize; + this.code = this.data & this.codeMask; + this.data >>= this.codeSize; + this.bits -= this.codeSize; // Interpret the code - if (code > availableCode || code == endCode) + if (this.code > this.availableCode || this.code == this.endCode) { break; } - if (code == clearCode) + if (this.code == this.clearCode) { // Reset the decoder - codeSize = minCodeSize + 1; - codeMask = (1 << codeSize) - 1; - availableCode = clearCode + 2; - oldCode = NullCode; + this.codeSize = this.minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.availableCode = this.clearCode + 2; + this.oldCode = NullCode; continue; } - if (oldCode == NullCode) + if (this.oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code); - oldCode = code; - first = code; + Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code); + this.oldCode = this.code; + this.first = this.code; continue; } - int inCode = code; - if (code == availableCode) + int inCode = this.code; + if (this.code == this.availableCode) { - Unsafe.Add(ref pixelStackRef, top++) = (byte)first; + Unsafe.Add(ref pixelStackRef, this.top++) = (byte)this.first; - code = oldCode; + this.code = this.oldCode; } - while (code > clearCode) + while (this.code > this.clearCode) { - Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code); - code = Unsafe.Add(ref prefixRef, code); + Unsafe.Add(ref pixelStackRef, this.top++) = Unsafe.Add(ref suffixRef, this.code); + this.code = Unsafe.Add(ref prefixRef, this.code); } - int suffixCode = Unsafe.Add(ref suffixRef, code); - first = suffixCode; - Unsafe.Add(ref pixelStackRef, top++) = suffixCode; + int suffixCode = Unsafe.Add(ref suffixRef, this.code); + this.first = suffixCode; + Unsafe.Add(ref pixelStackRef, this.top++) = suffixCode; // Fix for Gifs that have "deferred clear code" as per here : // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (availableCode < MaxStackSize) + if (this.availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, availableCode) = oldCode; - Unsafe.Add(ref suffixRef, availableCode) = first; - availableCode++; - if (availableCode == codeMask + 1 && availableCode < MaxStackSize) + Unsafe.Add(ref prefixRef, this.availableCode) = this.oldCode; + Unsafe.Add(ref suffixRef, this.availableCode) = this.first; + this.availableCode++; + if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) { - codeSize++; - codeMask = (1 << codeSize) - 1; + this.codeSize++; + this.codeMask = (1 << this.codeSize) - 1; } } - oldCode = inCode; + this.oldCode = inCode; } // Pop a pixel off the pixel stack. - top--; + this.top--; // Clear missing pixels xyz++; - Unsafe.Add(ref pixelsRowRef, x++) = (byte)Unsafe.Add(ref pixelStackRef, top); } } @@ -267,6 +381,7 @@ public void Dispose() this.prefix.Dispose(); this.suffix.Dispose(); this.pixelStack.Dispose(); + this.scratchBuffer.Dispose(); } } }
src/ImageSharp/Formats/Jpeg/Components/Quantization.cs+1 −1 modified@@ -147,7 +147,7 @@ public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan<byte> target quality = (int)Math.Round(5000.0 / sumPercent); } - return quality; + return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor); } /// <summary>
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png+2 −2 modified@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3a24c066895fd3a76649da376485cbc1912d6a3ae15369575f523e66364b3b6 -size 141563 +oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70 +size 142244
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png+3 −0 added@@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38 +size 81944
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png+3 −0 added@@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38 +size 81944
tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png+2 −2 modified@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:489642f0c81fd12e97007fe6feb11b0e93e351199a922ce038069a3782ad0722 -size 135 +oid sha256:5016a323018f09e292165ad5392d82dcbad5e79c2b6b93aff3322dffff80b309 +size 126
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs+11 −0 modified@@ -183,6 +183,17 @@ public void Issue1530_BadDescriptorDimensions<TPixel>(TestImageProvider<TPixel> image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + // https://github.com/SixLabors/ImageSharp/issues/2758 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2758, PixelTypes.Rgba32)] + public void Issue2758_BadDescriptorDimensions<TPixel>(TestImageProvider<TPixel> provider) + where TPixel : unmanaged, IPixel<TPixel> + { + using Image<TPixel> image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } + // https://github.com/SixLabors/ImageSharp/issues/405 [Theory] [WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)]
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs+26 −0 modified@@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using Xunit; @@ -364,6 +365,31 @@ public void EncodedStringTags_Read() } } + [Theory(Skip = "2.1 JPEG decoder detects this image as invalid.")] + [WithFile(TestImages.Jpeg.Issues.Issue2758, PixelTypes.L8)] + public void Issue2758_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider) + where TPixel : unmanaged, IPixel<TPixel> + { + using Image<TPixel> image = provider.GetImage(); + + Assert.Equal(59787, image.Width); + Assert.Equal(511, image.Height); + + JpegMetadata meta = image.Metadata.GetJpegMetadata(); + + // Quality determination should be between 1-100. + Assert.Equal(15, meta.LuminanceQuality); + Assert.Equal(1, meta.ChrominanceQuality); + + // We want to test the encoder to ensure the determined values can be encoded but not by encoding + // the full size image as it would be too slow. + // We will crop the image to a smaller size and then encode it. + image.Mutate(x => x.Crop(new(0, 0, 100, 100))); + + using MemoryStream ms = new(); + image.Save(ms, new JpegEncoder()); + } + private static void VerifyEncodedStrings(ExifProfile exif) { Assert.NotNull(exif);
tests/ImageSharp.Tests/TestImages.cs+2 −0 modified@@ -269,6 +269,7 @@ public static class Issues public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg"; public const string Issue2133DeduceColorSpace = "Jpg/issues/Issue2133.jpg"; public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg"; + public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; public static class Fuzz { @@ -463,6 +464,7 @@ public static class Issues public const string Issue1962NoColorTable = "Gif/issues/issue1962_tiniest_gif_1st.gif"; public const string Issue2012EmptyXmp = "Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif"; public const string Issue2012BadMinCode = "Gif/issues/issue2012_drona1.gif"; + public const string Issue2758 = "Gif/issues/issue_2758.gif"; } public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };
tests/Images/Input/Gif/issues/issue_2758.gif+3 −0 added@@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13e9374181c7536d1d2ecb514753a5290c0ec06234ca079c6c8c8a832586b668 +size 199
tests/Images/Input/Jpg/issues/issue-2758.jpg+3 −0 added@@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f32a238b57b7073f7442f8ae7efd6ba3ae4cda30d57e6666fb8a1eaa27108558 +size 1412
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-qxrv-gp6x-rc23ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-41132ghsaADVISORY
- docs.sixlabors.com/articles/imagesharp.web/processingcommands.htmlghsax_refsource_MISCWEB
- docs.sixlabors.com/articles/imagesharp/security.htmlghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/commit/59de13c8cc47f2b402e2c43aa7024511d029d515ghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/commit/9816ca45016c5d3859986f3c600e8934bc450a56ghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/commit/b496109051cc39feee1f6cde48fca6481de17f9aghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/pull/2759ghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/pull/2764ghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/pull/2770ghsax_refsource_MISCWEB
- github.com/SixLabors/ImageSharp/security/advisories/GHSA-qxrv-gp6x-rc23ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.