diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs
index 16925cc65..773fe7c26 100644
--- a/src/ImageSharp/Formats/Ani/AniConstants.cs
+++ b/src/ImageSharp/Formats/Ani/AniConstants.cs
@@ -23,6 +23,15 @@ internal static class AniConstants
///
public const int IconDirHeaderSize = 6;
+ ///
+ /// The maximum number of bytes retained from an ancillary chunk.
+ ///
+ ///
+ /// 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.
+ ///
+ public const int MaxAncillaryChunkSize = 8 * 1024 * 1024;
+
///
/// The list of MIME types that identify ANI data.
///
diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs
index 39583cfe4..cdfcf3016 100644
--- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs
+++ b/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;
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 111f61fbf..af6582420 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -890,7 +890,7 @@ internal sealed class BmpEncoderCore
where TPixel : unmanaged, IPixel
{
// 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);
}
diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
index e1ff17fec..93de4d2d9 100644
--- a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
@@ -145,4 +145,66 @@ public class AniDecoderTests
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream));
}
+
+ ///
+ /// Verifies that oversized control arrays are rejected before allocation and follow ancillary integrity handling.
+ ///
+ /// to append a sequence chunk; otherwise, a rate chunk.
+ [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 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 image = AniDecoder.Instance.Decode(DecoderOptions.Default, defaultStream);
+
+ Assert.Equal(4, image.Frames.Count);
+
+ using MemoryStream strictStream = new(data, false);
+ DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
+ Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream));
+ }
+
+ ///
+ /// Verifies that oversized information text is rejected before allocation and follows ancillary integrity handling.
+ ///
+ [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 image = AniDecoder.Instance.Decode(DecoderOptions.Default, defaultStream);
+
+ Assert.Equal(4, image.Frames.Count);
+
+ using MemoryStream strictStream = new(data, false);
+ DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
+ Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream));
+ }
}
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
index f637d64bf..08bb537ad 100644
--- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
+++ b/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);
}
+
+ ///
+ /// Verifies that the final partial AND-mask byte contains every pixel when the bitmap width is not byte-aligned.
+ ///
+ /// The bitmap width to encode.
+ [Theory]
+ [InlineData(1)]
+ [InlineData(7)]
+ [InlineData(9)]
+ [InlineData(15)]
+ public void BmpEntry_WritesPartialAlphaMaskByte(int width)
+ {
+ using Image image = new(width, 1, Color.Red.ToPixel());
+ image[width - 1, 0] = Color.Transparent.ToPixel();
+
+ 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 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]);
+ }
}