Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Case of the Wrong Window Owner

DZone's Guide to

The Case of the Wrong Window Owner

·
Free Resource

This post was inspired by a debugging war story Dima Zurbalev told me a couple of days ago. All the credit for finding the bug and describing the diagnostic process belongs to Dima. (This isn’t the first time—see Garbage Collection Thread Suspension Delay and Improving Cold Startup Performance for two additional examples of Dima’s work.)

Anyway, the case in point is a WinForms application that stops responding (I reproduced this in a simple scenario, but originally the problem was obscured by additional details). After attaching WinDbg to the process and loading SOS, the managed thread inspection shows three threads:

0:004> !Threads
[…edited for brevity…]
   0    1  528 STA
   2    2  f14 MTA (Finalizer)
   4    3 1f74 MTA (Threadpool Worker)

And the only thread with a current managed call stack is the main thread:

0:004> ~0s; !CLRStack
[…edited for brevity…]
OS Thread Id: 0x528 (0)
ESP       EIP    
0024e7f0 771b723b [NDirectMethodFrameStandalone: 0024e7f0] System.Windows.Forms.SafeNativeMethods.GetWindowTextLength(System.Runtime.InteropServices.HandleRef)
0024e808 7089cb58 System.Windows.Forms.Control.get_WindowText()
0024e838 7089c8dc System.Windows.Forms.Form.get_WindowText()
0024e844 7089c82b System.Windows.Forms.Control.get_Text()
0024e850 7089c7f5 System.Windows.Forms.Form.get_Text()
0024e854 7089de91 System.Windows.Forms.Control.set_Text(System.String)
0024e868 7089de55 System.Windows.Forms.Form.set_Text(System.String)
0024e86c 00380522 MyNiceApp.MyMainForm.btnDoIt_Click(System.Object, System.EventArgs)
[…snipped…]
0024ece8 70865911 System.Windows.Forms.Application.Run(System.Windows.Forms.Form)
0024ecfc 003800ae MyNiceApp.Program.Main()
0024ef20 72891b5c [GCFrame: 0024ef20]

From this call stack, it seems that the main thread is stuck in Control.get_WindowText(), an operation that is not supposed to take a significant amount of time. The native method on the top of the call stack is:

0:000> kb
ChildEBP RetAddr  Args to Child             
0024e754 771c1c01 00060a7c 0000000e 00000000 USER32!NtUserMessageCall+0x15
0024e794 771dffd8 0104b3f0 00000000 00570ce2 USER32!SendMessageWorker+0x5e9
0024e7bc 0027baf6 00060a7c 80b49efb 00000000 USER32!GetWindowTextLengthW+0x40
WARNING: Frame IP not in any known module. Following frames may be wrong.
0024e7d8 7089cb58 02662088 026621d8 0266e2f0 0x27baf6

…all righty, an attempt to send a message to another window to retrieve its text. The window handle is the first parameter of GetWindowTextLengthW, and it’s 00060a7c. Now the question shifts to that window—which thread owns that window? This thread is supposed to answer window messages directed to that window.

Enter our good friend Spy++ that still ships with Visual Studio. In Spy++, a window handle is sufficient to locate the actual window as well as the thread that owns it.

image

Here’s the find result:

image

And the thread that owns the window:

image

Let’s see. The thread ID looks familiar. This is one of the threads from our application!

0:004> !Threads
[…edited for brevity…]
   0    1  528 STA
   2    2  f14 MTA (Finalizer)
   4    3 1f74 MTA (Threadpool Worker)

Oh my. The thread pool thread owns this window! What is the thread pool thread doing right now?

0:000> ~4s; k
[…edited for brevity…]
ChildEBP RetAddr 
05cff614 759f0816 ntdll!ZwWaitForSingleObject+0x15
05cff680 77501184 KERNELBASE!WaitForSingleObjectEx+0x98
05cff698 728ac661 KERNEL32!WaitForSingleObjectExImplementation+0x75
05cff6dc 728ac597 mscorwks!PEImage::LoadImage+0x1af
05cff72c 728ac5b6 mscorwks!CLREvent::WaitEx+0x117
05cff740 729ee03f mscorwks!CLREvent::Wait+0x17
05cff7c0 729ee4cd mscorwks!ThreadpoolMgr::SafeWait+0x73
05cff824 729d1ec9 mscorwks!ThreadpoolMgr::WorkerThreadStart+0x11c
05cff948 77503677 mscorwks!Thread::intermediateThreadProc+0x49
05cff954 77a59d42 KERNEL32!BaseThreadInitThunk+0xe
05cff994 77a59d15 ntdll!__RtlUserThreadStart+0x70
05cff9ac 00000000 ntdll!_RtlUserThreadStart+0x1b

It seems to be waiting—it’s a thread pool thread, after all, and it doesn’t have any work to do right now. One thing this thread is not doing, however, is pumping window messages—and this is why our attempt to retrieve the window’s text is failing. There is simply no one listening on the other side of the SendMessage call.

Clearly, creating a window from a thread pool thread is a bad idea. But how do you detect this kind of thing? If this were a native Win32 application, we could use Application Verifier. One of the stops under the Threadpool category is “Unclosed window belonged to the current thread”. This is so cool that it’s worth a demo of its own. After properly configuring Application Verifier…

image

…you would get the following in a debugger if a thread pool thread orphans a window:

=======================================
VERIFIER STOP 00000703: pid 0x1680: Unclosed window belonged to the current thread.

    012B132F : Callback function.
    00000066 : Context.
    03F98A58 : Threadpool Object allocation stack trace, use dps to dump it.
    00000000 : Not Used.

