Skip to content

fix(windows-ime): eliminate thread deadlock causing AppHangB1 in host processes#745

Open
scientificmonster wants to merge 2 commits into
Open-Less:betafrom
scientificmonster:fix/ime-thread-deadlock
Open

fix(windows-ime): eliminate thread deadlock causing AppHangB1 in host processes#745
scientificmonster wants to merge 2 commits into
Open-Less:betafrom
scientificmonster:fix/ime-thread-deadlock

Conversation

@scientificmonster

@scientificmonster scientificmonster commented Jun 24, 2026

Copy link
Copy Markdown

User description

Summary

Fixes #665 — the intermittent AppHangB1 in explorer.exe, chrome.exe, clash-verge.exe and other host processes caused by OpenLessIme.dll.

Root Cause

OpenLessPipeServer::Stop() calls thread_.join(), which blocks the calling thread (often the TSF owner/UI thread). Meanwhile, the pipe server thread can be in SubmitTextFromPipe()SendMessageTimeoutW(), which blocks waiting for the owner thread's message pump. Classic circular wait deadlock lasting up to 2 seconds (the SendMessageTimeoutW timeout), causing the host process to appear hung. Windows detects this as AppHangB1 and may restart the process.

Fix

Replace SendMessageTimeoutW with PostMessageW + WaitForMultipleObjects in the cross-thread path of SubmitTextFromPipe. A shutdown_event (manual-reset event owned by OpenLessPipeServer) is added to the wait array. When Stop() is called, it signals the shutdown event before calling thread_.join(), unblocking the pipe thread immediately.

Changes (4 files, +69/-14)

File Change
ipc_client.h Add HANDLE shutdown_event_ member
ipc_client.cpp Create/destroy event; signal in Stop() before join; pass to SubmitTextFromPipe
text_service.h Add HANDLE shutdown_event = nullptr parameter
text_service.cpp PostMessageW + WaitForMultipleObjects replaces SendMessageTimeoutW; MessageWindowProc signals completion event

Architecture

Before (deadlock):
  pipe thread: HandleSubmitLine → SubmitTextFromPipe → SendMessageTimeoutW [blocked]
  owner thread: Deactivate → Stop → thread_.join() [blocked waiting for pipe thread]
  → DEADLOCK until SendMessageTimeoutW times out (2s)

After (fixed):
  pipe thread: HandleSubmitLine → SubmitTextFromPipe → PostMessage → WaitForMultipleObjects [blocked]
  owner thread: Deactivate → Stop → SetEvent(shutdown) → thread_.join() [unblocked]
  → pipe thread wakes from WaitForMultipleObjects, loop exits, join succeeds immediately

Test Plan

  • Build OpenLessIme.dll with Visual Studio (open OpenLessIme.sln, build Release x64 + x86)
  • Replace installed DLLs at the OpenLess installation directory under windows-ime\x64\ and x86\
  • Enable OpenLess Voice Input TSF (HKLM\SOFTWARE\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Enable = 1)
  • Use OpenLess voice input normally for 24+ hours
  • Verify zero new AppHangB1 events in Event Viewer
  • Verify OpenLessIme.dll is loaded in Explorer but no hangs occur

Environment Confirmed Affected

  • Windows 10 19045 + Windows 11 26100
  • OpenLess v1.3.8 through v1.3.10
  • Host processes: explorer.exe, chrome.exe, clash-verge.exe

PR Type

Bug fix


Description

  • Replace SendMessageTimeoutW with PostMessageW + WaitForMultipleObjects to prevent deadlock

  • Introduce shutdown_event to unblock pipe thread during Stop() before join

  • Heap-allocate SubmitTextRequest with atomic ref-count to avoid use-after-free

  • Drain pending kSubmitTextMessage in DestroyMessageWindow for clean shutdown


Diagram Walkthrough

flowchart LR
  A["Pipe Thread"]
  B["Message Window"]
  C["Completion Event"]
  D["Shutdown Event"]
  E["Stop()"]
  A -- "PostMessageW" --> B
  A -- "WaitForMultipleObjects" --> C
  A -- "WaitForMultipleObjects" --> D
  B -- "SetEvent" --> C
  E -- "SetEvent" --> D
Loading

File Walkthrough

Relevant files
Bug fix
ipc_client.cpp
Manage shutdown event lifecycle and signal in Stop()         

