winforms icon indicating copy to clipboard operation
winforms copied to clipboard

System.Windows.Forms.WebBrowser memory leak issue

Open phuongdoan13 opened this issue 5 months ago • 4 comments

.NET version

We are migrating our software from .NET framework 4.8 to .NET core 8.0

Did it work in .NET Framework?

Yes

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

Yes, it works in .NET 7

Issue description

I suspect that the System.Windows.Forms.WebBrowser has a memory leak issue in .NET 8.0 and 9.0.

Please see the repro code in Step to reproduce section.

Context

Our team is trying to migrate our software from .NET framework 4.8.1 into .NET core 8.0. We have a class called ZWebBrowser : WebBrowser. Any test that deals with this class, even just create this object alone, will fail. ZWebBrowser only add extra keys handlers, so I don't think it causes the un-GC issue above.

We know that WebBrowser is advised against, however, due to resources issue, we cannot abandon it.

Here is the memory profiler result on .NET core 8, showing that the inner class WebBrowserSite and WebBrowserEvent have (strong) RefCounted handle root, which does not let them get garbage collected. In .NET framework 4.8, these roots are Weak, RefCounted handle

Image

Steps to reproduce

Based on the test below, the wrapped WebBrowser object is garbage collected:

  • NOT successfully in .NET Core 8.0 and 9.0
  • successfully in .NET Core 7.0 + .NET Framework 4.8.1

using System.Runtime.CompilerServices;


namespace WinFormsTest
{
    public static class WebBrowserTests
    {
        public static void TestRunLeak()
        {
            var browserWeakRef = CreateWebBrowser();

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
           
            var isBrowserGC = !browserWeakRef.IsAlive;

            // NET CORE 8.0+: isBrowserGC is false;
            // NET CORE 7.0: isBrowserGC is true;
            // NET Framework 4.8: isBrowserGC is true;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        static WeakReference CreateWebBrowser()
        {
            using (var browser = new System.Windows.Forms.WebBrowser())
            {
                browser.Navigate("about:blank");
                return new WeakReference(browser);
            }
        }
    }
}

Extra

When diffing the .NET 7.0 and 8.0 source code, this code looks suspicious https://github.com/dotnet/winforms/blob/e9badd948720225f73defcafa0bccac3011b3256/src/System.Windows.Forms/src/System/Windows/Forms/WebBrowserBase.cs#L690C43-L690C54

phuongdoan13 avatar Aug 01 '25 00:08 phuongdoan13

@Olina-Zhang can you try to reproduce this and we can try to go from there?

merriemcgaw avatar Oct 06 '25 21:10 merriemcgaw

@merriemcgaw we can confirm the issue repro in .NET 8.0/9.0/10.0, not in .NET 7.0

WebBrowserLeakTestSample.zip

.NET 8.0:

Image

.NET 7.0:

Image

Olina-Zhang avatar Oct 09 '25 06:10 Olina-Zhang

@JeremyKuhne - this looks to be related to your CSWin32 migration from back in 8 timeframe. Can you help point us in teh right direction here?

@KlausLoeffelmann @LeafShi1 @Shyam-Gupta FYI

merriemcgaw avatar Nov 20 '25 23:11 merriemcgaw

@merriemcgaw: From the description given the event callbacks (ConnectionPointCookie) are what are appearing to keep this from being collected. The callback object might need to be held in a WeakHandle, but it isn't clear off the top of my head if that will cause grief from the native code. Happy to help whoever is assigned to this navigate possible solutions. (Note that this might be a problem for most ActiveX objects.)

The best thing to do as a WinForms consumer is not dangle disposable WinForms objects. I understand that isn't always plausible. :)

JeremyKuhne avatar Nov 21 '25 18:11 JeremyKuhne