Skip to content
Merged
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
36 changes: 32 additions & 4 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,31 @@ 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;

// Reject impossible RGB and bit-fields dimensions before allocating the image. Ignoring image data errors
// does not make these dimensions safe: a tiny file could otherwise request a large pixel buffer.
// Match ReadBitFields, which uses 32-bit storage for every depth other than 16.
int storedBitsPerPixel = this.infoHeader.Compression switch
{
BmpCompression.RGB => bitsPerPixel,
BmpCompression.BitFields or BmpCompression.BI_ALPHABITFIELDS => bitsPerPixel is 16 ? 16 : 32,
_ => 0
};

if (storedBitsPerPixel > 0)
{
// Stored rows start on four-byte boundaries. Retain support for missing padding after the final row.
// Reserve that row's pixel bytes, then count the complete strides available for the other rows.
// Division avoids multiplying width by height, which can overflow even long for 32-bit BMP dimensions.
long rowBits = (long)this.infoHeader.Width * storedBitsPerPixel;
long stride = (rowBits + 31) / 32 * 4;
long finalRowBytes = (rowBits + 7) / 8;
long remainingLength = stream.Length - stream.Position;
if (remainingLength < finalRowBytes || this.infoHeader.Height - 1 > (remainingLength - finalRowBytes) / stride)
{
BmpThrowHelper.ThrowInvalidImageContentException("The bitmap does not contain enough pixel data for its declared dimensions.");
}
}

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 +210,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 +1594,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
77 changes: 77 additions & 0 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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 +697,80 @@ public void BmpDecoder_ThrowsException_Issue3074()
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
});
}

/// <summary>
/// Invalid RGB bit depths cannot define a pixel layout, even when image data errors are ignored.
/// </summary>
/// <param name="path">The stored bitmap with an unsupported RGB bit depth.</param>
[Theory]
[InlineData(InvalidRgbDepth0)]
[InlineData(InvalidRgbDepth3)]
[InlineData(InvalidRgbDepth7)]
[InlineData(InvalidRgbDepth12)]
[InlineData(InvalidRgbDepth33)]
public void BmpDecoder_InvalidUncompressedBitsPerPixel_ThrowsInvalidImageContentException(string path)
{
byte[] data = TestFile.Create(path).Bytes;

foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, data));
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
}

/// <summary>
/// Rejects dimensions that require more pixel data than the file contains before requesting pixel storage.
/// </summary>
/// <param name="path">The stored bitmap with incomplete RGB or bit-fields pixel data.</param>
[Theory]
[InlineData(TruncatedRgb24LargeImage)]
[InlineData(TruncatedRgb24WideRow)]
[InlineData(TruncatedBitFields16)]
[InlineData(TruncatedBitFields32)]
[InlineData(TruncatedAlphaBitFields32)]
[InlineData(TruncatedRgb32MaximumDimensions)]
public void BmpDecoder_PixelDataSmallerThanDeclared_ThrowsBeforeAllocatingPixelBuffer(string path)
{
byte[] data = TestFile.Create(path).Bytes;

foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
// The allocator's group limit prevents a failed maximum-dimension regression from allocating that image.
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = integrity };

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

// An allocation-limit exception is too late. No Rgba32 buffer, including a split buffer, may be requested.
Assert.Null(exception.InnerException);
Assert.DoesNotContain(allocator.AllocationLog, request => request.ElementType == typeof(Rgba32));
}
}

/// <summary>
/// Accepts a missing padding byte after the final stored row in either row order.
/// </summary>
/// <param name="path">The stored one-column bitmap with a red top pixel and a blue bottom pixel.</param>
[Theory]
[InlineData(MissingFinalRowPadding)]
[InlineData(MissingFinalRowPaddingTopDown)]
public void BmpDecoder_MissingLastRowPadding_Decodes(string path)
{
using Image<Rgb24> actual = Image.Load<Rgb24>(TestFile.Create(path).Bytes);

Assert.Equal(new Size(1, 2), actual.Size);
Assert.Equal(new Rgb24(255, 0, 0), actual[0, 0]);
Assert.Equal(new Rgb24(0, 0, 255), actual[0, 1]);
}
}
13 changes: 13 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,19 @@ public static class Bmp
// Note: The inverted images have been generated by altering the BitmapInfoHeader using a hex editor.
// As such, the expected pixel output will be the reverse of the unaltered equivalent images.
public const string Car = "Bmp/Car.bmp";
public const string InvalidRgbDepth0 = "Bmp/headers/invalid-rgb-depth-0.bmp";
public const string InvalidRgbDepth3 = "Bmp/headers/invalid-rgb-depth-3.bmp";
public const string InvalidRgbDepth7 = "Bmp/headers/invalid-rgb-depth-7.bmp";
public const string InvalidRgbDepth12 = "Bmp/headers/invalid-rgb-depth-12.bmp";
public const string InvalidRgbDepth33 = "Bmp/headers/invalid-rgb-depth-33.bmp";
public const string TruncatedRgb24LargeImage = "Bmp/headers/truncated-rgb24-4000x4000.bmp";
public const string TruncatedRgb24WideRow = "Bmp/headers/truncated-rgb24-100000x1.bmp";
public const string TruncatedBitFields16 = "Bmp/headers/truncated-bitfields16.bmp";
public const string TruncatedBitFields32 = "Bmp/headers/truncated-bitfields32.bmp";
public const string TruncatedAlphaBitFields32 = "Bmp/headers/truncated-alphabitfields32.bmp";
public const string TruncatedRgb32MaximumDimensions = "Bmp/headers/truncated-rgb32-max-dimensions.bmp";
public const string MissingFinalRowPadding = "Bmp/headers/missing-final-padding.bmp";
public const string MissingFinalRowPaddingTopDown = "Bmp/headers/missing-final-padding-top-down.bmp";
public const string F = "Bmp/F.bmp";
public const string NegHeight = "Bmp/neg_height.bmp";
public const string CoreHeader = "Bmp/BitmapCoreHeaderQR.bmp";
Expand Down
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/invalid-rgb-depth-0.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/invalid-rgb-depth-12.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/invalid-rgb-depth-3.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/invalid-rgb-depth-33.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/invalid-rgb-depth-7.bmp
Git LFS file not shown
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/missing-final-padding.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/truncated-alphabitfields32.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/truncated-bitfields16.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/truncated-bitfields32.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/truncated-rgb24-100000x1.bmp
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Bmp/headers/truncated-rgb24-4000x4000.bmp
Git LFS file not shown
Git LFS file not shown
Loading