Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -629,4 +629,167 @@ describe('ReactPerformanceTracks', () => {
],
]);
});

// A faithful model of a cross-origin (opaque origin) Window. Unlike a plain
// object, *any* property access throws a SecurityError — including the `in`
// operator (the `has` trap), getOwnPropertyDescriptor and walking the
// prototype. Only the handful of symbols the JS engine itself probes are
// allowed. A real cross-origin Window's *only* enumerable own properties are
// the numeric indices of its child frames, so `childFrameCount` models those.
// The previous fixture only trapped `get`, which let `'$$typeof' in value`
// slip through and so never reproduced the real crash.
function createCrossOriginWindow(childFrameCount = 0) {
const allowed = new Set([
Symbol.toStringTag,
Symbol.hasInstance,
Symbol.isConcatSpreadable,
]);
const indices = [];
for (let i = 0; i < childFrameCount; i++) {
indices.push(String(i));
}
const isChildFrameIndex = prop =>
typeof prop === 'string' && indices.includes(prop);
const guard = prop => {
if (typeof prop === 'symbol' && allowed.has(prop)) {
return;
}
if (prop === 'then') {
return;
}
// An existing child-frame index is readable; everything else (including
// the `in` check for a *missing* index) throws.
if (isChildFrameIndex(prop)) {
return;
}
throw new Error(
`Failed to read a named property '${String(prop)}' from 'Window'`,
);
};
return new Proxy(
{},
{
get(target, prop) {
guard(prop);
// A child-frame index resolves to a (childless) cross-origin Window.
return isChildFrameIndex(prop)
? createCrossOriginWindow(0)
: undefined;
},
has(target, prop) {
guard(prop);
return isChildFrameIndex(prop);
},
getOwnPropertyDescriptor(target, prop) {
guard(prop);
if (isChildFrameIndex(prop)) {
return {
enumerable: true,
configurable: true,
value: createCrossOriginWindow(0),
};
}
return undefined;
},
ownKeys() {
return indices;
},
getPrototypeOf() {
return null;
},
},
);
}

// @gate __DEV__ && enableComponentPerformanceTrack
it('does not crash diffing cross-origin Window props (opaque origin)', async () => {
const App = function App({win}) {
Scheduler.unstable_advanceTime(10);
React.useEffect(() => {}, [win]);
};

Scheduler.unstable_advanceTime(1);
await act(() => {
ReactNoop.render(<App win={createCrossOriginWindow()} />);
});
performanceMeasureCalls.length = 0;

Scheduler.unstable_advanceTime(10);
await act(() => {
// Re-render with a *different* cross-origin Window so the prop diffs and
// the perf-track logger walks it.
ReactNoop.render(<App win={createCrossOriginWindow()} />);
});

expect(performanceMeasureCalls).toEqual([
[
'​App',
{
detail: {
devtools: {
color: 'primary-dark',
properties: [
['Changed Props', ''],
[
' \xa0win',
'Referentially unequal but deeply equal objects. Consider memoization.',
],
],
tooltipText: 'App',
track: 'Components ⚛',
},
},
end: 31,
start: 21,
},
],
]);
});

// @gate __DEV__ && enableComponentPerformanceTrack
it('does not crash diffing cross-origin Windows that contain child frames', async () => {
const App = function App({win}) {
Scheduler.unstable_advanceTime(10);
React.useEffect(() => {}, [win]);
};

Scheduler.unstable_advanceTime(1);
await act(() => {
// A cross-origin Window that contains a nested iframe exposes the child
// frame as an enumerable numeric index ('0'), so `for...in` walks it.
ReactNoop.render(<App win={createCrossOriginWindow(1)} />);
});
performanceMeasureCalls.length = 0;

Scheduler.unstable_advanceTime(10);
await act(() => {
// The new Window has no child frame, so the diff checks `'0' in next` on a
// cross-origin Window that doesn't have it — which throws a SecurityError
// unless the `in` check is guarded.
ReactNoop.render(<App win={createCrossOriginWindow(0)} />);
});

// Should not throw. The removed '0' child frame is reported in the diff.
expect(performanceMeasureCalls).toEqual([
[
'​App',
{
detail: {
devtools: {
color: 'primary-dark',
properties: [
['Changed Props', ''],
[' \xa0win', ''],
['-\xa0\xa0\xa00', '…'],
],
tooltipText: 'App',
track: 'Components ⚛',
},
},
end: 31,
start: 21,
},
],
]);
});
});
32 changes: 26 additions & 6 deletions packages/shared/ReactPerformanceTrackProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,30 @@ export function addObjectToProperties(
}

function readReactElementTypeof(value: Object): mixed {
// Prevents dotting into $$typeof in opaque origin windows.
return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
? value.$$typeof
: undefined;
// Prevents dotting into $$typeof in opaque origin windows. Reading any
// property of a cross-origin Window throws a SecurityError, and that includes
// the `in` operator below (it triggers the [[HasProperty]] internal method),
// so the whole access has to be guarded — otherwise the DEV-only perf logger
// would throw out of the commit phase and corrupt the fiber tree.
try {
return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
? value.$$typeof
: undefined;
} catch (x) {
return undefined;
}
}

function safeHas(key: string, object: Object): boolean {
// `key in object` triggers the [[HasProperty]] internal method, which throws
// a SecurityError for a cross-origin Window — including for an enumerable
// child-frame index that is present on one side of a diff but not the other.
// An unreadable key is treated as absent so the DEV-only logger never throws.
try {
return key in object;
} catch (x) {
return false;
}
}

export function addValueToProperties(
Expand Down Expand Up @@ -322,7 +342,7 @@ export function addObjectDiffToProperties(
break;
}

if (!(key in next)) {
if (!safeHas(key, next)) {
properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
isDeeplyEqual = false;
}
Expand All @@ -342,7 +362,7 @@ export function addObjectDiffToProperties(
break;
}

if (key in prev) {
if (safeHas(key, prev)) {
const prevValue = prev[key];
const nextValue = next[key];
if (prevValue !== nextValue) {
Expand Down