Skip to content

BMP decoder accepts invalid uncompressed bit depths in Identify and allocates the full pixel buffer for truncated files #3235

Open

Description

@jafin

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 DEBUG and RELEASE mode
  • 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.Identify accepts the header. With a depth of 0 it reports 0 bits per pixel.
  • Image.Load fails with a different exception for each depth:
    • 0 or 33: InvalidImageContentException
    • 3, 5 or 7: ImageFormatException that wraps an IndexOutOfRangeException from the palette reader
    • 12: NotSupportedException

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: Identify and Load both throw InvalidImageContentException for every one of these depths.
  • Cases 2a and 2b: Load throws InvalidImageContentException before 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions