Skip to content

Commit 984a619

Browse files
committed
Keep values alive until the end of any pending requests
A request is the only thing that is expected to do any work. The principle is that you can derive values from out of a tainted entry during a request. Including stashing it in a per request cache. What you can't do is store a derived value in a global module level cache. At least not without also tainting the object.
1 parent 03f39a0 commit 984a619

File tree

5 files changed

+120
-2
lines changed

5 files changed

+120
-2
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@
1010

1111
'use strict';
1212

13+
const heldValues = [];
14+
let finalizationCallback;
15+
function FinalizationRegistryMock(callback) {
16+
finalizationCallback = callback;
17+
}
18+
FinalizationRegistryMock.prototype.register = function (target, heldValue) {
19+
heldValues.push(heldValue);
20+
};
21+
global.FinalizationRegistry = FinalizationRegistryMock;
22+
23+
function gc() {
24+
for (let i = 0; i < heldValues.length; i++) {
25+
finalizationCallback(heldValues[i]);
26+
}
27+
heldValues.length = 0;
28+
}
29+
1330
let act;
1431
let use;
1532
let startTransition;
@@ -1589,4 +1606,58 @@ describe('ReactFlight', () => {
15891606

15901607
expect(errors).toEqual(['Cannot pass a secret token to the client']);
15911608
});
1609+
1610+
// @gate enableTaint
1611+
it('keep a tainted value tainted until the end of any pending requests', async () => {
1612+
function UserClient({user}) {
1613+
return <span>{user.name}</span>;
1614+
}
1615+
const User = clientReference(UserClient);
1616+
1617+
function getUser() {
1618+
const user = {
1619+
name: 'Seb',
1620+
token: '3e971ecc1485fe78625598bf9b6f85db',
1621+
};
1622+
ReactServer.unstable_taintValue(
1623+
'Cannot pass a secret token to the client',
1624+
user,
1625+
user.token,
1626+
);
1627+
return user;
1628+
}
1629+
1630+
function App() {
1631+
const user = getUser();
1632+
const derivedValue = {...user};
1633+
// A garbage collection can happen at any time. Even before the end of
1634+
// this request. This would clean up the user object.
1635+
gc();
1636+
// We should still block the tainted value.
1637+
return <User user={derivedValue} />;
1638+
}
1639+
1640+
let errors = [];
1641+
ReactNoopFlightServer.render(<App />, {
1642+
onError(x) {
1643+
errors.push(x.message);
1644+
},
1645+
});
1646+
1647+
expect(errors).toEqual(['Cannot pass a secret token to the client']);
1648+
1649+
// After the previous requests finishes, the token can be rendered again.
1650+
1651+
errors = [];
1652+
ReactNoopFlightServer.render(
1653+
<User user={{token: '3e971ecc1485fe78625598bf9b6f85db'}} />,
1654+
{
1655+
onError(x) {
1656+
errors.push(x.message);
1657+
},
1658+
},
1659+
);
1660+
1661+
expect(errors).toEqual([]);
1662+
});
15921663
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export type Request = {
198198
writtenProviders: Map<string, number>,
199199
identifierPrefix: string,
200200
identifierCount: number,
201+
taintCleanupQueue: Array<string | bigint>,
201202
onError: (error: mixed) => ?string,
202203
onPostpone: (reason: string) => void,
203204
toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
@@ -207,6 +208,7 @@ const {
207208
TaintRegistryObjects,
208209
TaintRegistryValues,
209210
TaintRegistryByteLengths,
211+
TaintRegistryPendingRequests,
210212
ReactCurrentDispatcher,
211213
ReactCurrentCache,
212214
} = ReactServerSharedInternals;
@@ -216,6 +218,23 @@ function throwTaintViolation(message: string) {
216218
throw new Error(message);
217219
}
218220

221+
function cleanupTaintQueue(request: Request): void {
222+
const cleanupQueue = request.taintCleanupQueue;
223+
TaintRegistryPendingRequests.delete(cleanupQueue);
224+
for (let i = 0; i < cleanupQueue.length; i++) {
225+
const entryValue = cleanupQueue[i];
226+
const entry = TaintRegistryValues.get(entryValue);
227+
if (entry !== undefined) {
228+
if (entry.count === 1) {
229+
TaintRegistryValues.delete(entryValue);
230+
} else {
231+
entry.count--;
232+
}
233+
}
234+
}
235+
cleanupQueue.length = 0;
236+
}
237+
219238
function defaultErrorHandler(error: mixed) {
220239
console['error'](error);
221240
// Don't transform to our wrapper
@@ -250,6 +269,10 @@ export function createRequest(
250269

251270
const abortSet: Set<Task> = new Set();
252271
const pingedTasks: Array<Task> = [];
272+
const cleanupQueue: Array<string | bigint> = [];
273+
if (enableTaint) {
274+
TaintRegistryPendingRequests.add(cleanupQueue);
275+
}
253276
const hints = createHints();
254277
const request: Request = {
255278
status: OPEN,
@@ -273,6 +296,7 @@ export function createRequest(
273296
writtenProviders: new Map(),
274297
identifierPrefix: identifierPrefix || '',
275298
identifierCount: 1,
299+
taintCleanupQueue: cleanupQueue,
276300
onError: onError === undefined ? defaultErrorHandler : onError,
277301
onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone,
278302
// $FlowFixMe[missing-this-annot]
@@ -1249,6 +1273,9 @@ function logRecoverableError(request: Request, error: mixed): string {
12491273
}
12501274

12511275
function fatalError(request: Request, error: mixed): void {
1276+
if (enableTaint) {
1277+
cleanupTaintQueue(request);
1278+
}
12521279
// This is called outside error handling code such as if an error happens in React internals.
12531280
if (request.destination !== null) {
12541281
request.status = CLOSED;
@@ -1573,6 +1600,9 @@ function flushCompletedChunks(
15731600
flushBuffered(destination);
15741601
if (request.pendingChunks === 0) {
15751602
// We're done.
1603+
if (enableTaint) {
1604+
cleanupTaintQueue(request);
1605+
}
15761606
close(destination);
15771607
}
15781608
}

packages/react/src/ReactServerSharedInternals.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
TaintRegistryObjects,
1212
TaintRegistryValues,
1313
TaintRegistryByteLengths,
14+
TaintRegistryPendingRequests,
1415
} from './ReactTaintRegistry';
1516

1617
import {enableTaint} from 'shared/ReactFeatureFlags';
@@ -25,6 +26,8 @@ if (enableTaint) {
2526
ReactServerSharedInternals.TaintRegistryValues = TaintRegistryValues;
2627
ReactServerSharedInternals.TaintRegistryByteLengths =
2728
TaintRegistryByteLengths;
29+
ReactServerSharedInternals.TaintRegistryPendingRequests =
30+
TaintRegistryPendingRequests;
2831
}
2932

3033
export default ReactServerSharedInternals;

packages/react/src/ReactTaint.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import {enableTaint, enableBinaryFlight} from 'shared/ReactFeatureFlags';
1212
import binaryToComparableString from 'shared/binaryToComparableString';
1313

1414
import ReactServerSharedInternals from './ReactServerSharedInternals';
15-
const {TaintRegistryObjects, TaintRegistryValues, TaintRegistryByteLengths} =
16-
ReactServerSharedInternals;
15+
const {
16+
TaintRegistryObjects,
17+
TaintRegistryValues,
18+
TaintRegistryByteLengths,
19+
TaintRegistryPendingRequests,
20+
} = ReactServerSharedInternals;
1721

1822
interface Reference {}
1923

@@ -29,6 +33,10 @@ const defaultMessage =
2933
function cleanup(entryValue: string | bigint): void {
3034
const entry = TaintRegistryValues.get(entryValue);
3135
if (entry !== undefined) {
36+
TaintRegistryPendingRequests.forEach(function (requestQueue) {
37+
requestQueue.push(entryValue);
38+
entry.count++;
39+
});
3240
if (entry.count === 1) {
3341
TaintRegistryValues.delete(entryValue);
3442
} else {

packages/react/src/ReactTaintRegistry.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ export const TaintRegistryValues: Map<string | bigint, TaintEntry> = new Map();
1919
// Byte lengths of all binary values we've ever seen. We don't both refcounting this.
2020
// We expect to see only a few lengths here such as the length of token.
2121
export const TaintRegistryByteLengths: Set<number> = new Set();
22+
23+
// When a value is finalized, it means that it has been removed from any global caches.
24+
// No future requests can get a handle on it but any ongoing requests can still have
25+
// a handle on it. It's still tainted until that happens.
26+
type RequestCleanupQueue = Array<string | bigint>;
27+
export const TaintRegistryPendingRequests: Set<RequestCleanupQueue> = new Set();

0 commit comments

Comments
 (0)