Skip to content

Input/Stylus: WPF WISP pen stack hangs indefinitely when stylus subsystem is accessed after disposal #11486

@etvorun

Description

@etvorun

[Input/Stylus] WPF WISP pen stack hangs indefinitely when stylus subsystem is accessed after disposal

Summary

When a WPF application shuts down and the dispatcher on the main thread has been deactivated, the WISP stylus subsystem can be triggered to create a new window (e.g., by a system event). This window creation path calls PenThreadWorker.WorkerGetTabletsInfo() on an already-disposed PenThreadWorker, causing the calling thread to hang indefinitely — the application process never exits cleanly.

A secondary issue causes PenThreadPool to return disposed PenThread objects as valid candidates, potentially prolonging or exacerbating the shutdown hang.

Observed behavior

  • Application process does not exit after UI is closed.
  • The hung thread is blocked in WaitHandle.WaitOne() with no timeout inside PenThreadWorker.WorkerGetTabletsInfo().
  • The application must be force-killed.

Repro notes

  1. Open a WPF application on a system with a WISP-compatible device (e.g., a display that reports display settings changes).
  2. Leave the application running for an extended period or trigger a display settings change event (e.g., reconnect a monitor/devbox session) while the application is shutting down.
  3. Close the application.
  4. Observe that the process does not terminate — it hangs indefinitely.

Note: exact repro timing depends on a race between dispatcher shutdown and a system event (e.g., WM_DISPLAYCHANGE) arriving during the shutdown window.

Impact

  • Affected WPF applications never exit cleanly; they must be force-terminated.
  • Users perceive the application as frozen after the UI disappears.
  • Reproducible under display settings change events that arrive during shutdown.

Expected behavior

When PenThreadWorker has been disposed, calls to WorkerGetTabletsInfo() should return an empty result immediately without blocking.

PenThreadPool should not return disposed PenThread objects; disposed entries should be pruned from the pool.

Actual behavior

WorkerGetTabletsInfo() unconditionally enqueues a work item and then calls WaitOne() with no timeout. If the pen thread has already exited because PenThreadWorker was disposed, no one will ever call Set() on the done event and the caller blocks forever.

PenThreadPool.GetPenThreadForPenContextHelper checks only whether a WeakReference<PenThread> is alive, but does not check whether the target PenThread is disposed. A disposed-but-still-referenced thread (kept alive by its registered PenContext / _handles array) is incorrectly returned as a usable candidate.

Suspected root cause

Gap 1 — WorkerGetTabletsInfo missing disposed guard

Every other Worker* method in PenThreadWorker (WorkerAddPenContext, WorkerRemovePenContext, WorkerCreateContext, WorkerAcquireTabletLocks, …) starts with a disposed check and returns a safe value when disposed. WorkerGetTabletsInfo was the sole exception.

When called on a disposed worker it:

  1. Enqueues a WorkerOperationGetTabletsInfo onto _workerOperation.
  2. Calls RaiseResetEvent on the PIMC reset handle.
  3. Calls getTablets.DoneEvent.WaitOne()with no timeout.

The pen thread's ThreadProc exits when __disposed is true, so nobody calls DoneEvent.Set(). The caller waits forever.

Gap 2 — PenThreadPool returns disposed PenThread

PenThreadPool.GetPenThreadForPenContextHelper selects a candidate by checking only whether the WeakReference<PenThread> is still alive. It does not verify that the PenThread itself is not disposed. A disposed, but still strongly-referenced, PenThread is incorrectly returned as valid.

Proposed fix direction

  • Add internal bool IsDisposed => __disposed; to PenThreadWorker to expose disposal state.
  • Add the matching internal bool IsDisposed => _penThreadWorker.IsDisposed; surface to PenThread.
  • Add a disposed guard at the top of PenThreadWorker.WorkerGetTabletsInfo() that returns Array.Empty<TabletDeviceInfo>() immediately when disposed (consistent with all other Worker* methods).
  • In PenThreadPool.GetPenThreadForPenContextHelper, extend weak-reference cleanup to also remove entries whose target PenThread is disposed, and skip disposed candidates during selection.
  • Make PenThread._penThreadWorker readonly (it is always assigned in the constructor and never reassigned) and remove the now-redundant null check in DisposeHelper.

Metadata

Metadata

Assignees

No one assigned

    Labels

    🚧 work in progressBugProduct bug (most likely)InvestigateRequires further investigation by the WPF team.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions