Skip to content

Commit aadbe96

Browse files
fkgozalimeta-codesync[bot]
authored andcommitted
Guard Scheduler deferred lambdas against delegate teardown (facebook#56680)
Summary: Pull Request resolved: facebook#56680 Scheduler::uiManagerDidFinishTransaction and Scheduler::uiManagerDidDispatchCommand queue lambdas via runtimeScheduler_->scheduleRenderingUpdate that capture the raw delegate_ pointer by value. With RuntimeScheduler_Modern (the bridgeless implementation), the lambda runs asynchronously, so if the SchedulerDelegate is destroyed between enqueue and execution (surface teardown, Scheduler destruction, or setDelegate flip), the lambda dereferences dangling memory → EXC_BAD_ACCESS / KERN_INVALID_ADDRESS. Add a per-delegate-identity invalidation token (`shared_ptr<atomic<bool>>`) owned by Scheduler and captured by-value into the deferred lambdas. The destructor and setDelegate flip the flag to true; lambdas check it (acquire ordering) before dereferencing the captured raw delegate. The shared_ptr keeps the atomic alive for any outstanding lambdas. Public API is unchanged (private member only), so C++ API snapshots are unaffected. Changelog: [General][Fixed] - Prevent Scheduler use-after-free crash when surfaces tear down with pending rendering updates Reviewed By: mdvacca, Abbondanzo Differential Revision: D103727974 fbshipit-source-id: 8abf5073b7fd35e88d65a91c42f311b238d8f156
1 parent d0672fb commit aadbe96

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ Scheduler::Scheduler(
2929
const SchedulerToolbox& schedulerToolbox,
3030
UIManagerAnimationDelegate* animationDelegate,
3131
SchedulerDelegate* delegate)
32-
: runtimeExecutor_(schedulerToolbox.runtimeExecutor),
32+
: delegateInvalidated_(std::make_shared<std::atomic<bool>>(false)),
33+
runtimeExecutor_(schedulerToolbox.runtimeExecutor),
3334
contextContainer_(schedulerToolbox.contextContainer) {
3435
// Creating a container for future `EventDispatcher` instance.
3536
eventDispatcher_ = std::make_shared<std::optional<const EventDispatcher>>();
@@ -171,6 +172,15 @@ Scheduler::~Scheduler() {
171172
LOG(WARNING) << "Scheduler::~Scheduler() was called (address: " << this
172173
<< ").";
173174

175+
// Invalidate any lambdas already queued via scheduleRenderingUpdate that
176+
// captured a raw delegate_ pointer; without this they'd dereference a
177+
// dangling SchedulerDelegate after Scheduler teardown. (No replacement
178+
// token is allocated here — Scheduler is going away.)
179+
// Gated to allow controlled rollout / rollback.
180+
if (ReactNativeFeatureFlags::enableSchedulerDelegateInvalidation()) {
181+
*delegateInvalidated_ = true;
182+
}
183+
174184
auto weakRuntimeScheduler =
175185
contextContainer_->find<std::weak_ptr<RuntimeScheduler>>(
176186
RuntimeSchedulerKey);
@@ -260,6 +270,21 @@ Scheduler::findComponentDescriptorByHandle_DO_NOT_USE_THIS_IS_BROKEN(
260270
#pragma mark - Delegate
261271

262272
void Scheduler::setDelegate(SchedulerDelegate* delegate) {
273+
// Gated to allow controlled rollout / rollback.
274+
if (ReactNativeFeatureFlags::enableSchedulerDelegateInvalidation() &&
275+
delegate_ != delegate) {
276+
// Mark the *current* token invalid: any rendering-update lambda already
277+
// queued holds a shared_ptr to this atomic and will observe `true` on
278+
// its next read, so it no-ops instead of calling into the previous
279+
// delegate (which the caller is about to drop).
280+
*delegateInvalidated_ = true;
281+
// Then install a *fresh* token (a new atomic) so lambdas captured
282+
// against the new delegate use their own non-invalidated flag.
283+
// Reusing the previous atomic and flipping it back to `false` would
284+
// re-arm the in-flight lambdas — exactly the use-after-free we're
285+
// trying to prevent — because they share the same shared_ptr.
286+
delegateInvalidated_ = std::make_shared<std::atomic<bool>>(false);
287+
}
263288
delegate_ = delegate;
264289
}
265290

@@ -288,10 +313,21 @@ void Scheduler::uiManagerDidFinishTransaction(
288313
if (!mountSynchronously) {
289314
auto surfaceId = mountingCoordinator->getSurfaceId();
290315

316+
// Capture the gating flag at queue time: the lambda's decision to
317+
// honor the invalidation guard is fixed when we enqueue, not when it
318+
// later runs. Avoids per-invocation feature-flag reads and keeps the
319+
// contract for an in-flight lambda stable across flag flips.
320+
auto guardEnabled =
321+
ReactNativeFeatureFlags::enableSchedulerDelegateInvalidation();
291322
runtimeScheduler_->scheduleRenderingUpdate(
292323
surfaceId,
293324
[delegate = delegate_,
325+
invalidated = delegateInvalidated_,
326+
guardEnabled,
294327
mountingCoordinator = std::move(mountingCoordinator)]() {
328+
if (guardEnabled && *invalidated) {
329+
return;
330+
}
295331
delegate->schedulerShouldRenderTransactions(mountingCoordinator);
296332
});
297333
} else {
@@ -314,12 +350,20 @@ void Scheduler::uiManagerDidDispatchCommand(
314350
"Scheduler::uiManagerDispatchCommand", "commandName", commandName);
315351
if (delegate_ != nullptr) {
316352
auto shadowView = ShadowView(*shadowNode);
353+
// See comment in uiManagerDidFinishTransaction above for gating shape.
354+
auto guardEnabled =
355+
ReactNativeFeatureFlags::enableSchedulerDelegateInvalidation();
317356
runtimeScheduler_->scheduleRenderingUpdate(
318357
shadowNode->getSurfaceId(),
319358
[delegate = delegate_,
359+
invalidated = delegateInvalidated_,
360+
guardEnabled,
320361
shadowView = std::move(shadowView),
321362
commandName,
322363
args]() {
364+
if (guardEnabled && *invalidated) {
365+
return;
366+
}
323367
delegate->schedulerDidDispatchCommand(shadowView, commandName, args);
324368
});
325369
}

packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#pragma once
99

10+
#include <atomic>
1011
#include <memory>
1112

1213
#include <ReactCommon/RuntimeExecutor.h>
@@ -122,6 +123,11 @@ class Scheduler final : public UIManagerDelegate {
122123
friend class SurfaceHandler;
123124

124125
SchedulerDelegate *delegate_;
126+
// Invalidation token captured by-value into lambdas deferred via
127+
// runtimeScheduler_->scheduleRenderingUpdate. Set to true on delegate
128+
// change or Scheduler destruction so a lambda that outlives its captured
129+
// raw delegate pointer can no-op instead of dereferencing dangling memory.
130+
std::shared_ptr<std::atomic<bool>> delegateInvalidated_;
125131
SharedComponentDescriptorRegistry componentDescriptorRegistry_;
126132
RuntimeExecutor runtimeExecutor_;
127133
std::shared_ptr<UIManager> uiManager_;

0 commit comments

Comments
 (0)