winforms icon indicating copy to clipboard operation
winforms copied to clipboard

Crash (AccessViolationException/ExecutionEngineException) graphics.FillRectangle into Bitmap with Format24bppRgb

Open deng0 opened this issue 1 month ago • 9 comments

.NET version

The crash happens in all .NET Core Version I have tested (8/9/10). In .NET 8.0 it's an AccessViolationException and in 9/10 it's an ExecutionEngineException.

Did it work in .NET Framework?

Yes

Did it work in any of the earlier releases of .NET Core or .NET 5+?

No response

Issue description

When not debugging the application just crashes. When debugging you get an AccessViolationException/ExecutionEngineException on the graphics.FillRectangle command. But it's not just FillRectangle. The problem also occurs when using DrawImage with a similar target rectangleF. I suppose there is a serious bug in System.Drawing.Drawing2D/GDI, that it tries to set pixel values outside the bounds of the bitmap data.

Steps to reproduce

Create simple winforms app and add the following code:

            for (int i = 0; i < 100; i++)
            {
                using (var bmp = new Bitmap(256, 256, PixelFormat.Format24bppRgb))
                using (var graphics = Graphics.FromImage(bmp))
                {
                    graphics.SmoothingMode = SmoothingMode.AntiAlias;
                    graphics.FillRectangle(Brushes.Green, new RectangleF(190.5f, 180.5f, 100, 100)); // <- Crash
                }
            }

Most of the time the crash occurs on first attempt, but not everytime, that's why I added the loop.

It seems the crash only occurs with the specific combination of PixelFormat.Format24bppRgb, SmoothingMode.AntiAlias and a Rectangle that exceeds the bounds of the Bitmap and uses non-integer coordinates.

deng0 avatar Dec 05 '25 14:12 deng0

I'll take a look at this.

It appears this is a known issue where GDI+ writes out of bounds with this specific combination of parameters (PixelFormat.Format24bppRgb, SmoothingMode.AntiAlias, and drawing outside the bitmap bounds).

In .NET 8 this resulted in an AccessViolationException, which the runtime marshaled. In .NET 9+, the runtime treats this memory corruption more strictly, resulting in a fatal ExecutionEngineException.

Since we cannot catch ExecutionEngineException (nor should we, as the process state is corrupted), the fix is to validate the parameters in System.Drawing before calling into GDI+ to prevent the crash entirely.

merriemcgaw avatar Dec 05 '25 17:12 merriemcgaw

@merriemcgaw Thanks for taking a look at it.

To be honest, I wouldn't call your change a "fix". Obviously a fatal crash is very bad, but your change would also break cases where it would have worked before. And, as i said, the same happens with DrawImage. There are probably other methods/overloads that run into the same GDI+ problem. I'm not sure, if adding this kind of parameter validation to multiple methods is worth it, as it wouldn't really fix the problem.

To work around this problem I've changed our code to use a different PixelFormat for the rendertarget bitmap. This seems to work. You say, that this is a known issue with Format24bppRgb and GDI+, can you point me to the documentation/discussion of that issue? I would like to get a confirmation that PixelFormats like Format32bppRgb or Format32bppArgb do not suffer from this GDI+ bug?

In general, we are in agreement that calls like graphics.FillRectangle() should normally never cause an exception/crash even when the rectangle is outside of the bounds of the underlying bitmap, right?

deng0 avatar Dec 05 '25 18:12 deng0

@deng0 Thanks for the feedback. You are absolutely right that throwing an exception here is too aggressive and breaks valid use cases where drawing should simply be clipped.

I've updated the PR to implement manual clipping in System.Drawing instead.

When we detect the specific crash condition (Format24bppRgb + AntiAlias + Out-of-bounds), we now:

  1. Calculate the intersection of the drawing rectangle and the bitmap bounds.
  2. Skip the call if the rectangle is completely out of bounds.
  3. Pass clipped coordinates to GDI+ if there is an overlap.
    • For FillRectangle / FillRectangles, we draw the clipped rectangle.
    • For DrawImage, we mathematically adjust the srcRect to match the clipped destRect, ensuring the image is not distorted or scaled incorrectly.

This fix covers:

  • FillRectangle (all overloads).
  • FillRectangles (specifically the RectangleF overloads).
  • DrawImage (all Rectangle / RectangleF / x,y,w,h overloads).

