You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_states → accState plus accRole, and IA2States for IAccessible2 objects
Three consequences:
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.
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.
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
Set NVDA's logging level to "debug warning" (Preferences → Settings → Privacy and Security).
Set both "Speak typed characters" and "Speak typed words" to off, to show that the settings
are not involved.
In Google Chrome, open the context menu on a link, arrow down to "Copy link address" and
press Enter.
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:
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.
Complements Reduce cross-process UIA calls by making better use of UIA caching #20608, which reduced cross-process UIA calls by caching and batching more
aggressively. This is the same concern from the other end: the cheapest cross-process call
is the one that is not made at all, and for a control character with both echo settings
disabled this one need never happen.
Whether the IAccessible path can block the same way is untested. accState is a standard
COM call and should be cancellable by CoCancelCall, unlike buildUpdatedCache, but I have
not verified that.
Scope of fix 1: isFocusEditable(), added by Add "Only in edit controls" mode for typing echo #17505, also reads obj.states, and is called
whenever echo is set to "Only in edit controls" — the default for typed characters since
2025.1. This is not an extra cross-process call, since states is cached per core cycle
(NVDAObject.cachePropertiesByDefault), but it does mean a printable character on the
default setting still triggers the fetch, via isFocusEditable() rather than isTypingProtected(). Fix 1 therefore removes the fetch outright for control characters in
any configuration, and for printable characters only when echo is off.
The freeze has been seen across three independent UIA-backed toolkits — Chromium browser
chrome, Qt, and XAML/WinUI — which points at the UIA client call rather than at any one
application's implementation.
Suggested priority: P2 per projectDocs/issues/triage.md — a freeze affecting a subset of
users, intermittent and dependent on provider state. The unconditional-work aspect is a
separate, universal performance concern.
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.
Brief summary
speech.speakTypedCharacters()callsapi.isTypingProtected()as its first statement, forevery typed character, before any configuration is consulted. That resolves
focusObject.isProtected, which requiresself.states, and on everyNVDAObjectimplementation that means synchronous cross-process accessibility calls:
NVDAObjects.UIA.UIA._get_states→_prefetchUIACacheForPropertyIDs→IUIAutomationElement.buildUpdatedCachewith the 15 IDs in_UIAStatesPropertyIDsNVDAObjects.IAccessible.IAccessible._get_states→accStateplusaccRole, andIA2Statesfor IAccessible2 objectsThree consequences:
"Speak typed words" set to off. Those settings are not read until roughly thirty lines
after the call.
0x03and Enter produces0x0D. Character echo is gated onch >= FIRST_NONCONTROL_CHAR(a space), so both failthat test after the full cost has already been paid.
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
are not involved.
press Enter.
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, sointernal_keyDownEventreturnsFalseandwinInputHook.keyboardHookreturns1, swallowing the key. The application never receives aWM_CHAR, so notypedCharacterevent is queued from eitherkeyboardHandlerornvdaControllerInternal_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:
Log, with the freeze bracketed by provider failures:
The 15 property IDs in that failure match
UIA._UIAStatesPropertyIDsexactly.The watchdog could not cancel the call. Freeze recovery began at 0.5 s, after which
_recoverAttemptinvokedCoCancelCalleveryRECOVER_ATTEMPT_INTERVAL(0.05 s) for theremaining ~4.5 s — roughly 90 attempts — and
buildUpdatedCachestill ran to its own internaltimeout before failing. This bears on the assumption discussed in #6106, that UIA would manage
cancellation itself.
Expected behavior
those features.
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.loghad rotated by thetime 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 inspeakTypedCharacters. Toolkit-independent, andthe primary fix.
The value is needed in only three places: appending to
_curWordChars(Unicode categoryL/M/N), gating word echo, and
realCharfor character echo. Deferring evaluation to thosebranches 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_isProtectedremains sticky-once-true per #7908.2. Route
buildUpdatedCachethroughwatchdog.cancellableExecute. Mitigates the UIA casespecifically.
On cancellation, fall through to the existing
except COMError: returnbranch. This is thetreatment commit
961db0e66(PR #20170, fixes #20169) applied toUiaHasServerSideProviderinUIAHandlerand to the Word object-model RPC. Note that #20170's commit message also lists theUIA client element lookups in
NVDAObjects.UIA, but its diff touches onlywinword.py,UIAHandler/__init__.pyandchanges.md, soNVDAObjects/UIA/__init__.pywas never covered —buildUpdatedCacheincluded. Routing it caps every other caller of_prefetchUIACacheForPropertyIDs— notablyevent_gainFocus— atMIN_CORE_ALIVE_TIMEOUT.Cost. This adds a thread handoff and a
CoWaitForMultipleHandleswait to every prefetch, soit makes the common case slightly more expensive in exchange for cancellability.
_prefetchUIACacheForPropertyIDsis 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.
CancellableCallThreadis MTA while the core thread is anSTA, so this hands a raw
IUIAutomationElementacross an apartment boundary. I tested itdirectly, 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 dependson
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 checkcounts unchanged,ruff format --checkclean. Verifiedagainst a
release=1build.Notes
unambiguous. Everything was read against
masteras of Reduce cross-process UIA calls by making better use of UIA caching #20608 (5 August 2026), which iswhat a fix would target, and observed on 2026.1. The code is materially the same in both,
except that the
event_gainFocusprefetch mentioned under fix 2 was added by Reduce cross-process UIA calls by making better use of UIA caching #20608 and isnot in 2026.1.
isTypingProtected,speakTypedCharacters,buildUpdatedCache, andisProtectedperformance.aggressively. This is the same concern from the other end: the cheapest cross-process call
is the one that is not made at all, and for a control character with both echo settings
disabled this one need never happen.
IAccessiblepath can block the same way is untested.accStateis a standardCOM call and should be cancellable by
CoCancelCall, unlikebuildUpdatedCache, but I havenot verified that.
isFocusEditable(), added by Add "Only in edit controls" mode for typing echo #17505, also readsobj.states, and is calledwhenever echo is set to "Only in edit controls" — the default for typed characters since
2025.1. This is not an extra cross-process call, since
statesis cached per core cycle(
NVDAObject.cachePropertiesByDefault), but it does mean a printable character on thedefault setting still triggers the fetch, via
isFocusEditable()rather thanisTypingProtected(). Fix 1 therefore removes the fetch outright for control characters inany configuration, and for printable characters only when echo is off.
chrome, Qt, and XAML/WinUI — which points at the UIA client call rather than at any one
application's implementation.
projectDocs/issues/triage.md— a freeze affecting a subset ofusers, intermittent and dependent on provider state. The unconditional-work aspect is a
separate, universal performance concern.
System configuration
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),
_prefetchUIACacheForPropertyIDsand_UIAStatesPropertyIDsdate fromfa768843f(2017), andNVDAObjects/UIA/chromium.pyfrom 2021. Since the freeze alsodepends on provider responsiveness, the onset may reflect application or platform changes
rather than an NVDA regression. Happy to bisect if that would help.
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.