[Perf Tracks] Prevent crash when accessing `$$typeof` (#35679)
Sebastian "Sebbie" Silbermann committed
Feb 3, 2026 at 17:53 UTC
6853d7ab2f73989f31fb5a8ed652bd58225ead26
2 files changed
+117
-3
packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
+106
@@ -523,4 +523,110 @@ describe('ReactPerformanceTracks', () => {
523
],
524
]);
525
});
526
+
527
+ // @gate __DEV__ && enableComponentPerformanceTrack
528
+ it('diffs HTML-like objects', async () => {
529
+ const App = function App({container}) {
530
+ Scheduler.unstable_advanceTime(10);
531
+ React.useEffect(() => {}, [container]);
532
+ };
533
+
534
+ class Window {}
535
+ const createOpaqueOriginWindow = () => {
536
+ return new Proxy(new Window(), {
537
+ get(target, prop) {
538
+ if (prop === Symbol.toStringTag) {
539
+ return target[Symbol.toStringTag];
540
+ }
541
+ // Some properties are allowed if JS itself is accessign those e.g.
542
+ // Symbol.toStringTag.
543
+ // Just make sure React isn't accessing arbitrary properties.
544
+ throw new Error(
545
+ `Failed to read named property '${String(prop)}' from Window`,
546
+ );
547
+ },
548
+ });
549
+ };
550
+
551
+ class OpaqueOriginHTMLIFrameElement {
552
+ constructor(textContent) {
553
+ this.textContent = textContent;
554
+ }
555
+ contentWindow = createOpaqueOriginWindow();
556
+ nodeType = 1;
557
+ [Symbol.toStringTag] = 'HTMLIFrameElement';
558
+ }
559
+
560
+ Scheduler.unstable_advanceTime(1);
561
+ await act(() => {
562
+ ReactNoop.render(
563
+ <App
564
+ container={new OpaqueOriginHTMLIFrameElement('foo')}
565
+ contentWindow={createOpaqueOriginWindow()}
566
+ />,
567
+ );
568
+ });
569
+
570
+ expect(performanceMeasureCalls).toEqual([
571
+ [
572
+ 'Mount',
573
+ {
574
+ detail: {
575
+ devtools: {
576
+ color: 'warning',
577
+ properties: null,
578
+ tooltipText: 'Mount',
579
+ track: 'Components ⚛',
580
+ },
581
+ },
582
+ end: 11,
583
+ start: 1,
584
+ },
585
+ ],
586
+ ]);
587
+ performanceMeasureCalls.length = 0;
588
+
589
+ Scheduler.unstable_advanceTime(10);
590
+
591
+ await act(() => {
592
+ ReactNoop.render(
593
+ <App
594
+ container={new OpaqueOriginHTMLIFrameElement('bar')}
595
+ contentWindow={createOpaqueOriginWindow()}
596
+ />,
597
+ );
598
+ });
599
+
600
+ expect(performanceMeasureCalls).toEqual([
601
+ [
602
+ 'App',
603
+ {
604
+ detail: {
605
+ devtools: {
606
+ color: 'primary-dark',
607
+ properties: [
608
+ ['Changed Props', ''],
609
+ ['- container', 'HTMLIFrameElement'],
610
+ ['- contentWindow', 'Window'],
611
+ ['- nodeType', '1'],
612
+ ['- textContent', '"foo"'],
613
+ ['+ container', 'HTMLIFrameElement'],
614
+ ['+ contentWindow', 'Window'],
615
+ ['+ nodeType', '1'],
616
+ ['+ textContent', '"bar"'],
617
+ [
618
+ ' contentWindow',
619
+ 'Referentially unequal but deeply equal objects. Consider memoization.',
620
+ ],
621
+ ],
622
+ tooltipText: 'App',
623
+ track: 'Components ⚛',
624
+ },
625
+ },
626
+ end: 31,
627
+ start: 21,
628
+ },
629
+ ],
630
+ ]);
631
+ });
632
});
packages/shared/ReactPerformanceTrackProperties.js
+11
-3
@@ -82,6 +82,13 @@ export function addObjectToProperties(
82
}
83
}
84
85
+function readReactElementTypeof(value: Object): mixed {
86
+ // Prevents dotting into $$typeof in opaque origin windows.
87
+ return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
88
+ ? value.$$typeof
89
+ : undefined;
90
+}
91
+
92
export function addValueToProperties(
93
propertyName: string,
94
value: mixed,
@@ -96,7 +103,7 @@ export function addValueToProperties(
103
desc = 'null';
104
break;
105
} else {
99
- if (value.$$typeof === REACT_ELEMENT_TYPE) {
106
+ if (readReactElementTypeof(value) === REACT_ELEMENT_TYPE) {
107
// JSX
108
const typeName = getComponentNameFromType(value.type) || '\u2026';
109
const key = value.key;
@@ -352,9 +359,10 @@ export function addObjectDiffToProperties(
359
typeof nextValue === 'object' &&
360
prevValue !== null &&
361
nextValue !== null &&
355
- prevValue.$$typeof === nextValue.$$typeof
362
+ readReactElementTypeof(prevValue) ===
363
+ readReactElementTypeof(nextValue)
364
) {
357
- if (nextValue.$$typeof === REACT_ELEMENT_TYPE) {
365
+ if (readReactElementTypeof(nextValue) === REACT_ELEMENT_TYPE) {
366
if (
367
prevValue.type === nextValue.type &&
368
prevValue.key === nextValue.key