Why only Format24bppRgb? We've confirmed that this issue is specific to Format24bppRgb because 24-bit bitmaps lack padding for 32-bit alignment on pixel boundaries, which seems to trigger a buffer calculation error in the native GDI+ anti-aliasing algorithm when coordinates exceed the canvas. Formats like Format32bppArgb are naturally 32-bit aligned and do not suffer from this specific memory corruption bug.

Regarding Safety & Regressions: To ensure safety, I've scoped the fix strictly: the manual clipping logic only executes when the specific crash conditions are met. All other drawing operations remain 100% untouched and use the standard GDI+ path, so there is no risk of regression for existing working code.

For the DrawImage calculation itself, we use linear interpolation to find the corresponding portion of the source image, preserving scaling and aspect ratio. The only theoretical edge case is if you were using negative width/height (to flip an image) while triggering this crash. In that specific case, the fix normalizes the rectangle to prevent the crash, which might draw the image unflipped. Given the alternative is a fatal ExecutionEngineException, we believe this is the correct trade-off.

I've added a comprehensive regression test suite covering FillRectangle, FillRectangles, and DrawImage with various out-of-bounds scenarios (fully out, partially out, negative coordinates) to verify the fix is robust.

FYI @JeremyKuhne

merriemcgaw avatar Dec 06 '25 00:12 merriemcgaw

Presumably this issue reproduces in all versions of .NET as it appears to be a GDI+ issue (or possibly a WIC problem, the codec code does get updated there). If we can't repro in .NET Framework we should validate why not and that this issue is, in fact, a GDI+ issue. Ultimately, if System.Drawing is calling GDI+ correctly, the bug should go to GDI+ and we should not try to protect from a bug further down the stack- anyone will hit this if they are using GDI+ directly.

JeremyKuhne avatar Dec 06 '25 20:12 JeremyKuhne

@merriemcgaw I really respect your effort in trying to work around the GDI+ bug, but I still think it's not worth it/feasible trying to fix it that way. I haven't tested it, but I suspect your workaround would not work if there is a Transform defined on the graphics. Also your clipping of the rectangle would probably break the placement of special FillBrushes (e.g. Hatched). Also I suspect that almost any drawing method is probably affected by this GDI+ bug if it hits the right pixel.

I agree with @JeremyKuhne that it would be very interesting to find out why this crash does not seem to happen in .NET Framework. I've noticed that the size of the Bitmap is also an important contributing factor. The crash happens much more consistently if the bitmap size is a power of two (e.g. 256x256). I've tried to create a test with more randomness (sizes and memory allocations):

                var random = new Random();
                for (int i = 0; i < 300; i++)
                {
                    int rand = random.Next(0, 300);
                    List<Bitmap> bitmaps = new List<Bitmap>();

                    for (int j = 0; j < rand; j++)
                    {
                        int width = 256;//random.Next(100, 500);
                        int height = 256;//random.Next(100, 500);

                        var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);
                        bitmaps.Add(bmp);

                        using (var graphics = Graphics.FromImage(bmp))
                        {
                            graphics.SmoothingMode = SmoothingMode.AntiAlias;

                            for (int k = 0; k < 100; k++)
                            {
                                float x = random.Next(100, width) + 0.5f;
                                float y = random.Next(100, height) + 0.5f;
                                float w = 100 + random.Next(100, width);
                                float h = 100 + random.Next(100, height);

                                var rect = new RectangleF(x, y, w, h);
                                System.Diagnostics.Debug.WriteLine($"Trying {rect} on bitmap {width}x{height}");
                                System.Diagnostics.Debug.Flush(); // flushing sometimes does not work before the crash
                                graphics.FillRectangle(Brushes.Green, rect);
                            }
                        }
                    }

                    foreach (var bmp in bitmaps)
                    {
                        bmp.Dispose();
                    }
                }

But I haven't gotten it to crash with .NET Framework. Perhaps in .NET Framework the Format24bppRgb Bitmap is allocated with a padding?

If it's really not possible to fix the actual problem, perhaps it would be better to implement a breaking change on the .NET side that drawing on Format24bppRgb with AntiAliasing is not supported (throwing NotSupportedException when AntiAliasing is enabled or perhaps even throwing NotSupportedException when doing Graphics.FromImage(bmp with Format24bppRgb)).

deng0 avatar Dec 07 '25 09:12 deng0

