Possible Racing and multithreading problem on WritableBitmap.TryLock
- Dominant language
- C#
- Stars
- 7.7k
- Forks
- 1.3k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 61
Description
* .NET Core Version: 3.1
* Windows version: 21H1 19043.964
* Does the bug reproduce also in WPF for .NET Framework 4.8?: Not tested
* Is this bug related specifically to tooling in Visual Studio (e.g. XAML Designer, Code editing, etc...)? No
* Security issues and bugs should be reported privately, learn more via our [responsible disclosure guidelines](https://github.com/dotnet/wpf/blob/main/README.md#reporting-security-issues-and-security-bugs).
**Problem description:**
I'm using WritableBitmap in OnRender function inside a Control to draw something on the window, to access the Bitmap memory, I'm using WritableBitmap.TryLock but in some cases, it throws COMException.
**Actual behavior:**
```
System.Runtime.InteropServices.COMException
HResult=0x88982F0D
Message=There is already an outstanding read or write lock. (0x88982F0D)
Source=PresentationCore
StackTrace:
at System.Windows.Media.Imaging.WriteableBitmap.TryLock(Duration timeout)
at XXXXXX.OnRender(DrawingContext drawingContext) line 117
at System.Windows.UIElement.Arrange(Rect finalRect)
at System.Windows.ContextLayoutManager.UpdateLayout()
at System.Windows.ContextLayoutManager.UpdateLayoutCallback(Object arg)
at System.Windows.Media.MediaContext.InvokeOnRenderCallback.DoWork()
at System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
at System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
at System.Windows.Media.MediaContext.AnimatedRenderMessageHandler(Object resizedCompositionTarget)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
at System.Windows.Threading.DispatcherOperation.InvokeImpl()
at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
at System.Windows.Threading.DispatcherOperation.Invoke()
at System.Windows.Threading.Dispatcher.ProcessQueue()
at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
at System.Windows.Threading.Dispatcher.Run()
at System.Windows.Application.RunDispatcher(Object ignore)
at System.Windows.Application.RunInternal(Window window)
at System.Windows.Application.Run()
at XXXXXX.App.Main()
This exception was originally thrown at this call stack:
System.Windows.Media.Imaging.WriteableBitmap.TryLock(System.Windows.Duration)
XXXXXX.OnRender(System.Windows.Media.DrawingContext) in Spectrum.cs
System.Windows.UIElement.Arrange(System.Windows.Rect)
System.Windows.ContextLayoutManager.UpdateLayout()
System.Windows.ContextLayoutManager.UpdateLayoutCallback(object)
System.Windows.Media.MediaContext.InvokeOnRenderCallback.DoWork()
System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
System.Windows.Media.MediaContext.RenderMessageHandlerCore(object)
System.Windows.Media.MediaContext.AnimatedRenderMessageHandler(object)
System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate, object, int)
...
[Call Stack Truncated]
```
**Expected behavior:**
It should wait for a buffer until the timeout instead of throwing a COMException.
**Minimal repro:**
Not available, you can try lock & unlock the bitmap frequently, but since it's a racing condtion, it's really hard to repro directly and really depends on luck.
I did some analysis on the source code and I think it may because of the WritableBitmap implementation.
First of all, in the [WritableBitmap.TryLock](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/WriteableBitmap.cs#L265) it checks if `_lockCount == 0` and then process to `AcquireBackBuffer`.
In the `AcquireBackBuffer` function [_pBackBuffer](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/WriteableBitmap.cs#L1052) is check to see if there is an available buffer and if there is, it skip the acquisition of the buffer and return directly.
If `AcquireBackBuffer` succeed, the TryLock will put a lock not on `_pBackBuffer`, but on [WicSourceHandle](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/WriteableBitmap.cs#L1052) which was set to the same value as `_pBackBuffer` by `AcquireBackBuffer` when the first time buffer is acquired after previous commit.
And finally `UnsubscribeFromCommittingBatch` is called to prevent the commit of a locked bitmap.
Now, if we look at the `Unlock` function we can see the when the lock count go to zero in [Unlock](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/WriteableBitmap.cs#L339) function the commit event is subscribed again with ` SubscribeToCommittingBatch()`.
And the problem here is the racing for the buffer. I'm not sure which thread will be able to call `OnCommittingBatch` but I guess it's not only Main Thread, otherwise a block waiting for the lock on main thread will never success if the commit also runs on main thread. This might be wrong but even in that case, I assume here might still have racing problem. But let's be simple and think they are two threads.
In the main thread, I called `InvalidateVisual()` and it ends in someone called the `OnRender` function in my class, and I called `TryLock` for the first time after last commit.
`TryLock` returns without issue, and I can draw something, `Unlock`, and return without any exception.
The `CommittingBatchHandler` is called in any other thread (I **cannot** comfirm if this can happen, when I use dnspy to break on this function, it never hits. ) , but before it reaches `_pBackBuffer = null;`, that is [line 1386](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/WriteableBitmap.cs#L1386), I call trigger another `InvalidateVisual()` and eventually called `TryLock` again in the Main Thread.
In that thread, as `_lockCount ` is now zero, `AcquireBackBuffer` is called again for the buffer, but noticing that `_pBackBuffer ` is not null until line 1386 executed, this function returns true directly, without doing any event waiting or acquire another buffer.
After `AcquireBackBuffer` checks for the `_pBackBuffer`, the `CommittingBatchHandler` can continue execution and `_pBackBuffer` is set to null.
The `_pBackBuffer` is never used in `TryLock` and instead, `WicSourceHandle` is used as the handle to the Bitmap, `TryLock` can still lock the Bitmap using *correct* pointer rather than `null` even when the `CommittingBatchHandler` across line 1386.
Next, a `MILCMD_DOUBLEBUFFEREDBITMAP_COPYFORWARD` is sent to some thread (not sure which) and inside the [doublebufferedbitmap.cpp](https://github.com/dotnet/wpf/blob/89d172db0b7a192de720c6cfba5e28a1e7d46123/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/sw/swlib/doublebufferedbitmap.cpp#L453) this back buffer will be write locked.
And in the main thraed, the same buffer will be write locked and obvious here we have a racing.
In my case, I never seen exception on `OnCommittingBatch` though.
It might be wrong as I'm not sure how .net schedule the UI/Rendering threads, but there is such exception so I assume it's at least locked on two threads, and it's not me because I only lock on main thread.
Contributor guide
Assessment
This issue has not been assessed yet.