Browse Source

Handle oversized ANI chunks and partial icon masks

pull/2899/head
James Jackson-South 3 weeks ago
parent
commit
eadb2a6c79
  1. 9
      src/ImageSharp/Formats/Ani/AniConstants.cs
  2. 11
      src/ImageSharp/Formats/Ani/AniDecoderCore.cs
  3. 6
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  4. 62
      tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
  5. 31
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs

9
src/ImageSharp/Formats/Ani/AniConstants.cs

@ -23,6 +23,15 @@ internal static class AniConstants
/// </summary>
public const int IconDirHeaderSize = 6;
/// <summary>
/// The maximum number of bytes retained from an ancillary chunk.
/// </summary>
/// <remarks>
/// Control arrays and information strings come from untrusted input. Bounding them independently of the allocator
/// prevents a physically large RIFF chunk from consuming an unreasonable amount of memory.
/// </remarks>
public const int MaxAncillaryChunkSize = 8 * 1024 * 1024;
/// <summary>
/// The list of MIME types that identify ANI data.
/// </summary>

11
src/ImageSharp/Formats/Ani/AniDecoderCore.cs

@ -411,6 +411,14 @@ internal sealed class AniDecoderCore : ImageDecoderCore, IDisposable
return;
}
// MaxFrames controls retained animation steps, but its default is intentionally unbounded. Apply a separate
// byte limit before allocation so an oversized control chunk follows ancillary integrity handling.
if (chunkSize > AniConstants.MaxAncillaryChunkSize)
{
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk is too large.");
return;
}
int count = (int)Math.Min(chunkSize / sizeof(uint), this.Options.MaxFrames);
if (count is 0)
{
@ -494,7 +502,8 @@ internal sealed class AniDecoderCore : ImageDecoderCore, IDisposable
{
value = null;
if (chunkSize > int.MaxValue)
// INFO text is optional metadata. Reject or skip oversized values before renting their backing buffer.
if (chunkSize > AniConstants.MaxAncillaryChunkSize)
{
this.ThrowOrIgnoreNonStrictSegmentError("The ANI information text chunk is too large.");
return false;

6
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -890,7 +890,7 @@ internal sealed class BmpEncoderCore
where TPixel : unmanaged, IPixel<TPixel>
{
// Each byte represents eight pixels and every scanline is padded to a 4-byte DIB boundary.
int arrayWidth = encodingFrame.Width / 8;
int arrayWidth = (encodingFrame.Width + 7) / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
@ -910,7 +910,9 @@ internal sealed class BmpEncoderCore
{
int x = i * 8;
for (int j = 0; j < 8; j++)
// The final byte can represent fewer than eight pixels when the image width is not byte-aligned.
int pixelCount = Math.Min(8, encodingFrame.Width - x);
for (int j = 0; j < pixelCount; j++)
{
WriteAlphaMask(row[x + j], ref mask[i], j);
}

62
tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs

@ -145,4 +145,66 @@ public class AniDecoderTests
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => AniDecoder.Instance.Decode<Rgba32>(strict, strictStream));
}
/// <summary>
/// Verifies that oversized control arrays are rejected before allocation and follow ancillary integrity handling.
/// </summary>
/// <param name="sequence"><see langword="true"/> to append a sequence chunk; otherwise, a rate chunk.</param>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AniDecoder_OversizedControlChunk_FollowsIntegrityHandling(bool sequence)
{
byte[] source = TestFile.Create(Help).Bytes.ToArray();
int chunkOffset = (source.Length + 1) & ~1;
int payloadSize = AniConstants.MaxAncillaryChunkSize + sizeof(uint);
byte[] data = new byte[chunkOffset + AniConstants.ChunkHeaderSize + payloadSize];
source.CopyTo(data, 0);
ReadOnlySpan<byte> identifier = sequence ? "seq "u8 : "rate"u8;
identifier.CopyTo(data.AsSpan(chunkOffset));
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(chunkOffset + sizeof(uint)), (uint)payloadSize);
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sizeof(uint)), (uint)data.Length - AniConstants.ChunkHeaderSize);
using MemoryStream defaultStream = new(data, false);
using Image<Rgba32> image = AniDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, defaultStream);
Assert.Equal(4, image.Frames.Count);
using MemoryStream strictStream = new(data, false);
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => AniDecoder.Instance.Decode<Rgba32>(strict, strictStream));
}
/// <summary>
/// Verifies that oversized information text is rejected before allocation and follows ancillary integrity handling.
/// </summary>
[Fact]
public void AniDecoder_OversizedInformationText_FollowsIntegrityHandling()
{
byte[] source = TestFile.Create(Help).Bytes.ToArray();
int listOffset = (source.Length + 1) & ~1;
int textSize = AniConstants.MaxAncillaryChunkSize + 1;
int paddedTextSize = textSize + (textSize & 1);
int listSize = sizeof(uint) + AniConstants.ChunkHeaderSize + paddedTextSize;
byte[] data = new byte[listOffset + AniConstants.ChunkHeaderSize + listSize];
source.CopyTo(data, 0);
"LIST"u8.CopyTo(data.AsSpan(listOffset));
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(listOffset + sizeof(uint)), (uint)listSize);
"INFO"u8.CopyTo(data.AsSpan(listOffset + AniConstants.ChunkHeaderSize));
int textOffset = listOffset + AniConstants.ChunkHeaderSize + sizeof(uint);
"INAM"u8.CopyTo(data.AsSpan(textOffset));
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(textOffset + sizeof(uint)), (uint)textSize);
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sizeof(uint)), (uint)data.Length - AniConstants.ChunkHeaderSize);
using MemoryStream defaultStream = new(data, false);
using Image<Rgba32> image = AniDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, defaultStream);
Assert.Equal(4, image.Frames.Count);
using MemoryStream strictStream = new(data, false);
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => AniDecoder.Instance.Decode<Rgba32>(strict, strictStream));
}
}

31
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Icon;
@ -141,4 +142,34 @@ public class IcoEncoderTests
Assert.NotNull(decoded.Metadata.ExifProfile);
Assert.Equal(image.Metadata.ExifProfile.Values, decoded.Metadata.ExifProfile.Values);
}
/// <summary>
/// Verifies that the final partial AND-mask byte contains every pixel when the bitmap width is not byte-aligned.
/// </summary>
/// <param name="width">The bitmap width to encode.</param>
[Theory]
[InlineData(1)]
[InlineData(7)]
[InlineData(9)]
[InlineData(15)]
public void BmpEntry_WritesPartialAlphaMaskByte(int width)
{
using Image<Rgba32> image = new(width, 1, Color.Red.ToPixel<Rgba32>());
image[width - 1, 0] = Color.Transparent.ToPixel<Rgba32>();
IcoFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetIcoMetadata();
metadata.Compression = IconFrameCompression.Bmp;
metadata.BmpBitsPerPixel = BmpBitsPerPixel.Bit32;
using MemoryStream stream = new();
image.Save(stream, Encoder);
// These widths produce one DWORD-aligned mask row at the end of the bitmap resource.
ReadOnlySpan<byte> mask = stream.GetBuffer().AsSpan(checked((int)stream.Length) - sizeof(uint), sizeof(uint));
int pixelIndex = width - 1;
int byteIndex = pixelIndex / 8;
int bitIndex = pixelIndex % 8;
Assert.Equal((byte)(0b10000000 >> bitIndex), mask[byteIndex]);
}
}

Loading…
Cancel
Save