Browse Source

Fix #2859

pull/2887/head
James Jackson-South 1 year ago
parent
commit
29f17eb05e
  1. 325
      src/ImageSharp/Formats/Gif/LzwDecoder.cs
  2. 12
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  3. 2
      tests/ImageSharp.Tests/TestImages.cs
  4. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png
  5. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png
  6. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png
  7. 3
      tests/Images/Input/Gif/issues/issue_2859_A.gif
  8. 3
      tests/Images/Input/Gif/issues/issue_2859_B.gif

325
src/ImageSharp/Formats/Gif/LzwDecoder.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@ -37,22 +36,22 @@ internal sealed class LzwDecoder : IDisposable
/// <summary>
/// The prefix buffer.
/// </summary>
private readonly IMemoryOwner<int> prefix;
private readonly IMemoryOwner<int> prefixOwner;
/// <summary>
/// The suffix buffer.
/// </summary>
private readonly IMemoryOwner<int> suffix;
private readonly IMemoryOwner<int> suffixOwner;
/// <summary>
/// The scratch buffer for reading data blocks.
/// </summary>
private readonly IMemoryOwner<byte> scratchBuffer;
private readonly IMemoryOwner<byte> bufferOwner;
/// <summary>
/// The pixel stack buffer.
/// </summary>
private readonly IMemoryOwner<int> pixelStack;
private readonly IMemoryOwner<int> pixelStackOwner;
private readonly int minCodeSize;
private readonly int clearCode;
private readonly int endCode;
@ -79,11 +78,12 @@ internal sealed class LzwDecoder : IDisposable
public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size.");
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.prefixOwner = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.suffixOwner = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.pixelStackOwner = memoryAllocator.Allocate<int>(MaxStackSize + 1, AllocationOptions.Clean);
this.bufferOwner = memoryAllocator.Allocate<byte>(byte.MaxValue, AllocationOptions.None);
this.minCodeSize = minCodeSize;
// Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
@ -93,11 +93,15 @@ internal sealed class LzwDecoder : IDisposable
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++)
// Fill the suffix buffer with the initial values represented by the number of colors.
Span<int> suffix = this.suffixOwner.GetSpan()[..this.clearCode];
int i;
for (i = 0; i < suffix.Length; i++)
{
Unsafe.Add(ref suffixRef, (uint)this.code) = (byte)this.code;
suffix[i] = i;
}
this.code = i;
}
/// <summary>
@ -112,8 +116,7 @@ internal sealed class LzwDecoder : IDisposable
// 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)
if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize)
{
// Don't attempt to decode the frame indices.
// Theoretically we could determine a min code size from the length of the provided
@ -132,112 +135,139 @@ internal sealed class LzwDecoder : IDisposable
{
indices.Clear();
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();
int x = 0;
int xyz = 0;
while (xyz < indices.Length)
// Get span values from the owners.
Span<int> prefix = this.prefixOwner.GetSpan();
Span<int> suffix = this.suffixOwner.GetSpan();
Span<int> pixelStack = this.pixelStackOwner.GetSpan();
Span<byte> buffer = this.bufferOwner.GetSpan();
// Cache frequently accessed instance fields into locals.
// This helps avoid repeated field loads inside the tight loop.
BufferedReadStream stream = this.stream;
int top = this.top;
int bits = this.bits;
int codeSize = this.codeSize;
int codeMask = this.codeMask;
int minCodeSize = this.minCodeSize;
int availableCode = this.availableCode;
int oldCode = this.oldCode;
int first = this.first;
int data = this.data;
int count = this.count;
int bufferIndex = this.bufferIndex;
int code = this.code;
int clearCode = this.clearCode;
int endCode = this.endCode;
int i = 0;
while (i < indices.Length)
{
if (this.top == 0)
if (top == 0)
{
if (this.bits < this.codeSize)
if (bits < codeSize)
{
// Load bytes until there are enough bits for a code.
if (this.count == 0)
if (count == 0)
{
// Read a new data block.
this.count = this.ReadBlock(buffer);
if (this.count == 0)
count = ReadBlock(stream, buffer);
if (count == 0)
{
break;
}
this.bufferIndex = 0;
bufferIndex = 0;
}
this.data += buffer[this.bufferIndex] << this.bits;
this.bits += 8;
this.bufferIndex++;
this.count--;
data += buffer[bufferIndex] << bits;
bits += 8;
bufferIndex++;
count--;
continue;
}
// Get the next code
this.code = this.data & this.codeMask;
this.data >>= this.codeSize;
this.bits -= this.codeSize;
code = data & codeMask;
data >>= codeSize;
bits -= codeSize;
// Interpret the code
if (this.code > this.availableCode || this.code == this.endCode)
if (code > availableCode || code == endCode)
{
break;
}
if (this.code == this.clearCode)
if (code == clearCode)
{
// Reset the decoder
this.codeSize = this.minCodeSize + 1;
this.codeMask = (1 << this.codeSize) - 1;
this.availableCode = this.clearCode + 2;
this.oldCode = NullCode;
codeSize = minCodeSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
continue;
}
if (this.oldCode == NullCode)
if (oldCode == NullCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.oldCode = this.code;
this.first = this.code;
pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
int inCode = this.code;
if (this.code == this.availableCode)
int inCode = code;
if (code == availableCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first;
this.code = this.oldCode;
pixelStack[top++] = first;
code = oldCode;
}
while (this.code > this.clearCode)
while (code > clearCode && top < MaxStackSize)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.code = Unsafe.Add(ref prefixRef, (uint)this.code);
pixelStack[top++] = suffix[code];
code = prefix[code];
}
int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code);
this.first = suffixCode;
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode;
int suffixCode = suffix[code];
first = suffixCode;
pixelStack[top++] = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here :
// Fix for GIFs that have "deferred clear code" as per:
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (this.availableCode < MaxStackSize)
if (availableCode < MaxStackSize)
{
Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode;
Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first;
this.availableCode++;
if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
prefix[availableCode] = oldCode;
suffix[availableCode] = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
this.codeSize++;
this.codeMask = (1 << this.codeSize) - 1;
codeSize++;
codeMask = (1 << codeSize) - 1;
}
}
this.oldCode = inCode;
oldCode = inCode;
}
// Pop a pixel off the pixel stack.
this.top--;
top--;
// Clear missing pixels
xyz++;
Unsafe.Add(ref pixelsRowRef, (uint)x++) = (byte)Unsafe.Add(ref pixelStackRef, (uint)this.top);
// Clear missing pixels.
indices[i++] = (byte)pixelStack[top];
}
// Write back the local values to the instance fields.
this.top = top;
this.bits = bits;
this.codeSize = codeSize;
this.codeMask = codeMask;
this.availableCode = availableCode;
this.oldCode = oldCode;
this.first = first;
this.data = data;
this.count = count;
this.bufferIndex = bufferIndex;
this.code = code;
}
/// <summary>
@ -246,130 +276,161 @@ internal sealed class LzwDecoder : IDisposable
/// <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 xyz = 0;
while (xyz < length)
// Get span values from the owners.
Span<int> prefix = this.prefixOwner.GetSpan();
Span<int> suffix = this.suffixOwner.GetSpan();
Span<int> pixelStack = this.pixelStackOwner.GetSpan();
Span<byte> buffer = this.bufferOwner.GetSpan();
// Cache frequently accessed instance fields into locals.
// This helps avoid repeated field loads inside the tight loop.
BufferedReadStream stream = this.stream;
int top = this.top;
int bits = this.bits;
int codeSize = this.codeSize;
int codeMask = this.codeMask;
int minCodeSize = this.minCodeSize;
int availableCode = this.availableCode;
int oldCode = this.oldCode;
int first = this.first;
int data = this.data;
int count = this.count;
int bufferIndex = this.bufferIndex;
int code = this.code;
int clearCode = this.clearCode;
int endCode = this.endCode;
int i = 0;
while (i < length)
{
if (this.top == 0)
if (top == 0)
{
if (this.bits < this.codeSize)
if (bits < codeSize)
{
// Load bytes until there are enough bits for a code.
if (this.count == 0)
if (count == 0)
{
// Read a new data block.
this.count = this.ReadBlock(buffer);
if (this.count == 0)
count = ReadBlock(stream, buffer);
if (count == 0)
{
break;
}
this.bufferIndex = 0;
bufferIndex = 0;
}
this.data += buffer[this.bufferIndex] << this.bits;
this.bits += 8;
this.bufferIndex++;
this.count--;
data += buffer[bufferIndex] << bits;
bits += 8;
bufferIndex++;
count--;
continue;
}
// Get the next code
this.code = this.data & this.codeMask;
this.data >>= this.codeSize;
this.bits -= this.codeSize;
code = data & codeMask;
data >>= codeSize;
bits -= codeSize;
// Interpret the code
if (this.code > this.availableCode || this.code == this.endCode)
if (code > availableCode || code == endCode)
{
break;
}
if (this.code == this.clearCode)
if (code == clearCode)
{
// Reset the decoder
this.codeSize = this.minCodeSize + 1;
this.codeMask = (1 << this.codeSize) - 1;
this.availableCode = this.clearCode + 2;
this.oldCode = NullCode;
codeSize = minCodeSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
continue;
}
if (this.oldCode == NullCode)
if (oldCode == NullCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.oldCode = this.code;
this.first = this.code;
pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
int inCode = this.code;
if (this.code == this.availableCode)
int inCode = code;
if (code == availableCode)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first;
this.code = this.oldCode;
pixelStack[top++] = first;
code = oldCode;
}
while (this.code > this.clearCode)
while (code > clearCode && top < MaxStackSize)
{
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
this.code = Unsafe.Add(ref prefixRef, (uint)this.code);
pixelStack[top++] = suffix[code];
code = prefix[code];
}
int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code);
this.first = suffixCode;
Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode;
int suffixCode = suffix[code];
first = suffixCode;
pixelStack[top++] = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here :
// Fix for GIFs that have "deferred clear code" as per:
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (this.availableCode < MaxStackSize)
if (availableCode < MaxStackSize)
{
Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode;
Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first;
this.availableCode++;
if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
prefix[availableCode] = oldCode;
suffix[availableCode] = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
this.codeSize++;
this.codeMask = (1 << this.codeSize) - 1;
codeSize++;
codeMask = (1 << codeSize) - 1;
}
}
this.oldCode = inCode;
oldCode = inCode;
}
// Pop a pixel off the pixel stack.
this.top--;
top--;
// Clear missing pixels
xyz++;
// Skip missing pixels.
i++;
}
// Write back the local values to the instance fields.
this.top = top;
this.bits = bits;
this.codeSize = codeSize;
this.codeMask = codeMask;
this.availableCode = availableCode;
this.oldCode = oldCode;
this.first = first;
this.data = data;
this.count = count;
this.bufferIndex = bufferIndex;
this.code = code;
}
/// <summary>
/// Reads the next data block from the stream. A data block begins with a byte,
/// which defines the size of the block, followed by the block itself.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the block in.</param>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ReadBlock(Span<byte> buffer)
private static int ReadBlock(BufferedReadStream stream, Span<byte> buffer)
{
int bufferSize = this.stream.ReadByte();
int bufferSize = stream.ReadByte();
if (bufferSize < 1)
{
return 0;
}
int count = this.stream.Read(buffer, 0, bufferSize);
int count = stream.Read(buffer, 0, bufferSize);
return count != bufferSize ? 0 : bufferSize;
}
@ -377,9 +438,9 @@ internal sealed class LzwDecoder : IDisposable
/// <inheritdoc />
public void Dispose()
{
this.prefix.Dispose();
this.suffix.Dispose();
this.pixelStack.Dispose();
this.scratchBuffer.Dispose();
this.prefixOwner.Dispose();
this.suffixOwner.Dispose();
this.pixelStackOwner.Dispose();
this.bufferOwner.Dispose();
}
}

