Skip to content

Commit 020147d

Browse files
committed
Fix: Move destroy field to shared instance object (#26561)
This fixes the "double free" bug illustrated by the regression test added in the previous commit. The underlying issue is that `effect.destroy` field is a mutable field but we read it during render. This is a concurrency bug — if we had a borrow checker, it would not allow this. It's rare in practice today because the field is updated during the commit phase, which takes a lock on the fiber tree until all the effects have fired. But it's still theoretically wrong because you can have multiple Fiber copies each with their own reference to a single destroy function, and indeed we discovered in production a scenario where this happens via our current APIs. In the future these types of scenarios will be much more common because we will introduce features where effects may run concurrently with the render phase — i.e. an imperative `hide` method that synchronously hides a React tree and unmounts all its effects without entering the render phase, and without interrupting a render phase that's already in progress. A future version of React may also be able to run the entire commit phase concurrently with a subsequent render phase. We can't do this now because our data structures are not fully thread safe (see: the Fiber alternate model) but we should be able to do this in the future. The fix I've introduced in this commit is to move the `destroy` field to a separate object. The effect "instance" is a shared object that remains the same for the entire lifetime of an effect. In Rust terms, a RefCell. The field is `undefined` if the effect is unmounted, or if the effect ran but is not stateful. We don't explicitly track whether the effect is mounted or unmounted because that can be inferred by the hiddenness of the fiber in the tree, i.e. whether there is a hidden Offscreen fiber above it. It's unfortunate that this is stored on a separate object, because it adds more memory per effect instance, but it's conceptually sound. I think there's likely a better data structure we could use for effects; perhaps just one array of effect instances per fiber. But I think this is OK for now despite the additional memory and we can follow up with performance optimizations later. --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com> Co-authored-by: Rick Hanlon <rickhanlonii@gmail.com> Co-authored-by: Jan Kassens <jan@kassens.net> DiffTrain build for commit 85bb7b6.
1 parent 6a14212 commit 020147d

File tree

13 files changed

+373
-276
lines changed

13 files changed

+373
-276
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6005,7 +6005,21 @@ var didWarnAboutUseWrappedInTryCatch;
60056005
{
60066006
didWarnAboutMismatchedHooksForComponent = new Set();
60076007
didWarnAboutUseWrappedInTryCatch = new Set();
6008-
} // These are set right before calling the component.
6008+
} // The effect "instance" is a shared object that remains the same for the entire
6009+
// lifetime of an effect. In Rust terms, a RefCell. We use it to store the
6010+
// "destroy" function that is returned from an effect, because that is stateful.
6011+
// The field is `undefined` if the effect is unmounted, or if the effect ran
6012+
// but is not stateful. We don't explicitly track whether the effect is mounted
6013+
// or unmounted because that can be inferred by the hiddenness of the fiber in
6014+
// the tree, i.e. whether there is a hidden Offscreen fiber above it.
6015+
//
6016+
// It's unfortunate that this is stored on a separate object, because it adds
6017+
// more memory per effect instance, but it's conceptually sound. I think there's
6018+
// likely a better data structure we could use for effects; perhaps just one
6019+
// array of effect instances per fiber. But I think this is OK for now despite
6020+
// the additional memory and we can follow up with performance
6021+
// optimizations later.
6022+
// These are set right before calling the component.
60096023

60106024
var renderLanes$1 = NoLanes; // The work-in-progress fiber. I've named it differently to distinguish it from
60116025
// the work-in-progress hook.
@@ -6970,7 +6984,7 @@ function mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
69706984
pushEffect(
69716985
HasEffect | Passive,
69726986
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
6973-
undefined,
6987+
createEffectInstance(),
69746988
null
69756989
);
69766990
return nextSnapshot;
@@ -7025,7 +7039,7 @@ function updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
70257039
pushEffect(
70267040
HasEffect | Passive,
70277041
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
7028-
undefined,
7042+
createEffectInstance(),
70297043
null
70307044
); // Unless we're rendering a blocking lane, schedule a consistency check.
70317045
// Right before committing, we will walk the tree and check if any of the
@@ -7150,11 +7164,11 @@ function rerenderState(initialState) {
71507164
return rerenderReducer(basicStateReducer);
71517165
}
71527166

