Repository navigation
BMP decoder accepts invalid uncompressed bit depths in Identify and allocates the full pixel buffer for truncated files #3235
Description
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp
- I have verified if the problem exist in both
DEBUGandRELEASEmode - I have searched open and closed issues to ensure it has not already been reported
ImageSharp version
main @ a73c43d (also present in v4.1.3)
Other ImageSharp packages and versions
None
Environment (Operating system, version and so on)
Windows 11 Pro 10.0.22631, x64
.NET Framework version
.NET 10 (net10.0)
Description
BmpDecoderCore has two problems with malformed headers.
1. Invalid bit depth for an uncompressed (BI_RGB) bitmap
For BI_RGB, the only valid bit depths are 1, 2, 4, 8, 16, 24 and 32. Other depths are not handled consistently:
Image.Identifyaccepts the header. With a depth of 0 it reports 0 bits per pixel.Image.Loadfails with a different exception for each depth:- 0 or 33:
InvalidImageContentException - 3, 5 or 7:
ImageFormatExceptionthat wraps anIndexOutOfRangeExceptionfrom the palette reader - 12:
NotSupportedException
- 0 or 33:
Expected: both Identify and Load throw InvalidImageContentException for any other depth. The cause is that ReadImageHeaders does not validate the depth. The only check is a bitsPerPixel is <= 0 or > 32 case in Decode, and the palette and RGB readers reach their own failures first.
2. Pixel data smaller than the header declares
Decode allocates the full pixel buffer before it reads any pixel data. A 126-byte file that declares 4000x4000 at 24 bpp requests a 64 MB buffer before the first row read fails. At 10000x10000 the request is about 400 MB. The row readers fail only when a read returns no bytes. Because of this, a file that declares one 100000-pixel row but holds only 72 bytes of pixel data decodes without an error.
Expected: for uncompressed and bit fields bitmaps the pixel data size is fixed by the header. If the stream is shorter than that size, the decoder throws InvalidImageContentException before it allocates the image.
A fix is proposed in #3223.
Related: #2696 (closed) reported the same large allocation from a small file. Allocation limits were added then, but the decoder still allocates the full declared pixel buffer before it finds that the data is missing.
Steps to Reproduce
using System.Buffers.Binary;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
// 54-byte file header + BITMAPINFOHEADER (BI_RGB), followed by zeroed pixel data.
static byte[] CreateBmp(int width, int height, short bitsPerPixel, int pixelDataLength)
{
byte[] bmp = new byte[54 + pixelDataLength];
bmp[0] = (byte)'B';
bmp[1] = (byte)'M';
BinaryPrimitives.WriteInt32LittleEndian(bmp.AsSpan(2), bmp.Length);
BinaryPrimitives.WriteInt32LittleEndian(bmp.AsSpan(10), 54);
BinaryPrimitives.WriteInt32LittleEndian(bmp.AsSpan(14), 40);
BinaryPrimitives.WriteInt32LittleEndian(bmp.AsSpan(18), width);
BinaryPrimitives.WriteInt32LittleEndian(bmp.AsSpan(22), height);
BinaryPrimitives.WriteInt16LittleEndian(bmp.AsSpan(26), 1);
BinaryPrimitives.WriteInt16LittleEndian(bmp.AsSpan(28), bitsPerPixel);
return bmp;
}
// 1. Invalid bit depths.
foreach (short bpp in new short[] { 0, 3, 7, 12, 33 })
{
byte[] data = CreateBmp(2, 2, bpp, 8);
ImageInfo info = Image.Identify(data); // does not throw
Console.WriteLine($"{bpp}: Identify -> {info.PixelType.BitsPerPixel} bpp");
try
{
using Image<Rgba32> image = Image.Load<Rgba32>(data);
}
catch (Exception ex)
{
Console.WriteLine($"{bpp}: Load -> {ex.GetType().Name}: {ex.Message}");
}
}
// 2a. 4000x4000 at 24 bpp with 72 bytes of pixel data.
// The 4000 x 4000 x 4 byte (64 MB) Rgba32 pixel buffer is allocated before the first row read fails.
using (Image<Rgba32> image = Image.Load<Rgba32>(CreateBmp(4000, 4000, 24, 72))) { }
// 2b. One 100000-pixel row (300000 bytes declared) with 72 bytes of pixel data.
using (Image<Rgba32> image = Image.Load<Rgba32>(CreateBmp(100000, 1, 24, 72)))
{
Console.WriteLine($"Decoded {image.Width}x{image.Height}"); // no error
}Actual result:
0: Identify -> 0 bpp
0: Load -> InvalidImageContentException: Invalid bits per pixel: 0
3: Identify -> 3 bpp
3: Load -> ImageFormatException: Bitmap does not have a valid format. (inner: IndexOutOfRangeException)
7: Identify -> 7 bpp
7: Load -> ImageFormatException: Bitmap does not have a valid format. (inner: IndexOutOfRangeException)
12: Identify -> 12 bpp
12: Load -> NotSupportedException: ImageSharp does not support this kind of bitmap files.
33: Identify -> 33 bpp
33: Load -> InvalidImageContentException: Invalid bits per pixel: 33
2a: InvalidImageContentException: Could not read enough data for a pixel row! (thrown after the 64 MB buffer is allocated)
2b: Decoded 100000x1
Expected result:
- Case 1:
IdentifyandLoadboth throwInvalidImageContentExceptionfor every one of these depths. - Cases 2a and 2b:
LoadthrowsInvalidImageContentExceptionbefore it allocates the pixel buffer, because the stream holds less pixel data than the header declares. A file that is missing only the padding of its last row should still decode, as it does now.
Images
No images needed; the repro builds the input in memory.
Activity