Skip to content

Add C++ test for Scheduler delegate UAF after JS-throw teardown (#56800)#56800

Open
fkgozali wants to merge 1 commit into
facebook:mainfrom
fkgozali:export-D104777850
Open

Add C++ test for Scheduler delegate UAF after JS-throw teardown (#56800)#56800
fkgozali wants to merge 1 commit into
facebook:mainfrom
fkgozali:export-D104777850

Conversation

@fkgozali
Copy link
Copy Markdown
Contributor

@fkgozali fkgozali commented May 12, 2026

Summary:

Reproduces, in a standalone gtest, the use-after-free race between Scheduler
teardown and pending rendering-update lambdas previously enqueued via
runtimeScheduler->scheduleRenderingUpdate inside
Scheduler::uiManagerDidFinishTransaction.

The lambda captures the SchedulerDelegate by raw pointer; when the delegate
is destroyed (as part of an instance teardown triggered by an uncaught fatal
error) before the lambda runs, the dereference is a use-after-free unless
the invalidation-token guard in Scheduler::setDelegate
(enableSchedulerDelegateInvalidation) is enabled at queue time.

The test:

  • Drives the real Scheduler::uiManagerDidFinishTransaction so the lambda
    is enqueued via the regular code path into a real RuntimeScheduler's
    pending-rendering-updates queue.
  • Initiates teardown via an uncaught JSI host-function throw routed through
    RuntimeScheduler's onTaskError callback (the test's analog of a host-side
    fatal handler), which drops the delegate.
  • Triggers the next event loop tick to drain the queue.

Three test cases:

  1. Sanity_LambdaRunsOnNextTickWhenDelegateAlive -- baseline: lambda runs and
    reaches the delegate when no teardown happens.
  2. GuardEnabled_JSThrowInitiatedTeardownIsSafe -- with the guard ON, the
    pending lambda observes the invalidation token after teardown and returns
    without touching the freed delegate. Safe.
  3. GuardDisabled_JSThrowInitiatedTeardownIsUAF -- with the guard OFF, the
    lambda dereferences the destroyed delegate. Caught by EXPECT_DEATH via a
    magic-sentinel ASSERT_EQ in the recording delegate, or by AddressSanitizer
    on the vptr load.

Fantom is intentionally not used here: it shares the global runtime VM across
tests, which would interfere with this test's contract that no further JS
executes after a fatal-driven instance teardown.

Changelog: [Internal]

Reviewed By: javache

Differential Revision: D104777850

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 12, 2026
@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented May 12, 2026

@fkgozali has exported this pull request. If you are a Meta employee, you can view the originating Diff in D104777850.

@fkgozali fkgozali force-pushed the export-D104777850 branch from 9e56873 to 27c7f91 Compare May 12, 2026 16:10
@meta-codesync meta-codesync Bot changed the title Add C++ test for Scheduler delegate UAF after JS-throw teardown Add C++ test for Scheduler delegate UAF after JS-throw teardown (#56800) May 12, 2026
fkgozali added a commit to fkgozali/react-native that referenced this pull request May 12, 2026
…book#56800)

Summary:


Reproduces, in a standalone gtest, the use-after-free race between Scheduler
teardown and pending rendering-update lambdas previously enqueued via
runtimeScheduler->scheduleRenderingUpdate inside
Scheduler::uiManagerDidFinishTransaction.

The lambda captures the SchedulerDelegate by raw pointer; when the delegate
is destroyed (as part of an instance teardown triggered by an uncaught fatal
error) before the lambda runs, the dereference is a use-after-free unless
the invalidation-token guard in Scheduler::setDelegate
(enableSchedulerDelegateInvalidation) is enabled at queue time.

The test:
- Drives the *real* Scheduler::uiManagerDidFinishTransaction so the lambda
  is enqueued via the regular code path into a real RuntimeScheduler's
  pending-rendering-updates queue.
- Initiates teardown via an uncaught JSI host-function throw routed through
  RuntimeScheduler's onTaskError callback (the test's analog of a host-side
  fatal handler), which drops the delegate.
- Triggers the next event loop tick to drain the queue.

Three test cases:
1. Sanity_LambdaRunsOnNextTickWhenDelegateAlive -- baseline: lambda runs and
   reaches the delegate when no teardown happens.
2. GuardEnabled_JSThrowInitiatedTeardownIsSafe -- with the guard ON, the
   pending lambda observes the invalidation token after teardown and returns
   without touching the freed delegate. Safe.
3. GuardDisabled_JSThrowInitiatedTeardownIsUAF -- with the guard OFF, the
   lambda dereferences the destroyed delegate. Caught by EXPECT_DEATH via a
   magic-sentinel ASSERT_EQ in the recording delegate, or by AddressSanitizer
   on the vptr load.

Fantom is intentionally not used here: it shares the global runtime VM across
tests, which would interfere with this test's contract that no further JS
executes after a fatal-driven instance teardown.


Changelog: [Internal]

Reviewed By: javache

Differential Revision: D104777850
@fkgozali fkgozali force-pushed the export-D104777850 branch from 27c7f91 to 8e4535e Compare May 12, 2026 18:00
…book#56800)

Summary:


Reproduces, in a standalone gtest, the use-after-free race between Scheduler
teardown and pending rendering-update lambdas previously enqueued via
runtimeScheduler->scheduleRenderingUpdate inside
Scheduler::uiManagerDidFinishTransaction.

The lambda captures the SchedulerDelegate by raw pointer; when the delegate
is destroyed (as part of an instance teardown triggered by an uncaught fatal
error) before the lambda runs, the dereference is a use-after-free unless
the invalidation-token guard in Scheduler::setDelegate
(enableSchedulerDelegateInvalidation) is enabled at queue time.

The test:
- Drives the *real* Scheduler::uiManagerDidFinishTransaction so the lambda
  is enqueued via the regular code path into a real RuntimeScheduler's
  pending-rendering-updates queue.
- Initiates teardown via an uncaught JSI host-function throw routed through
  RuntimeScheduler's onTaskError callback (the test's analog of a host-side
  fatal handler), which drops the delegate.
- Triggers the next event loop tick to drain the queue.

Three test cases:
1. Sanity_LambdaRunsOnNextTickWhenDelegateAlive -- baseline: lambda runs and
   reaches the delegate when no teardown happens.
2. GuardEnabled_JSThrowInitiatedTeardownIsSafe -- with the guard ON, the
   pending lambda observes the invalidation token after teardown and returns
   without touching the freed delegate. Safe.
3. GuardDisabled_JSThrowInitiatedTeardownIsUAF -- with the guard OFF, the
   lambda dereferences the destroyed delegate. Caught by EXPECT_DEATH via a
   magic-sentinel ASSERT_EQ in the recording delegate, or by AddressSanitizer
   on the vptr load.

Fantom is intentionally not used here: it shares the global runtime VM across
tests, which would interfere with this test's contract that no further JS
executes after a fatal-driven instance teardown.


Changelog: [Internal]

Reviewed By: javache

Differential Revision: D104777850
@fkgozali fkgozali force-pushed the export-D104777850 branch from 8e4535e to 9e699f7 Compare May 12, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. fb-exported meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant