Skip to content

Commit 7498134

Browse files
fkgozalifacebook-github-bot
authored andcommitted
Add unstable_recreateSchedulerDelegate hook for lifecycle tests
Summary: Adds a Fantom-only escape hatch that mirrors the iOS RCTScheduler dealloc lifecycle: detach the host's current SchedulerDelegate from the Scheduler and destroy it, then install a fresh one — all while the RuntimeScheduler (and any queued rendering-update lambdas) remains alive. This is exactly the production sequence in `RCTScheduler.mm` (`setDelegate(nullptr)` followed by destruction of the previous SchedulerDelegateProxy) that the host-level Scheduler in Fantom does not otherwise reproduce — surfaces come and go, but the SchedulerDelegate normally lives for the entire test process. Without this hook, Fantom tests cannot exercise SchedulerDelegate-lifecycle bugs (e.g. queued lambdas that captured a raw delegate pointer by value). With it, a test can: ```js dispatchCommand(element, 'someCommand', []); Fantom.unstable_recreateSchedulerDelegate(); Fantom.runWorkLoop(); ``` and observe a SIGSEGV when the queued lambda dereferences the previous delegate's freed storage. Implementation notes: - `ReactHost::unstable_recreateSchedulerDelegateForTesting()` allocates the replacement delegate **before** destroying the old one, to ensure the new SchedulerDelegate lands at a distinct heap address. Without this, malloc tends to reuse the just-freed slot, masking the use-after-free behind a same-address re-allocation. - Surface area is hidden behind the `unstable_` prefix and an explicit ForTesting suffix on the underlying ReactHost method to discourage product use. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D103744935
1 parent 1441d7b commit 7498134

6 files changed

Lines changed: 66 additions & 0 deletions

File tree

packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,26 @@ void ReactHost::destroyReactInstance() {
320320
reactInstanceData_->messageQueueThread = nullptr;
321321
}
322322

323+
void ReactHost::unstable_recreateSchedulerDelegateForTesting() {
324+
// Mirrors iOS RCTScheduler dealloc ordering (RCTScheduler.mm: setDelegate
325+
// (nullptr) followed by destruction of the previous SchedulerDelegateProxy,
326+
// while the RuntimeScheduler outlives both). The RuntimeScheduler may still
327+
// hold queued rendering-update lambdas that captured the previous
328+
// delegate's raw pointer by value.
329+
//
330+
// Allocate the replacement BEFORE destroying the previous one so the new
331+
// SchedulerDelegate lands at a distinct address. Otherwise the allocator
332+
// tends to reuse the just-freed slot, masking the use-after-free in
333+
// queued lambdas behind a same-address re-allocation.
334+
auto fresh = std::make_unique<SchedulerDelegateImpl>(
335+
reactInstanceData_->mountingManager);
336+
fresh->setUIManager(scheduler_->getUIManager());
337+
scheduler_->setDelegate(nullptr);
338+
schedulerDelegate_ = nullptr;
339+
scheduler_->setDelegate(fresh.get());
340+
schedulerDelegate_ = std::move(fresh);
341+
}
342+
323343
void ReactHost::reloadReactInstance() {
324344
if (isReloadingReactInstance_.exchange(true)) {
325345
return;

packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ class ReactHost {
9292

9393
void emitDeviceEvent(folly::dynamic &&args);
9494

95+
/*
96+
* Test-only. Mirrors the iOS RCTScheduler dealloc lifecycle: detach the
97+
* SchedulerDelegate from the Scheduler and destroy it, then install a
98+
* fresh one — all while the RuntimeScheduler (and any queued rendering
99+
* update lambdas) remains alive. This lets Fantom tests reproduce
100+
* lifecycle bugs where a queued lambda outlived its captured raw
101+
* SchedulerDelegate pointer.
102+
*/
103+
void unstable_recreateSchedulerDelegateForTesting();
104+
95105
private:
96106
void createReactInstance();
97107
void destroyReactInstance();

packages/react-native/src/private/testing/fantom/specs/NativeFantom.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ interface Spec extends TurboModule {
110110
flushMessageQueue: () => void;
111111
flushEventQueue: () => void;
112112
produceFramesForDuration: (miliseconds: number) => void;
113+
/**
114+
* Test-only. Mirrors the iOS RCTScheduler dealloc lifecycle by tearing
115+
* down the host's current SchedulerDelegate and installing a fresh one,
116+
* while leaving the RuntimeScheduler (and any queued rendering-update
117+
* lambdas) alive. Used to reproduce SchedulerDelegate-lifecycle UAFs.
118+
*/
119+
unstable_recreateSchedulerDelegate: () => void;
113120
validateEmptyMessageQueue: () => void;
114121
getRenderedOutput: (
115122
surfaceId: RootTag,

private/react-native-fantom/src/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,27 @@ export function runWorkLoop(): void {
379379
}
380380
}
381381

382+
/**
383+
* Test-only. Mirrors the iOS RCTScheduler dealloc lifecycle: detach the
384+
* host's current SchedulerDelegate from the Scheduler and destroy it, then
385+
* install a fresh one — all while the RuntimeScheduler (and any queued
386+
* rendering-update lambdas) remains alive. Used to reproduce
387+
* SchedulerDelegate-lifecycle bugs (e.g. queued lambdas that captured a
388+
* raw delegate pointer by value).
389+
*
390+
* @example
391+
* ```
392+
* dispatchCommand(element, 'someCommand', []);
393+
* Fantom.unstable_recreateSchedulerDelegate();
394+
* Fantom.runWorkLoop(); // pending lambda drains against the now-recreated
395+
* // delegate; if it dereferences the previous one,
396+
* // the test process crashes.
397+
* ```
398+
*/
399+
export function unstable_recreateSchedulerDelegate(): void {
400+
NativeFantom.unstable_recreateSchedulerDelegate();
401+
}
402+
382403
/**
383404
* Set this flag to `false` to let Fantom run tasks with LogBox installed
384405
* (necessary only if you are testing LogBox specifically).

private/react-native-fantom/tester/src/NativeFantom.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <react/renderer/components/modal/ModalHostViewShadowNode.h>
1515
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
1616
#include <react/renderer/uimanager/UIManagerBinding.h>
17+
#include <react/runtime/ReactHost.h>
1718
#include <fstream>
1819
#include <iostream>
1920

@@ -75,6 +76,11 @@ void NativeFantom::validateEmptyMessageQueue(jsi::Runtime& /*runtime*/) {
7576
}
7677
}
7778

79+
void NativeFantom::unstable_recreateSchedulerDelegate(
80+
jsi::Runtime& /*runtime*/) {
81+
appDelegate_.reactHost_->unstable_recreateSchedulerDelegateForTesting();
82+
}
83+
7884
std::vector<std::string> NativeFantom::takeMountingManagerLogs(
7985
jsi::Runtime& /*runtime*/,
8086
SurfaceId surfaceId) {

private/react-native-fantom/tester/src/NativeFantom.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class NativeFantom : public NativeFantomCxxSpec<NativeFantom> {
9393
void flushEventQueue(jsi::Runtime &runtime);
9494
void validateEmptyMessageQueue(jsi::Runtime &runtime);
9595

96+
void unstable_recreateSchedulerDelegate(jsi::Runtime &runtime);
97+
9698
std::vector<std::string> takeMountingManagerLogs(jsi::Runtime &runtime, SurfaceId surfaceId);
9799

98100
std::string getRenderedOutput(

0 commit comments

Comments
 (0)