December 23rd, 2022

The case of the recursively hung WM_DRAW­CLIPBOARD message

An application hang report showed that the application was stuck in this stack:

win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserMessageCall+0x14
user32!SendMessageWorker+0x823
user32!SendMessageW+0xda
contoso!CContosoWindow::WndProc+0xa5d
user32!UserCallWinProcCheckWow+0x2f8
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
user32!_InternalCallWinProc+0x2a
user32!InternalCallWinProc+0x1b
user32!DispatchClientMessage+0xea
user32!__fnDWORD+0x3f
ntdll!KiUserCallbackDispatcher+0x4c
win32u!NtUserGetMessage+0xc
user32!GetMessageW+0x30
contoso!WindowThreadProc+0x9b
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21

Inspecting the local variables at each recursive call shows that the message is always WM_DRAW­CLIPBOARD. The Contoso window receives the WM_DRAW­CLIPBOARD message, does its work, and then forwards the message to the next clipboard viewer window, just like the book says. While waiting for that window to respond, another WM_DRAW­CLIPBOARD message arrives, and the cycle repeats.

The clipboard viewer chain is a linked list of windows that have all subscribed to clipboard notifications. This linked list is managed cooperatively: When you add yourself to the chain, you are given the handle of the previous head of the chain. And when you finish dealing with a clipboard notification, you forward the notification to the next window in the chain. That way, all the windows in the chain eventually learn about the clipboard.

The clipboard viewer chain was developed back in the days of 16-bit Windows, when all programs were cooperatively multi-tasked and generally were trusted to behave properly. The clipboard viewer chain used the same trick that window hooks used to save space: It externalized the cost.

Here’s a sketch of how the clipboard viewer chain worked in 16-bit Windows:

HWND hwndClipboardViewer;

HWND SetClipboardViewer(HWND hwndNewViewer)
{
  HWND hwndOldViewer = hwndClipboardViewer;
  hwndClipboardViewer = hwndNewViewer;
  return hwndOldViewer;
}

HWND GetClipboardViewer()
{
  return hwndClipboardViewer;
}

HWND ChangeClipboardChain(HWND hwndRemove, HWND hwndNewNext)
{
  if (hwndClipboardViewer == hwndRemove) {
    hwndClipboardViewer = hwndNewNext;
  } else {
    SendMessage(hwndClipboardViewer, WM_CHANGECBCHAIN,
        (WPARAM)hwndRemove, (LPARAM)hwndNewNext);
  }
}

void NotifyClipboardViewers()
{
  if (hwndClipboardViewer) {
    SendMessage(hwndClipboardViewer, WM_DRAWCLIPBOARD, 0, 0);
  }
}

And that’s it! The entire clipboard viewer feature in 30 lines of code.

Okay, so back to our customer’s problem.

The window registered itself as a clipboard viewer, and the clipboard contents changed, causing it to receive a WM_DRAW­CLIPBOARD message. The window dealt with the clipboard change, and then dutifully called Send­Message to forward the WM_DRAW­CLIPBOARD message down the chain. Every window in the chain deals with the message, and then calls Send­Message.

What happened here is that some window in the chain is hung, and that causes all the other windows in the chain to hang, since they are all blocked on each other via Send­Message:

Window 1
SendMessage
Window 2
SendMessage
Window 3
SendMessage
Window 4 hung

In order for Window 1’s Send­Message to complete, Window 2 needs to return. But Window 2 is stuck in a Send­Message to Window 3, which is in turn stock in a Send­Message to Window 4, which is hung. That one hung window has caused a chain of windows to stop responding.

The Contoso window got caught in the chain of windows that are all waiting for that other hung window to process the WM_DRAW­CLIPBOARD message.

So what can Contoso do about this?

The best solution is to leave the game. Instead of using the old and busted clipboard viewer chain, use the new hotness Add­Clipboard­Format­Listener function to register to be notified when the clipboard contents change and escape the clipboard viewer chain.

Fortunately, converting from a clipboard viewer to a clipboard format listener is fairly simple and even involves deleting some code, so that’s a nice bonus.

  • Change Set­ClipboardViewer to Add­Clipboard­Format­Listener.
  • Delete the variable that held the previous clipboard viewer.
  • Delete the code that handled the WM_CHANGE­CB­CHAIN message.
  • Change case WM_DRAWCLIPBOARD to case WM_CLIPBOARDUPDATE.
  • Delete the Send­Message(hwndNextViewer, WM_DRAWCLIPBOARD, wParam, lParam).
  • Change Change­Clipboard­Chain to Remove­Clipboard­Format­Listener.

If for some reason you really want to be a clipboard viewer, you can at least switch to using Send­Notify­Message to forward the WM_DRAW­CLIPBOARD message to the next window in the chain. The Send­Notify­Message function is like Send­Message except that it doesn’t want for the recipient to return. It’s a fire-and-forget Send­Message.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮

    It’s the same idea Raymond used in his “Creating a co_await awaitable signal that can be awaited multiple times, part 4”.

  • Neil Rashbrook

    I thought PostMessage was the real fire-and-forget version, although there are some subtle differences e.g. posted messages get handled by the message pump.

    • Chris Iverson

      As far as I can tell, PostMessage is ALWAYS fire-and-forget.

      SendNotifyMessage behaves like SendMessage if the message is sent to a window belonging to the calling thread. It behaves (somewhat) like PostMessage if it’s a window belonging to a different thread.

      I say “somewhat” because I believe the window procedure is still called right away, instead of putting a message in the queue, but it won’t wait for a response before returning to the caller.

  • Yuhong Bao

    I wonder why did NT still use SendMessage instead of SendNotifyMessage for ChangeClipboardChain