Skip to content

NVDA freezes for ~5 seconds when a typed character reaches a slow UIA provider #20654

Description

@Khalil220

Brief summary

speech.speakTypedCharacters() calls api.isTypingProtected() as its first statement, for
every typed character, before any configuration is consulted. That resolves
focusObject.isProtected, which requires self.states, and on every NVDAObject
implementation that means synchronous cross-process accessibility calls:

  • NVDAObjects.UIA.UIA._get_states_prefetchUIACacheForPropertyIDs
    IUIAutomationElement.buildUpdatedCache with the 15 IDs in _UIAStatesPropertyIDs
  • NVDAObjects.IAccessible.IAccessible._get_statesaccState plus accRole, and
    IA2States for IAccessible2 objects

Three consequences:

  1. It ignores configuration. The work happens with both "Speak typed characters" and
    "Speak typed words" set to off. Those settings are not read until roughly thirty lines
    after the call.
  2. It runs for characters NVDA will never speak. Ctrl+C produces 0x03 and Enter produces
    0x0D. Character echo is gated on ch >= FIRST_NONCONTROL_CHAR (a space), so both fail
    that test after the full cost has already been paid.
  3. If the provider is unresponsive, the core thread blocks. Measured at 5.035 s against a
    UIA provider, with the watchdog unable to cancel it.

The first two are a constant cost in every application and every toolkit. The third is the
user-visible failure, and is worst under UIA.

Steps to reproduce

  1. Set NVDA's logging level to "debug warning" (Preferences → Settings → Privacy and Security).
  2. Set both "Speak typed characters" and "Speak typed words" to off, to show that the settings
    are not involved.
  3. In Google Chrome, open the context menu on a link, arrow down to "Copy link address" and
    press Enter.
  4. Repeat during ordinary browsing.

The freeze is intermittent: the call is made every time, but only blocks when the focused
element's UIA provider happens to be slow to answer, either because its UI thread is busy or
because the process backing it is terminating.

The same freeze occurs on any UIA-backed focus object. Besides Chromium's own UI (context
menus, omnibox) I have seen it in Qt applications and in Unigram, which is XAML. Copying is a
common trigger simply because Ctrl+C and Enter both produce control characters; any typed
character reaches the same code.

Ctrl+C in browse mode does not reproduce it. There the gesture is bound to
script_copyToClipboard, so internal_keyDownEvent returns False and
winInputHook.keyboardHook returns 1, swallowing the key. The application never receives a
WM_CHAR, so no typedCharacter event is queued from either keyboardHandler or
nvdaControllerInternal_typedCharacterNotify.

Actual behavior

NVDA goes silent and unresponsive for approximately 5 seconds, then resumes.

In the capture below the unresponsive component was a Chromium renderer process, which the
browser isolates from its own UI — so Chrome appeared entirely normal while NVDA was stalled.
That is one way to end up with a provider that will not answer; a provider that is merely busy
produces the same result, which is consistent with the freeze also occurring in single-process
XAML and Qt applications.

Call path, verbatim from the watchdog's stack dump:

eventHandler.executeEvent
NVDAObjects.NVDAObject.event_typedCharacter
speech.speech.speakTypedCharacters          <- calls api.isTypingProtected() unconditionally
api.isTypingProtected
NVDAObjects.NVDAObject._get_isProtected     <- needs self.states
NVDAObjects.UIA.chromium._get_states
NVDAObjects.UIA.web._get_states
NVDAObjects.UIA._get_states
NVDAObjects.UIA._prefetchUIACacheForPropertyIDs
    -> self.UIAElement.buildUpdatedCache(cacheRequest)   <- blocks ~5s

Log, with the freeze bracketed by provider failures:

23:08:03.781  DEBUGWARNING  oleacc.AccessibleObjectFromEvent failed with [WinError -2147467259]
              Unspecified error. WinEvent: window 55708444 (Chrome_RenderWidgetHostHWND),
              objectID 1, childID 0, process 13648 (chrome)
23:08:06.610  INFO          Starting freeze recovery after 0.5001957999775186 seconds.
23:08:11.130  DEBUGWARNING  IUIAutomationElement.buildUpdatedCache failed given IDs of
              {30019, 30086, 30022, 30025, 30155, 30060, 30138, 30036, 30070, 30103,
               30008, 30009, 30010, 30046, 30079}
23:08:11.145  INFO          Recovered from freeze after 5.035338699934073 seconds.
23:08:12.421  DEBUGWARNING  accRole failed: (-2147023174, 'The RPC server is unavailable.')
23:08:12.421  DEBUGWARNING  COMError: (-2147418113, 'Catastrophic failure') in getNearestWindowHandle
23:08:12.423  DEBUGWARNING  IAccessibleObject.attributes COMError (-2147023174,
              'The RPC server is unavailable.')