openless-all/app/windows-ime/src/ipc_client.cpp

  • Create/destroy shutdown_event_ in constructor/destructor
  • Signal event in Stop() before thread_.join() to unblock pipe thread
  • Pass shutdown_event_ to SubmitTextFromPipe for the wait array
+22/-2   
text_service.cpp
Eliminate deadlock with event-driven communication             

openless-all/app/windows-ime/src/text_service.cpp

  • Replace SendMessageTimeoutW with PostMessageW + WaitForMultipleObjects
  • Heap-allocate SubmitTextRequest with atomic ref-count to prevent
    use-after-free
  • Drain pending kSubmitTextMessage in DestroyMessageWindow for cleanup
+90/-19 
Enhancement
ipc_client.h
Add shutdown event handle member                                                 

openless-all/app/windows-ime/src/ipc_client.h

  • Add HANDLE shutdown_event_ member variable
+1/-0     
text_service.h
Update method signature to accept shutdown event                 

openless-all/app/windows-ime/src/text_service.h

  • Add HANDLE shutdown_event parameter to SubmitTextFromPipe
+2/-1     

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 03c7ef7)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

665 - PR Code Verified

Compliant requirements:

  • The deadlock scenario (pipe thread blocked on SendMessageTimeoutW while owner thread calls join) is replaced with a non-blocking PostMessage + WaitForMultipleObjects with a shutdown event, breaking the circular wait.
  • Stop() signals shutdown_event before join so the pipe thread wakes immediately.

Requires further human verification:

  • Actual verification on Windows 11 that the deadlock no longer occurs under the conditions described in the ticket (explorer.exe hangs with OpenLessIme.dll loaded).
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Potential robustness: unhandled CreateEvent failure

If CreateEventW in the constructor fails (returning nullptr), shutdown_event_ remains nullptr. All uses check for nullptr, so the event becomes optional. However, in Stop(), the shutdown event is not signaled, causing the pipe thread to wait up to 2 seconds on the completion_event alone (which never gets set because the owner thread is blocked in join). This reintroduces a 2-second hang per stop, close to the original symptom. While unlikely, handling this case (e.g., by logging or early exit) would improve robustness.

OpenLessPipeServer::OpenLessPipeServer()
    : shutdown_event_(CreateEventW(nullptr, TRUE, FALSE, nullptr)) {}

…rocesses

Replace SendMessageTimeoutW with PostMessage + WaitForMultipleObjects
in SubmitTextFromPipe. The old code caused a deadlock when Deactivate()
(called by TSF on the owner thread) blocked on thread_.join() while the
pipe thread was blocked on SendMessageTimeoutW waiting for the owner
thread's message pump.

The fix introduces a shutdown_event that Stop() signals before joining,
so the pipe thread can be unblocked from its wait even if the owner
thread is blocked. This prevents the 2-second deadlock that caused
AppHangB1 in Explorer, Chrome, Clash Verge, and other host processes.

Fixes: Open-Less#665
@scientificmonster scientificmonster force-pushed the fix/ime-thread-deadlock branch from 7b7273b to 1ae4876 Compare June 24, 2026 14:31
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 1ae4876

@appergb appergb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issue: SubmitTextFromPipe now posts a pointer to a stack-allocated SubmitTextRequest via PostMessageW. If WaitForMultipleObjects returns because of timeout or shutdown_event, the function closes completion_event and returns while the owner thread may still later process kSubmitTextMessage, dereference the stale stack pointer, and call SetEvent on a closed handle. The request lifetime needs to outlive the posted message, or the message must be cancelled/acknowledged before returning. Full platform CI is also missing; currently only PR-Agent has run.

Heap-allocate SubmitTextRequest with atomic ref-counting (ref_count=2:
pipe thread + queued message).  Store string copies instead of pointers
so the request's data survives HandleSubmitLine's return when the wait
times out.  Drain pending kSubmitTextMessage in DestroyMessageWindow to
release any reference left by a shutdown-race message.

The pipe thread releases its reference after WaitForMultipleObjects;
the message thread releases its reference after processing (or the
PeekMessage drain during teardown).  Whichever releases last closes
the event handle and deletes the request — no dangling pointer, no
stale SetEvent.
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 03c7ef7

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[windows][ime] OpenLessIme.dll causes intermittent explorer.exe/taskbar AppHang until OpenLess Voice Input TSF is disabled

2 participants