threadpool thread (98) having executed Callback (012B132F) has valid hwnd (70fc4: #32770) which could receive messages
=======================================
This verifier stop is continuable.
After debugging it use `go' to continue.
=======================================

(1680.98): Break instruction exception - code 80000003 (first chance)
[…snipped…]
vrfcore!VerifierStopMessageEx+0x4ca:
69853b61 cc              int     3

0:001> ln 012B132F
(012b132f)   AnMFCApp!ILT+810(?MyCallbackYGKPAXZ)

0:001> dps 03F98A58
03f98a58  629eb309 vfbasics!AVrfpRtlQueueWorkItem+0x2c
03f98a5c  7751ce2a kernel32!QueueUserWorkItem+0x14
03f98a60  012b2ec9 AnMFCApp!CAnMFCAppDlg::OnBnClickedOk+0x39
03f98a64  7868f632 mfc100ud!_AfxDispatchCmdMsg+0xb2 03f98a68  7868fd7a mfc100ud!CCmdTarget::OnCmdMsg+0x2ea 03f98a6c  786e9ac3 mfc100ud!CDialog::OnCmdMsg+0x23 03f98a70  787c6c54 mfc100ud!CWnd::OnCommand+0x174
03f98a74  7844977a mfc100ud!CDialogEx::OnCommand+0x3a
[…snipped…]

Note that the above output also includes the call stack that created the work item that resulted in orphaning the window!

However, in this case we’re dealing with a managed thread pool thread, and the managed thread pool doesn’t use the Win32 thread pool. Therefore, we have to resort to the old method of setting a thread-dependent breakpoint in CreateWindowExW or one of the managed APIs (e.g., Control..ctor). This would give us the exact location in code that creates a window from a thread pool thread, and is left as an exercise to the reader.

Another variety of the same problem (which mimics even more closely the original bug Dima encountered) is a WinForms-initiated cross-thread call from Control.Invoke. In this case, the managed call stack of the (stuck) GUI thread would be similar to the following:

0:000> !CLRStack
OS Thread Id: 0x1e78 (0)
ESP       EIP    
002eec74 77a400ed [HelperMethodFrame_1OBJ: 002eec74] System.Threading.WaitHandle.WaitOneNative(Microsoft.Win32.SafeHandles.SafeWaitHandle, UInt32, Boolean, Boolean)
002eed20 71ea685f System.Threading.WaitHandle.WaitOne(Int64, Boolean)
002eed3c 71ea6815 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
002eed50 70dc1a51 System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
002eed68 708a4a20 System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
002eee0c 70dc33d0 System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
002eee40 70dc3367 System.Windows.Forms.Control.Invoke(System.Delegate)
002eee44 00660595 MyNiceApp.MyMainForm.btnDoIt_Click(System.Object, System.EventArgs)
[…snipped…]
002ef2ec 006600ae MyNiceApp.Program.Main()
002ef514 72891b5c [GCFrame: 002ef514]

As you see, the implementation of Control.Invoke waits for an event. This event, as it is evident from the method’s implementation, is signaled when the target of the Invoke call completes the invocation on another thread. However, it is now more difficult to determine which window we are trying to call into and which thread owns it, as there is no SendMessage on the unmanaged call stack.

Fortunately, we have the source code of the MyNiceApp.MyMainForm.btnDoIt_Click method:

private void btnDoIt_Click(object sender, EventArgs e)
{
    if (_theNewForm.InvokeRequired)
    {
        _theNewForm.Invoke((Action)(() => 
        {
            _theNewForm.Text = "Hello World";
        }));
    }
    else
    {
        _theNewForm.Text = "Hello World";
    }
}

OK, so it’s making the Invoke call on the _theNewForm member. Now we need the this parameter of the btnDoIt_Click method, and !CLRStack –a gives it to us:

0:000> !CLRStack -a
OS Thread Id: 0x1e78 (0)
[…snipped…]
002eee44 00660595 MyNiceApp.MyMainForm.btnDoIt_Click(System.Object, System.EventArgs)
    PARAMETERS:
        this = 0x0261cd7c
        sender = 0x0263a224
        e = 0x0264de00
    LOCALS:
        0x002eee50 = 0x0264fda4
        0x002eee5c = 0x00000000

And now let’s take a look at the _theNewForm field:

0:000> .load c:\temp\psscor2\x86\psscor2

0:000> !DumpField -field _theNewForm 0x0261cd7c
Name: System.Windows.Forms.Form
MethodTable: 708ed878
EEClass: 706acb84
Size: 320(0x140) bytes
GC Generation: 0
Fields:
[…edited for brevity…]
4        System.Object  0 instance 00000000 __identity
8 ...ponentModel.ISite  0 instance 00000000 site
c ....EventHandlerList  0 instance 02642f0c events
108        System.Object  0   static 00000000 EventDisposed
10 ...ntrolNativeWindow  0 instance 026421d8 window
[…snipped…]


0:000> !do 026421d8
Name: System.Windows.Forms.Control+ControlNativeWindow
MethodTable: 708f0e14
EEClass: 706d62b8
Size: 56(0x38) bytes
GC Generation: 0
Fields:
[…edited for brevity…]
4        System.Object  0 instance 00000000 __identity
18        System.IntPtr  1 instance   723580 handle
8 ...veMethods+WndProc  0 instance 02642f5c windowProc
1c        System.IntPtr  1 instance  5180642 windowProcPtr
[…snipped…]

Now we have the window handle—723580–which we can inspect with Spy++ to find the owning thread.

Topics:

Published at DZone with permission of Sasha Goldshtein, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}