12
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -334,4 +334,16 @@ public class GifDecoderTests
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
// https://github.com/SixLabors/ImageSharp/issues/2859
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2859_A, PixelTypes.Rgba32)]
[WithFile(TestImages.Gif.Issues.Issue2859_B, PixelTypes.Rgba32)]
public void Issue2859_LZWPixelStackOverflow<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
}

2
tests/ImageSharp.Tests/TestImages.cs

@ -536,6 +536,8 @@ public static class TestImages
public const string Issue2450_B = "Gif/issues/issue_2450_2.gif";
public const string Issue2198 = "Gif/issues/issue_2198.gif";
public const string Issue2758 = "Gif/issues/issue_2758.gif";
public const string Issue2859_A = "Gif/issues/issue_2859_A.gif";
public const string Issue2859_B = "Gif/issues/issue_2859_B.gif";
}
public static readonly string[] Animated =

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:daa78347749c6ff49891e2e379a373599cd35c98b453af9bf8eac52f615f935c
size 12237

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:731299281f942f277ce6803e0adda3b5dd0395eb79cae26cabc9d56905fae0fd
size 1833

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50ccac7739142578d99a76b6d39ba377099d4a7ac30cbb0a5aee44ef1e7c9c8c
size 1271

3
tests/Images/Input/Gif/issues/issue_2859_A.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50a1a4afc62a3a36ff83596f1eb55d91cdd184c64e0d1339bbea17205c23eee1
size 3406142

3
tests/Images/Input/Gif/issues/issue_2859_B.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db9b2992be772a4f0ac495e994a17c7c50fb6de9794cfb9afc4a3dc26ffdfae0
size 4543
Loading…
Cancel
Save