If it's really not possible to fix the actual problem, perhaps it would be better to implement a breaking change on the .NET side that drawing on Format24bppRgb with AntiAliasing is not supported (throwing NotSupportedException when AntiAliasing is enabled or perhaps even throwing NotSupportedException when doing Graphics.FromImage(bmp with Format24bppRgb)).

Hi @deng0 my be this is an Os issue or Hardware bug becuase I can't regenrate this in my ASUS G75VW with NVIDIA GeForce GTX 660M graphics and Windows 11 24H2 (OS Build 261007171);

Live debug session

https://github.com/user-attachments/assets/4a149e64-b6c6-41b9-a22b-74a7a76d3ef9

Can you give me more informations about The test environment and full Exception Stack Trace from event viewer

See Live example

https://github.com/user-attachments/assets/85e2bdb2-0c24-4cc2-90c0-6f9e961c6f6f

memoarfaa avatar Dec 07 '25 16:12 memoarfaa

@memoarfaa This is strange that you can't reproduce the crash. On the systems I've tested it crashes almost immediately (after 3-4 FillRectangle calls):

'WinFormsApp1.exe' (CoreCLR: clrhost): 
Trying {X=217.5,Y=172.5,Width=321,Height=304} on bitmap 256x256
Trying {X=225.5,Y=201.5,Width=233,Height=309} on bitmap 256x256
Trying {X=130.5,Y=142.5,Width=205,Height=264} on bitmap 256x256
An unhandled exception of type 'System.ExecutionEngineException' occurred in System.Drawing.Common.dll

From the event viewer when not Debugging with VS: .NET Runtime:

Application: WinFormsApp1.exe
CoreCLR Version: 9.0.925.41916
.NET Version: 9.0.9
Description: The process was terminated due to an unhandled exception.
Stack:
   at System.Drawing.Graphics.FillRectangle(System.Drawing.Brush, Single, Single, Single, Single)
   at System.Drawing.Graphics.FillRectangle(System.Drawing.Brush, System.Drawing.RectangleF)
   at WinFormsApp1.Program.Main()

Application Error:

Faulting application name: WinFormsApp1.exe, version: 1.0.0.0, time stamp: 0x68f20000
Faulting module name: coreclr.dll, version: 9.0.925.41916, time stamp: 0x68a4f589
Exception code: 0xc0000005
Fault offset: 0x00000000002e5222
Faulting process id: 0x2E88
Faulting application start time: 0x1DC67A779A25B9E
Faulting application path: C:\Workspace\WinFormsApp1\WinFormsApp1\bin\Debug\net9.0-windows\WinFormsApp1.exe
Faulting module path: C:\Program Files\dotnet\shared\[Microsoft.NETCore.App](http://microsoft.netcore.app/)\9.0.9\coreclr.dll
Report Id: 1f2da677-a855-48f4-9ce1-fada23758665
Faulting package full name:
Faulting package-relative application ID: 

Windows Error Reporting:

Fault bucket 1825635147455034681, type 4
Event Name: APPCRASH
Response: Not available
Cab Id: 0

Problem signature:
P1: WinFormsApp1.exe
P2: 1.0.0.0
P3: 68f20000
P4: coreclr.dll
P5: 9.0.925.41916
P6: 68a4f589
P7: c0000005
P8: 00000000002e5222
P9:
P10: 

The system I've tested are the following: Notebook: OS: Windows 11 Home 24H2 26100.7171 CPU: AMD Ryzen AI 9 HX 370 w/ Radeon 890M RAM: 32GB GPU: NVIDIA GeForce RTX 4060 Laptop GPU

Desktop PC: OS: Windows 11 Pro 25H2 26200.7171 CPU: Intel Core i9-13900K RAM: 32GB GPU: NVIDIA GeForce RTX 4090

deng0 avatar Dec 07 '25 18:12 deng0

I am going to hold off on making any change here in WinForms because I'm sending this up the Windows chain right now.

merriemcgaw avatar Dec 08 '25 23:12 merriemcgaw

This should be a boundary handling defect in GDI+ in Version=2 mode(the version changed in https://github.com/dotnet/runtime/pull/35169). .NET Framework project couldn't reproduce the problem because it uses GdiplusVersion = 1, while .NET uses result.Base.GdiplusVersion = isWindows7 ? 1u : 2. Changing it back to 1 resolves the issue.

LeafShi1 avatar Dec 09 '25 09:12 LeafShi1