Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible Racing and multithreading problem on WritableBitmap.TryLock #5489

Open
Yangff opened this issue Oct 12, 2021 · 2 comments
Open

Possible Racing and multithreading problem on WritableBitmap.TryLock #5489

Yangff opened this issue Oct 12, 2021 · 2 comments

Comments

@Yangff
Copy link

Yangff commented Oct 12, 2021

  • .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.

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 it checks if _lockCount == 0 and then process to AcquireBackBuffer.

In the AcquireBackBuffer function _pBackBuffer 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 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 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, 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 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.

@singhashish-wpf
Copy link
Member

@Yangff Could you provide a minimal repro/sample app, so that we can understand how is it being implemented?

@Yangff
Copy link
Author

Yangff commented Oct 12, 2021

@Yangff Could you provide a minimal repro/sample app, so that we can understand how is it being implemented?

Yes, I isolated the code that causes this problem.
https://gist.github.com/Yangff/34b68eb382c15b8dd4e196a57f3f8cc4#file-spectrum-cs-L91

I take a look at the threads in WinDbg.. it seems more complex than the racing I thought as I cannot find another thread that hold this lock...

I also attached the dump here so you won't need to retry many times to get it throw the exception.

https://1drv.ms/u/s!AvYZqW_lMM53zBpFPXvLW74ONL_l?e=cQPyMU

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

No branches or pull requests

3 participants