7153-
function pushEffect(tag, create, destroy, deps) {
7167+
function pushEffect(tag, create, inst, deps) {
71547168
var effect = {
71557169
tag: tag,
71567170
create: create,
7157-
destroy: destroy,
7171+
inst: inst,
71587172
deps: deps,
71597173
// Circular
71607174
next: null
@@ -7181,6 +7195,12 @@ function pushEffect(tag, create, destroy, deps) {
71817195
return effect;
71827196
}
71837197

7198+
function createEffectInstance() {
7199+
return {
7200+
destroy: undefined
7201+
};
7202+
}
7203+
71847204
function mountRef(initialValue) {
71857205
var hook = mountWorkInProgressHook();
71867206

@@ -7205,25 +7225,24 @@ function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
72057225
hook.memoizedState = pushEffect(
72067226
HasEffect | hookFlags,
72077227
create,
7208-
undefined,
7228+
createEffectInstance(),
72097229
nextDeps
72107230
);
72117231
}
72127232

72137233
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
72147234
var hook = updateWorkInProgressHook();
72157235
var nextDeps = deps === undefined ? null : deps;
7216-
var destroy = undefined; // currentHook is null when rerendering after a render phase state update.
7236+
var effect = hook.memoizedState;
7237+
var inst = effect.inst; // currentHook is null when rerendering after a render phase state update.
72177238

72187239
if (currentHook !== null) {
7219-
var prevEffect = currentHook.memoizedState;
7220-
destroy = prevEffect.destroy;
7221-
72227240
if (nextDeps !== null) {
7241+
var prevEffect = currentHook.memoizedState;
72237242
var prevDeps = prevEffect.deps;
72247243

72257244
if (areHookInputsEqual(nextDeps, prevDeps)) {
7226-
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
7245+
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
72277246
return;
72287247
}
72297248
}
@@ -7233,7 +7252,7 @@ function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
72337252
hook.memoizedState = pushEffect(
72347253
HasEffect | hookFlags,
72357254
create,
7236-
destroy,
7255+
inst,
72377256
nextDeps
72387257
);
72397258
}
@@ -16254,10 +16273,12 @@ function commitHookEffectListUnmount(
1625416273
do {
1625516274
if ((effect.tag & flags) === flags) {
1625616275
// Unmount
16257-
var destroy = effect.destroy;
16258-
effect.destroy = undefined;
16276+
var inst = effect.inst;
16277+
var destroy = inst.destroy;
1625916278

1626016279
if (destroy !== undefined) {
16280+
inst.destroy = undefined;
16281+
1626116282
{
1626216283
if ((flags & Insertion) !== NoFlags) {
1626316284
setIsRunningInsertionEffect(true);
@@ -16297,7 +16318,9 @@ function commitHookEffectListMount(flags, finishedWork) {
1629716318
}
1629816319
}
1629916320

16300-
effect.destroy = create();
16321+
var inst = effect.inst;
16322+
var destroy = create();
16323+
inst.destroy = destroy;
1630116324

1630216325
{
1630316326
if ((flags & Insertion) !== NoFlags) {
@@ -16306,8 +16329,6 @@ function commitHookEffectListMount(flags, finishedWork) {
1630616329
}
1630716330

1630816331
{
16309-
var destroy = effect.destroy;
16310-
1631116332
if (destroy !== undefined && typeof destroy !== "function") {
1631216333
var hookName = void 0;
1631316334

@@ -17391,12 +17412,13 @@ function commitDeletionEffectsOnFiber(
1739117412
var effect = firstEffect;
1739217413

1739317414
do {
17394-
var _effect = effect,
17395-
destroy = _effect.destroy,
17396-
tag = _effect.tag;
17415+
var tag = effect.tag;
17416+
var inst = effect.inst;
17417+
var destroy = inst.destroy;
1739717418

1739817419
if (destroy !== undefined) {
1739917420
if ((tag & Insertion) !== NoFlags) {
17421+
inst.destroy = undefined;
1740017422
safelyCallDestroy(
1740117423
deletedFiber,
1740217424
nearestMountedAncestor,
@@ -17405,13 +17427,15 @@ function commitDeletionEffectsOnFiber(
1740517427
} else if ((tag & Layout) !== NoFlags) {
1740617428
if (shouldProfile(deletedFiber)) {
1740717429
startLayoutEffectTimer();
17430+
inst.destroy = undefined;
1740817431
safelyCallDestroy(
1740917432
deletedFiber,
1741017433
nearestMountedAncestor,
1741117434
destroy
1741217435
);
1741317436
recordLayoutEffectDuration(deletedFiber);
1741417437
} else {
17438+
inst.destroy = undefined;
1741517439
safelyCallDestroy(
1741617440
deletedFiber,
1741717441
nearestMountedAncestor,
@@ -23864,7 +23888,7 @@ function createFiberRoot(
2386423888
return root;
2386523889
}
2386623890

23867-
var ReactVersion = "18.3.0-next-60cfeeebe-20230406";
23891+
var ReactVersion = "18.3.0-next-85bb7b685-20230406";
2386823892

2386923893
// Might add PROFILE later.
2387023894

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-prod.js

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,7 +2272,7 @@ function updateSyncExternalStore(subscribe, getSnapshot) {
22722272
pushEffect(
22732273
9,
22742274
updateStoreInstance.bind(null, fiber, hook, nextSnapshot, getSnapshot),
2275-
void 0,
2275+
{ destroy: void 0 },
22762276
null
22772277
);
22782278
subscribe = workInProgressRoot;
@@ -2341,18 +2341,18 @@ function mountState(initialState) {
23412341
);
23422342
return [hook.memoizedState, initialState];
23432343
}
2344-
function pushEffect(tag, create, destroy, deps) {
2345-
tag = { tag: tag, create: create, destroy: destroy, deps: deps, next: null };
2344+
function pushEffect(tag, create, inst, deps) {
2345+
tag = { tag: tag, create: create, inst: inst, deps: deps, next: null };
23462346
create = currentlyRenderingFiber$1.updateQueue;
23472347
null === create
23482348
? ((create = createFunctionComponentUpdateQueue()),
23492349
(currentlyRenderingFiber$1.updateQueue = create),
23502350
(create.lastEffect = tag.next = tag))
2351-
: ((destroy = create.lastEffect),
2352-
null === destroy
2351+
: ((inst = create.lastEffect),
2352+
null === inst
23532353
? (create.lastEffect = tag.next = tag)
2354-
: ((deps = destroy.next),
2355-
(destroy.next = tag),
2354+
: ((deps = inst.next),
2355+
(inst.next = tag),
23562356
(tag.next = deps),
23572357
(create.lastEffect = tag)));
23582358
return tag;
@@ -2366,24 +2366,20 @@ function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
23662366
hook.memoizedState = pushEffect(
23672367
1 | hookFlags,
23682368
create,
2369-
void 0,
2369+
{ destroy: void 0 },
23702370
void 0 === deps ? null : deps
23712371
);
23722372
}
23732373
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
23742374
var hook = updateWorkInProgressHook();
23752375
deps = void 0 === deps ? null : deps;
2376-
var destroy = void 0;
2377-
if (null !== currentHook) {
2378-
var prevEffect = currentHook.memoizedState;
2379-
destroy = prevEffect.destroy;
2380-
if (null !== deps && areHookInputsEqual(deps, prevEffect.deps)) {
2381-
hook.memoizedState = pushEffect(hookFlags, create, destroy, deps);
2382-
return;
2383-
}
2384-
}
2385-
currentlyRenderingFiber$1.flags |= fiberFlags;
2386-
hook.memoizedState = pushEffect(1 | hookFlags, create, destroy, deps);
2376+
var inst = hook.memoizedState.inst;
2377+
null !== currentHook &&
2378+
null !== deps &&
2379+
areHookInputsEqual(deps, currentHook.memoizedState.deps)
2380+
? (hook.memoizedState = pushEffect(hookFlags, create, inst, deps))
2381+
: ((currentlyRenderingFiber$1.flags |= fiberFlags),
2382+
(hook.memoizedState = pushEffect(1 | hookFlags, create, inst, deps)));
23872383
}
23882384
function mountEffect(create, deps) {
23892385
mountEffectImpl(8390656, 8, create, deps);
@@ -2690,7 +2686,7 @@ var HooksDispatcherOnMount = {
26902686
pushEffect(
26912687
9,
26922688
updateStoreInstance.bind(null, fiber, root, nextSnapshot, getSnapshot),
2693-
void 0,
2689+
{ destroy: void 0 },
26942690
null
26952691
);
26962692
return nextSnapshot;
@@ -4883,10 +4879,11 @@ function commitHookEffectListUnmount(
48834879
var effect = (updateQueue = updateQueue.next);
48844880
do {
48854881
if ((effect.tag & flags) === flags) {
4886-
var destroy = effect.destroy;
4887-
effect.destroy = void 0;
4882+
var inst = effect.inst,
4883+
destroy = inst.destroy;
48884884
void 0 !== destroy &&
4889-
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
4885+
((inst.destroy = void 0),
4886+
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy));
48904887
}
48914888
effect = effect.next;
48924889
} while (effect !== updateQueue);
@@ -4899,8 +4896,10 @@ function commitHookEffectListMount(flags, finishedWork) {
48994896
var effect = (finishedWork = finishedWork.next);
49004897
do {
49014898
if ((effect.tag & flags) === flags) {
4902-
var create$81 = effect.create;
4903-
effect.destroy = create$81();
4899+
var create$81 = effect.create,
4900+
inst = effect.inst;
4901+
create$81 = create$81();
4902+
inst.destroy = create$81;
49044903
}
49054904
effect = effect.next;
49064905
} while (effect !== finishedWork);
@@ -5162,18 +5161,24 @@ function commitDeletionEffectsOnFiber(
51625161
) {
51635162
var effect = (prevHostParent = prevHostParent.next);
51645163
do {
5165-
var _effect = effect,
5166-
destroy = _effect.destroy;
5167-
_effect = _effect.tag;
5164+
var tag = effect.tag,
5165+
inst = effect.inst,
5166+
destroy = inst.destroy;
51685167
void 0 !== destroy &&
5169-
(0 !== (_effect & 2)
5170-
? safelyCallDestroy(deletedFiber, nearestMountedAncestor, destroy)
5171-
: 0 !== (_effect & 4) &&
5168+
(0 !== (tag & 2)
5169+
? ((inst.destroy = void 0),
5170+
safelyCallDestroy(
5171+
deletedFiber,
5172+
nearestMountedAncestor,
5173+
destroy
5174+
))
5175+
: 0 !== (tag & 4) &&
5176+
((inst.destroy = void 0),
51725177
safelyCallDestroy(
51735178
deletedFiber,
51745179
nearestMountedAncestor,
51755180
destroy
5176-
));
5181+
)));
51775182
effect = effect.next;
51785183
} while (effect !== prevHostParent);
51795184
}
@@ -8683,7 +8688,7 @@ var devToolsConfig$jscomp$inline_1028 = {
86838688
throw Error("TestRenderer does not support findFiberByHostInstance()");
86848689
},
86858690
bundleType: 0,
8686-
version: "18.3.0-next-60cfeeebe-20230406",
8691+
version: "18.3.0-next-85bb7b685-20230406",
86878692
rendererPackageName: "react-test-renderer"
86888693
};
86898694
var internals$jscomp$inline_1220 = {
@@ -8714,7 +8719,7 @@ var internals$jscomp$inline_1220 = {
87148719
scheduleRoot: null,
87158720
setRefreshHandler: null,
87168721
getCurrentFiber: null,
8717-
reconcilerVersion: "18.3.0-next-60cfeeebe-20230406"
8722+
reconcilerVersion: "18.3.0-next-85bb7b685-20230406"
87188723
};
87198724
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
87208725
var hook$jscomp$inline_1221 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

0 commit comments

Comments
 (0)