The 15 property IDs in that failure match UIA._UIAStatesPropertyIDs exactly.

The watchdog could not cancel the call. Freeze recovery began at 0.5 s, after which
_recoverAttempt invoked CoCancelCall every RECOVER_ATTEMPT_INTERVAL (0.05 s) for the
remaining ~4.5 s — roughly 90 attempts — and buildUpdatedCache still ran to its own internal
timeout before failing. This bears on the assumption discussed in #6106, that UIA would manage
cancellation itself.

Expected behavior

  • A character NVDA will not speak should not cause a cross-process property fetch.
  • With typed character and typed word echo both disabled, no work should be done on behalf of
    those features.
  • Where the fetch is genuinely required, the watchdog should be able to abandon it rather than
    waiting out the provider's timeout.

NVDA logs, crash dumps and other attachments

The excerpts above are quoted from a debug-warning level log captured when the freeze occurred,
including the watchdog's full Python stack dump. Unfortunately the log file itself was not
retained — NVDA was restarted several times afterwards and nvda-old.log had rotated by the
time I went looking for it.

I am happy to attach a complete log if I can catch it again; the freeze is intermittent, so
this may take a while. No crash dump exists, since NVDA recovers rather than crashing.

Proposed fixes

I have both changes implemented and tested locally, and would like to submit them once this is
triaged.

1. Evaluate isTypingProtected() lazily in speakTypedCharacters. Toolkit-independent, and
the primary fix.

The value is needed in only three places: appending to _curWordChars (Unicode category
L/M/N), gating word echo, and realChar for character echo. Deferring evaluation to those
branches removes the call entirely for control characters with an empty word buffer, and
removes it for all characters when both echo settings are off.

Security semantics are preserved: the check still runs on every path that buffers or speaks a
character, and _get_isProtected remains sticky-once-true per #7908.

2. Route buildUpdatedCache through watchdog.cancellableExecute. Mitigates the UIA case
specifically.

On cancellation, fall through to the existing except COMError: return branch. This is the
treatment commit 961db0e66 (PR #20170, fixes #20169) applied to UiaHasServerSideProvider in
UIAHandler and to the Word object-model RPC. Note that #20170's commit message also lists the
UIA client element lookups in NVDAObjects.UIA, but its diff touches only winword.py,
UIAHandler/__init__.py and changes.md, so NVDAObjects/UIA/__init__.py was never covered —
buildUpdatedCache included. Routing it caps every other caller of
_prefetchUIACacheForPropertyIDs — notably event_gainFocus — at MIN_CORE_ALIVE_TIMEOUT.

Cost. This adds a thread handoff and a CoWaitForMultipleHandles wait to every prefetch, so
it makes the common case slightly more expensive in exchange for cancellability.
_prefetchUIACacheForPropertyIDs is a hot path, so this is exactly the trade-off
@michaelDCurran raised in #6106 about using the cancellable thread on frequently-called code.
Fix 1 helps by reducing how often the path is entered at all. I am happy to drop this fix, or
split it out, if that trade-off is judged the wrong way round.

Verified safe across apartments. CancellableCallThread is MTA while the core thread is an
STA, so this hands a raw IUIAutomationElement across an apartment boundary. I tested it
directly, reproducing NVDA's topology — client object created on a dedicated MTA thread,
element obtained on the STA, call made from a second MTA worker, core thread waiting via
CoWaitForMultipleHandles — and the call completes and returns valid cached data. It depends
on ccPumpMessages=True, the default; with a non-pumping wait it deadlocks.

Testing. Full unit suite run with and without both changes, like for like: 1379 tests
baseline, 1400 with the fixes, being exactly the 21 new tests, with identical failures, errors
and skips either side. ruff check counts unchanged, ruff format --check clean. Verified
against a release=1 build.

Notes

System configuration

  • NVDA type: installed copy
  • NVDA version: 2026.1
  • Other NVDA versions tried: not bisected. My impression is that this became noticeable
    around 2025.1, but I have not verified that, and the code path is older: the unconditional
    isTypingProtected() call appears as unchanged context in the Add "Only in edit controls" mode for typing echo #17505 diff (605db3e42,
    January 2025), _prefetchUIACacheForPropertyIDs and _UIAStatesPropertyIDs date from
    fa768843f (2017), and NVDAObjects/UIA/chromium.py from 2021. Since the freeze also
    depends on provider responsiveness, the onset may reflect application or platform changes
    rather than an NVDA regression. Happy to bisect if that would help.
  • Windows version: Windows 11 Pro 10.0.26200
  • Add-ons: reproduced with all add-ons disabled.
  • Affected applications: Google Chrome, Unigram, and Qt applications. I have not pinned
    versions, since the defect is in NVDA's own code and is reachable from any UIA-backed focus
    object, but I can supply them if that would help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions