@samitouri / QOS-React / commits / 6853d7ab2f

[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