Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,33 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
ushort bitsPerPixel = this.infoHeader.BitsPerPixel;

// Uncompressed pixel data has a size fixed by the header, so a header that declares more data than the
// stream holds is rejected before the pixel buffer is allocated. Otherwise a small file could make the
// decoder allocate a buffer for its declared dimensions before the first row read fails.
// The bit fields reader decodes every depth other than 16 as 32 bits per pixel.
int storedBitsPerPixel = this.infoHeader.Compression switch
{
BmpCompression.RGB => bitsPerPixel,
BmpCompression.BitFields or BmpCompression.BI_ALPHABITFIELDS => bitsPerPixel is 16 ? 16 : 32,
_ => 0
};

if (storedBitsPerPixel > 0)
{
// Every row is padded to a multiple of four bytes. The padding of the last row is not required,
// because the row readers accept a short final read. Long arithmetic keeps the sizes of
// oversized declared dimensions from overflowing.
long rowBits = (long)this.infoHeader.Width * storedBitsPerPixel;
long stride = (rowBits + 31) / 32 * 4;
long requiredLength = (stride * (this.infoHeader.Height - 1)) + ((rowBits + 7) / 8);
long remainingLength = stream.Length - stream.Position;
if (requiredLength > remainingLength)
{
BmpThrowHelper.ThrowInvalidImageContentException(
$"The bitmap declares {requiredLength} bytes of pixel data, but only {remainingLength} bytes remain in the stream.");
}
}

image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);

Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
Expand Down Expand Up @@ -185,10 +212,6 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance

break;

case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32:
BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}");
break;

case BmpCompression.RLE24:
this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);

Expand Down Expand Up @@ -1573,6 +1596,13 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
BmpThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0.");
}

// Uncompressed pixel data is only defined for these depths. Rejecting any other depth here, rather than in
// the pixel readers, also keeps identification from reporting a header that cannot be decoded.
if (this.infoHeader.Compression is BmpCompression.RGB && this.infoHeader.BitsPerPixel is not (1 or 2 or 4 or 8 or 16 or 24 or 32))
{
BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {this.infoHeader.BitsPerPixel}");
}

int bytesPerColorMapEntry = 4;
int colorMapSizeBytes = -1;
if (this.infoHeader.ClrUsed == 0)
Expand Down
86 changes: 86 additions & 0 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers.Binary;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
Expand Down Expand Up @@ -696,4 +698,88 @@ public void BmpDecoder_ThrowsException_Issue3074()
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
});
}

[Theory]
[InlineData(0)]
[InlineData(3)]
[InlineData(7)]
[InlineData(12)]
[InlineData(33)]
public void BmpDecoder_InvalidUncompressedBitsPerPixel_ThrowsInvalidImageContentException(short bitsPerPixel)
{
byte[] data = CreateUncompressedBitmap(2, 2, bitsPerPixel, 8);

Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(data);
});
}

[Theory]
[InlineData(4000, 4000)]
[InlineData(100000, 1)]
public void BmpDecoder_PixelDataSmallerThanDeclared_ThrowsBeforeAllocatingPixelBuffer(int width, int height)
{
byte[] data = CreateUncompressedBitmap(width, height, 24, 72);
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
DecoderOptions options = new() { Configuration = configuration };

Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});

// The rejection must happen before the Rgba32 pixel buffer for the declared dimensions is requested.
Assert.DoesNotContain(allocator.AllocationLog, request => request.LengthInBytes >= width * height * 4);
}

[Fact]
public void BmpDecoder_MissingLastRowPadding_Decodes()
{
using Image<Rgb24> expected = new(1, 2);
expected[0, 0] = new Rgb24(255, 0, 0);
expected[0, 1] = new Rgb24(0, 0, 255);

using MemoryStream stream = new();
expected.SaveAsBmp(stream, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Bit24 });

// Each 24-bit row of one pixel holds three bytes of pixel data and one byte of padding.
// The pixel data is at the end of the file, so dropping the last byte removes the padding of the last row.
byte[] data = stream.ToArray()[..^1];

using Image<Rgb24> actual = Image.Load<Rgb24>(data);

Assert.Equal(expected[0, 0], actual[0, 0]);
Assert.Equal(expected[0, 1], actual[0, 1]);
}

/// <summary>
/// Creates a bitmap with a file header, a 40-byte BITMAPINFOHEADER without compression, and zeroed pixel data.
/// </summary>
/// <param name="width">The declared width.</param>
/// <param name="height">The declared height.</param>
/// <param name="bitsPerPixel">The declared bits per pixel.</param>
/// <param name="pixelDataLength">The number of pixel data bytes that follow the headers.</param>
/// <returns>The encoded bitmap.</returns>
private static byte[] CreateUncompressedBitmap(int width, int height, short bitsPerPixel, int pixelDataLength)
{
const int pixelDataOffset = 54;
byte[] bmp = new byte[pixelDataOffset + pixelDataLength];
Span<byte> span = bmp;
span[0] = (byte)'B';
span[1] = (byte)'M';
BinaryPrimitives.WriteInt32LittleEndian(span[2..], bmp.Length);
BinaryPrimitives.WriteInt32LittleEndian(span[10..], pixelDataOffset);
BinaryPrimitives.WriteInt32LittleEndian(span[14..], 40);
BinaryPrimitives.WriteInt32LittleEndian(span[18..], width);
BinaryPrimitives.WriteInt32LittleEndian(span[22..], height);
BinaryPrimitives.WriteInt16LittleEndian(span[26..], 1);
BinaryPrimitives.WriteInt16LittleEndian(span[28..], bitsPerPixel);

return bmp;
}
}