Skip to content

fix(pointer): crash (0xc0000005) when the pointer-capturing component has been unmounted#16334

Open
FaithfulAudio wants to merge 2 commits into
microsoft:0.83-stablefrom
FacilitronWorks:fix/pointer-capture-null-guard
Open

fix(pointer): crash (0xc0000005) when the pointer-capturing component has been unmounted#16334
FaithfulAudio wants to merge 2 commits into
microsoft:0.83-stablefrom
FacilitronWorks:fix/pointer-capture-null-guard

Conversation

@FaithfulAudio

@FaithfulAudio FaithfulAudio commented Jul 26, 2026

Copy link
Copy Markdown

fix(pointer): crash (0xc0000005) when the pointer-capturing component has been unmounted

Problem

A Fabric/Composition app crashes with an access violation (0xc0000005) during ordinary
list interaction on a touch device. Reproduced on a production RNW 0.83.2 new-arch app
(Facilitron FIT) by panning a virtualized inspection list: scroll the list, let
virtualization recycle the row that currently holds pointer capture, then press or release
again. The process dies rather than throwing.

Root cause

Both capture paths in CompositionEventHandler look the capturing component up by its
cached tag and then dereference the result without checking it:

auto targetComponentView =
    fabricuiManager->GetViewRegistry().componentViewDescriptorWithTag(m_pointerCapturingComponentTag).view;

winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(targetComponentView)
    ->OnPointerCaptureLost();

m_pointerCapturingComponentTag can outlive the component it names. If the capturing
component is unmounted without releasing capture — exactly what list/ScrollView
virtualization does when it recycles a row mid-pan — componentViewDescriptorWithTag
returns a descriptor whose .view is null. winrt::get_self on a null projected
reference yields a null pointer, and the ->OnPointerCaptureLost() call dereferences it.

The same unguarded pattern appears twice:

  • CompositionEventHandler::CapturePointer — when a new capture displaces a previous one
  • CompositionEventHandler::releasePointerCapture — when capture is released

This is independent of any pointer-routing behavior; it is purely a missing lifetime check
on a cached tag.

Fix

Null-check targetComponentView before notifying, at both sites. When the view is gone
there is nothing to notify — in CapturePointer the stale tag is overwritten immediately
below, and in releasePointerCapture the tag is cleared by the existing
m_capturedPointers.size() == 0 branch. So skipping the notify loses no state transition.

Confined to vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp.
No header, signature, or public-API change.

Validation

Built from source (UseExperimentalNuget=false) into the production RNW 0.83.2
new-arch app, so the patched Microsoft.ReactNative.dll is what was exercised rather than
a prebuilt NuGet package.

  • Before: panning the FIT inspection list and interacting with a recycled row terminated
    the process with 0xc0000005. Reproducible.
  • After: an automated soak drove the guarded paths with real PT_TOUCH contacts
    (InitializeTouchInjection / InjectTouchInput — synthetic mouse input does not exercise
    RNW's touch path). Across 8 cycles on the live Inspection List: 10 press-a-row-then-pan-
    far-while-still-held events (the row is recycled underneath the held contact, which is the
    exact sequence that produced the null view), 3 expand/collapse mount-unmount churns
    under the pointer, and 8 hard flicks during mount settle. No crash; the process stayed
    responsive throughout. A separate 20-tap soak including capture-loss and scroll-steal
    cases also passed with no dropped taps.

Honest limit on that evidence: the binary under test already contains this guard, so the
absence of a crash is consistent with the fix but does not prove causation — demonstrating
that would need an otherwise-identical build without the guard, which I have not produced
(it is a ~4.5 hour from-source framework build). What the soak does establish is that the
guarded code paths are reached repeatedly and behave correctly. The root cause itself is
plain from the code: m_pointerCapturingComponentTag can outlive the component it names,
and componentViewDescriptorWithTag(...).view is documented to return a null .view for a
stale tag.

Caveats for reviewers:

  • Silently skipping the notify is the conservative choice, but it does mean a component
    unmounted while holding capture never observes OnPointerCaptureLost. If you would
    rather RNW proactively released capture at unmount time (in the component teardown path)
    so the tag can never go stale, that is a larger but arguably more correct fix and I am
    happy to take that direction instead.
  • I did not add a test: exercising this needs a component unmounted while holding capture,
    which I could not express in the existing unit-test harness. Pointers to the right
    fixture welcome.
  • Targeting 0.83-stable to match the app in production. Glad to forward-port to main.
Microsoft Reviewers: Open in CodeFlow

…g OnPointerCaptureLost

CapturePointer and releasePointerCapture look the capturing component up by its
cached m_pointerCapturingComponentTag and dereference the result unguarded. That
tag can outlive the component it names: when list/ScrollView virtualization
recycles the capturing row mid-pan, componentViewDescriptorWithTag returns a
descriptor whose .view is null, so winrt::get_self(...)->OnPointerCaptureLost()
dereferences null and terminates the process with 0xc0000005.

Null-check targetComponentView at both sites. Skipping the notify loses no state
transition: CapturePointer overwrites the stale tag immediately below, and
releasePointerCapture clears it via the existing m_capturedPointers.size() == 0
branch.
@FaithfulAudio
FaithfulAudio requested a review from a team as a code owner July 26, 2026 06:45
@azure-pipelines

Copy link
Copy Markdown
Contributor
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant