Low chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Controls
DescriptionMemory corruption in Controls
ComponentControls
Sub-componentBlink>Media>Controls
Bug ClassMemory Corruption
Tracker562993085
Fix commitc2a5f6940ea9 (chromium/src) +37/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
  • third_party/blink/renderer/modules/media_controls/resources/mediaInterstitials.css
From c2a5f6940ea9e45c7a9a9c574711d48f74557974 Mon Sep 17 00:00:00 2001
From: Tommy Steimel <steimel@chromium.org>
Date: Fri, 18 Sep 2026 10:13:31 -0700
Subject: [PATCH] Media Controls: Use generic font for interstitial messages

Update the media remoting and Picture-in-Picture interstitial messages
to use the generic `sans-serif` font family instead of a concrete font
stack.

This ensures these UA shadow elements consistently resolve to the
platform's default sans-serif font rather than matching against document
font faces, aligning with the conventions used for other user-agent
controls.

Bug: 562993085
Change-Id: Ie45cef99fcc4e741cdaba0290d826b301c41823b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8421618
Reviewed-by: Yiren Wang <yrw@chromium.org>
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700902}
---
diff --git a/third_party/blink/renderer/modules/media_controls/resources/mediaInterstitials.css b/third_party/blink/renderer/modules/media_controls/resources/mediaInterstitials.css
index 8675a2cd..44877ec 100644
--- a/third_party/blink/renderer/modules/media_controls/resources/mediaInterstitials.css
+++ b/third_party/blink/renderer/modules/media_controls/resources/mediaInterstitials.css
@@ -69,7 +69,8 @@
     text-wrap: none;
     text-align: center;
     background-color: transparent;
-    font-family: Roboto-Regular, Sans-serif, Segoe, Serif, Helvetica;
+    /* Use a generic family consistent with other UA shadow controls. */
+    font-family: sans-serif;
     padding: 0px;
     margin: 0px;
 }
@@ -105,7 +106,8 @@
     text-align: center;
     background-color: rgba(40, 44, 50, 0.7);
     font-size: 90%;
-    font-family: Roboto-Regular, Sans-serif, Segoe, Serif, Helvetica;
+    /* Use a generic family consistent with other UA shadow controls. */
+    font-family: sans-serif;
     padding: 10pt 24pt 10pt 24pt;
     margin: 0px;
 }
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc b/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
index 38d2063d..9ed805ef 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
@@ -36,6 +36,7 @@
 #include "third_party/blink/renderer/core/layout/layout_object.h"
 #include "third_party/blink/renderer/core/loader/empty_clients.h"
 #include "third_party/blink/renderer/core/pointer_type_names.h"
+#include "third_party/blink/renderer/core/style/computed_style.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
 #include "third_party/blink/renderer/modules/media_controls/elements/media_control_cast_button_element.h"
 #include "third_party/blink/renderer/modules/media_controls/elements/media_control_current_time_display_element.h"
@@ -1825,4 +1826,36 @@
   EXPECT_TRUE(MediaControls().MediaElement().paused());
 }
 
+TEST_F(MediaControlsImplTest, InterstitialFontFamilyIsGeneric) {
+  auto& video =
+      To<HTMLVideoElement>(*GetDocument().QuerySelector(AtomicString("video")));
+  video.MediaRemotingStarted(WebString::FromUtf8("Living Room TV"));
+  UpdateAllLifecyclePhasesForTest();
+
+  ShadowRoot* shadow_root = video.UserAgentShadowRoot();
+  ASSERT_TRUE(shadow_root);
+
+  bool found_interstitial_message = false;
+  bool found_toast_message = false;
+
+  for (Element* child = ElementTraversal::FirstChild(*shadow_root); child;
+       child = ElementTraversal::Next(*child, shadow_root)) {
+    if (child->ShadowPseudoId() == "-internal-media-interstitial-message") {
+      found_interstitial_message = true;
+      const ComputedStyle* style = child->GetComputedStyle();
+      ASSERT_TRUE(style);
+      EXPECT_TRUE(style->GetFontDescription().Family().FamilyIsGeneric());
+    } else if (child->ShadowPseudoId() ==
+               "-internal-media-remoting-toast-message") {
+      found_toast_message = true;
+      const ComputedStyle* style = child->GetComputedStyle();
+      ASSERT_TRUE(style);
+      EXPECT_TRUE(style->GetFontDescription().Family().FamilyIsGeneric());
+    }
+  }
+
+  EXPECT_TRUE(found_interstitial_message);
+  EXPECT_TRUE(found_toast_message);
+}
+
 }  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc b/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
index 38d2063d..9ed805ef 100644
--- a/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
+++ b/third_party/blink/renderer/modules/media_controls/media_controls_impl_test.cc
@@ -36,6 +36,7 @@
 #include "third_party/blink/renderer/core/layout/layout_object.h"
 #include "third_party/blink/renderer/core/loader/empty_clients.h"
 #include "third_party/blink/renderer/core/pointer_type_names.h"
+#include "third_party/blink/renderer/core/style/computed_style.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
 #include "third_party/blink/renderer/modules/media_controls/elements/media_control_cast_button_element.h"
 #include "third_party/blink/renderer/modules/media_controls/elements/media_control_current_time_display_element.h"
@@ -1825,4 +1826,36 @@
   EXPECT_TRUE(MediaControls().MediaElement().paused());
 }
 
+TEST_F(MediaControlsImplTest, InterstitialFontFamilyIsGeneric) {
+  auto& video =
+      To<HTMLVideoElement>(*GetDocument().QuerySelector(AtomicString("video")));
+  video.MediaRemotingStarted(WebString::FromUtf8("Living Room TV"));
+  UpdateAllLifecyclePhasesForTest();
+
+  ShadowRoot* shadow_root = video.UserAgentShadowRoot();
+  ASSERT_TRUE(shadow_root);
+
+  bool found_interstitial_message = false;
+  bool found_toast_message = false;
+
+  for (Element* child = ElementTraversal::FirstChild(*shadow_root); child;
+       child = ElementTraversal::Next(*child, shadow_root)) {
+    if (child->ShadowPseudoId() == "-internal-media-interstitial-message") {
+      found_interstitial_message = true;
+      const ComputedStyle* style = child->GetComputedStyle();
+      ASSERT_TRUE(style);
+      EXPECT_TRUE(style->GetFontDescription().Family().FamilyIsGeneric());
+    } else if (child->ShadowPseudoId() ==
+               "-internal-media-remoting-toast-message") {
+      found_toast_message = true;
+      const ComputedStyle* style = child->GetComputedStyle();
+      ASSERT_TRUE(style);
+      EXPECT_TRUE(style->GetFontDescription().Family().FamilyIsGeneric());
+    }
+  }
+
+  EXPECT_TRUE(found_interstitial_message);
+  EXPECT_TRUE(found_toast_message);
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by vm...@google.com

Cast receiver name leak via @font-face oracle in MediaRemoting interstitial

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The UA stylesheet rule for the media remoting interstitial (video::-internal-media-interstitial-message) leads with the concrete font family Roboto-Regular. When a cast session with media remoting starts, Blink renders the browser-held Cast receiver friendly name (“Now casting to <name>”) in the <video> UA shadow root using this rule; because UA shadow trees resolve fonts against the document font selector, an author @font-face { font-family: Roboto-Regular; unicode-range: U+XX; src: url(/probe) } rule intercepts per-character font matching. The set of probe fonts fetched — observable via Resource Timing or server logs — discloses the character set of the receiver name (typically PII such as the owner’s first name) to page JS. Verified end-to-end in a release build: exact, deterministic recovery of the secret’s character set.

Affected files:

Estimated timestamp from git blame: 2017-08-03

Summary

The Cast receiver friendly name is browser-process data that the web platform deliberately never exposes (the remoteplayback module has no device-name API surface). During media remoting it flows browser → renderer via media.mojom.RemotingSource and is rendered by MediaRemotingInterstitial::Show inside the video element’s UA shadow root.

The UA rule styling that text (mediaInterstitials.css:72) leads with the concrete family Roboto-Regular instead of a generic one. UA shadow trees resolve style against the document’s font selector (font_builder.cc:635-645), and for non-generic families CSSFontSelector::GetFontData consults the author @font-face cache before any system font. A page can therefore declare one single-codepoint unicode-range probe per character of interest; Blink fetches a probe URL iff that codepoint is shaped in the interstitial string.

Verified in a release build with receiver name “Zoë’s 4K Bedroom TV #7”: exactly 25 of 159 probes were fetched in 3/3 runs — the 11 distinct characters of the localized prefix “Now casting to " plus all 14 remaining distinct characters of the secret — and the page recovered the full character set via Resource Timing ~70 ms after interstitial layout, with zero false positives or negatives. One caveat: no physical Cast device exists in the test environment, so session establishment was simulated in the browser process at CastRemotingConnector::ConnectWithMediaRemoter() (its production plug-in point); everything from the browser→renderer RemotingSource IPC through font matching and probe fetching ran stock. In production the equivalent precondition is simply that the user casts.

This is the same defect class as the autofill-preview oracle, already acknowledged and fixed for autofill pseudo-elements in html.css:645-649 (crbug.com/517710554; historic prior art CVE-2019-5848, crbug.com/951487). That fix did not touch mediaInterstitials.css.


Root Cause Analysis

  1. Secret rendered into UA shadow root: media_remoting_interstitial.cc:62-81 — on HTMLVideoElement::MediaRemotingStarted (html_video_element.cc:1053-1066), the interstitial sets “Now casting to <DEVICE_FRIENDLY_NAME>” via setInnerText on the div with pseudo id -internal-media-interstitial-message. The name originates in the browser process (components/mirroring/service/openscreen_session_host.cc:1176-1186, media/mojo/mojom/remoting_common.mojom:48-53).
  2. UA rule uses a concrete, author-shadowable family:
    /* mediaInterstitials.css:72 */
    font-family: Roboto-Regular, Sans-serif, Segoe, Serif, Helvetica;
    
    Roboto-Regular parses as FontFamily::Type::kFamilyName (not generic). The toast rule at line 108 is identical.
  3. Author @font-face wins the lookup: css_font_selector.cc:240-245 — UA shadow trees use the document font selector, and for non-generic families the author FontFaceCache is queried before system fonts. The cache is populated from author stylesheets with no origin or tree-scope gating, so a page-declared @font-face { font-family: Roboto-Regular; ... } shadows the UA’s intended font.
  4. unicode-range turns the lookup into a per-character network oracle: font_fallback_iterator.cc:218-227 — RangeSetContributesForHint requires the face’s unicode-range to contain the shaped codepoint before BeginLoadIfNeeded() issues the fetch. Probes for characters absent from the string are never requested; the fetched-probe set equals the string’s character set.

Reproduction

  1. Attacker page declares one probe per codepoint of interest, e.g. @font-face { font-family: Roboto-Regular; src: url(/probe-41); unicode-range: U+41; } for printable ASCII plus targeted Unicode blocks, and monitors performance.getEntriesByType('resource') (or its own server logs).
  2. Page embeds a viewport-filling <video> with remoting-compatible content (VP8/VP9/H.264 + AAC/Opus, > 60 s; no disableRemotePlayback attribute).
  3. The user casts — either tab casting via the browser menu (the dominant-content check then auto-starts remoting with no further interaction) or picking a device via video.remote.prompt() / Global Media Controls.
  4. Expected: rendering the receiver name in UA chrome should not be observable by the page.

Observed (release build, deterministic across runs): on remoting start the interstitial shapes “Now casting to <name>” and Blink fetches exactly the probes whose unicode-range matches a rendered character; the page recovers the receiver name’s full character set (minus characters shared with the known localized prefix) within ~70 ms of interstitial layout. Replacing the concrete family with sans-serif in both rules was rebuilt and re-tested: identical session, zero probe fetches.

Severity

low — Read-only disclosure of limited scope: the character set (not ordering) of one browser-held string per cast session, from an ordinary web page with no compromised renderer. It requires the user to cast during the session (normal Cast usage, plus the one-time media-remoting consent for the tab-cast path), and receiver names are user-identifying in practice (owner first names, household details). No memory corruption; matches the “info leaks of very limited scope” tier and the accepted rating of the same-root-cause autofill-preview oracle. Secondary, same root cause: without unicode-range, the shadowing substitutes attacker glyphs into the trusted “Now casting to…” overlay, toast, and PiP interstitial text (spoof), which the same fix removes.

Suggested Fix Direction

Same pattern as crbug.com/517710554: use a generic family (e.g. font-family: sans-serif) for video::-internal-media-interstitial-message, video::-internal-picture-in-picture-interstitial-message, and video::-internal-media-remoting-toast-message so CSSFontSelector::GetFontData never consults the author face cache. This exact change was built and verified to close the oracle while rendering the interstitial correctly.


POC: MediaRemotingInterstitial renders Cast receiver friendly name with author-@font-face-shadowable ‘Roboto-Regular’ family — @font-face/unicode-range oracle leaks the receiver name to page JS

Strategy: code-injection (browser-process simulation of the cast session; the attacker page is pure, unmodified web content) Bug Type: cross-boundary information disclosure (CWE-203) — no memory corruption, so no sanitizer signal; the evidence is the deterministic leak log + screenshots

Summary

When media remoting starts, Blink renders the browser-held Cast receiver friendly name (“Now casting to <name>”) inside the <video> element’s user-agent shadow root. The UA rule for that text (third_party/blink/renderer/modules/media_controls/resources/mediaInterstitials.css:72) leads with the concrete family Roboto-Regular:

font-family: Roboto-Regular, Sans-serif, Segoe, Serif, Helvetica;

UA shadow trees resolve style against the document font selector, and for a non-generic family CSSFontSelector::GetFontData (core/css/css_font_selector.cc:240) consults the author @font-face cache first. A web page can therefore declare one probe per codepoint:

@font-face { font-family: Roboto-Regular; src: url(/probe/004b); unicode-range: U+4B; }

Blink fetches /probe/<hex> iff that codepoint is shaped in the interstitial string (FontFallbackIterator::RangeSetContributesForHint + BeginLoadIfNeeded, platform/fonts/font_fallback_iterator.cc). The set of fetched probe URLs — observable via Resource Timing or the attacker’s server log — discloses the character set of the receiver name (typically PII such as the owner’s first name).

Result: deterministic, exact recovery. With receiver name Zoë's 4K Bedroom TV #7, exactly 25 of 159 probes were fetched — the 11 distinct characters of the known localized prefix “Now casting to " plus all 14 remaining distinct characters of the secret: # ' 4 7 B K T V Z d e m r ë. Zero false positives, zero false negatives, in 3/3 runs, ~70 ms after the interstitial’s first layout.

What is simulated vs. stock

There is no Cast device on this machine, so the one thing that cannot happen for real is the delivery of a receiver name by an actual tab-cast session. chromium-changes.patch (browser process, cast_remoting_connector.cc only) adds a fake that impersonates the mirroring service + Cast receiver at their exact production plug-in point, CastRemotingConnector:: ConnectWithMediaRemoter(): it announces a remoting-capable sink whose friendly name comes from the POC_FAKE_CAST_SINK_NAME env var, and answers Start() with OnStarted(), as the real receiver’s ANSWER does.

Everything at or past the trust boundary under test is completely stock and crosses the real process boundary via the production mojo interfaces: CastRemotingConnector → RemotingBridge → (browser→renderer IPC) media.mojom.RemotingSource → media::remoting::RendererController → WebMediaPlayerImpl::SwitchToRemoteRenderer → HTMLVideoElement::MediaRemotingStarted → MediaRemotingInterstitial::Show → style/font resolution → probe fetches. The attacker page performs no privileged action of any kind.

The remoting trigger used is the real dominant-content path (video ≥ 85% of viewport while the tab is cast: MediaCustomControlsFullscreenDetector → RendererController::OnBecameDominantVisibleContent), including the genuine duration (> 60 s), codec-capability, and 5 s pixel-rate checks — satisfied by ordinary MSE playback of a VP8 stream.

User-state notes (production realism):

  • The profile is pre-seeded with media_router.media_remoting.enabled = true, i.e. a user who has accepted Chrome’s one-time “optimized casting” media-remoting permission dialog in an earlier session (or re-enabled the toggle in cast settings). A first-time user would see that dialog once, during casting, before the leak fires.
  • In production the whole flow requires the user to cast (tab cast via the Chrome menu, or picking a device from video.remote.prompt()); the leak then needs no further interaction. This POC’s fake session stands in for that user journey.
  • Environment-only flags: --no-proxy-server (sandbox proxy env), --ignore-gpu-blocklist (software-GL lab), --lang=en-US (deterministic prefix charset). None affect the bug.

Prerequisites

  • Linux Chromium checkout + release build dir (used: out/Default, component build; any desktop build with enable_media_remoting=true, the default, works — no sanitizer needed).
  • X display (Chrome runs headful), python3.

Reproduction

# 1) Apply the browser-side cast-session simulation and build
cd /chromium_src/src
git apply /poc-artifacts/chromium-changes.patch
autoninja -C out/Default chrome

# 2) Run the whole POC (server + pref-seeded profile + chrome + collection)
cd /poc-artifacts
CHROME=/chromium_src/src/out/Default/chrome ./run_poc.sh

# 3) Clean up the tree
cd /chromium_src/src && git checkout -- chrome/browser/media/cast_remoting_connector.cc

run_poc.sh sets the secret (POC_FAKE_CAST_SINK_NAME="Zoë's 4K Bedroom TV #7"), serves poc.html + probes.css (159 single-codepoint probes, generated by gen_probes.py) + media.webm (VP8 video-only clip from Chromium’s media test data, buffered to 90 s via MSE sequence-mode appends), collects for 45 s, takes proof-screenshot.png, and prints server-log.txt.

Expected Result

~7–9 s after page load (5 s of it is the stock pixel-rate window) the interstitial appears and the server log shows:

  • ORACLE: font probe fetched for U+XXXX — exactly the distinct codepoints of “Now casting to Zoë’s 4K Bedroom TV #7” (25 probes; the other 134 are never requested);
  • the page’s own Resource-Timing-based recovery (PAGE-RECOVERED receiver-name charset), and
  • VERIFY *** EXACT MATCH *** against the ground truth (verification aid only; a real attacker keeps the recovered set).

proof-screenshot.png shows the interstitial rendering the secret name with the page’s recovered charset in the corner HUD.

Output (actual run)

[   0.000s] server starting on 127.0.0.1:8080 (pid 1621)
[   0.000s] ground truth (verification only): "Zoë's 4K Bedroom TV #7"
[   2.542s] PAGE: buffered 90.3s; duration=90.3s; playing. Waiting for cast…
[   2.543s] PAGE: video-playing
[   7.5xxs] ORACLE: font probe fetched for U+004E  N        <- "Now casting to " prefix chars…
[   7.5xxs] ORACLE: font probe fetched for U+00EB  ë        <- …and the secret's chars
[   7.5xxs] ORACLE: font probe fetched for U+005A  Z
[   7.5xxs] ORACLE: font probe fetched for U+004B  K
            ... 25 probes total, and no others ...
[   8.097s] PAGE-RECOVERED receiver-name charset: [ # ' 4 7 B K T V Z d e m r ë ]  (from 25 fetched probes)
[   8.097s] VERIFY ground truth "Zoë's 4K Bedroom TV #7": secret-only charset = { # ' 4 7 B K T V Z d e m r ë }
[   8.097s] VERIFY *** EXACT MATCH: page recovered the full character set of the receiver name (minus chars shared with the localized prefix) ***

Full log: server-log.txt. Screenshot: proof-screenshot.png.

Bandaid fix (verified)

bandaid-fix.patch applies the same pattern as the shipped autofill-preview fix in core/html/resources/html.css (concrete family → generic family so the author @font-face cache is never consulted): both video::-internal-media-interstitial-message / video::-internal-picture-in-picture-interstitial-message and video::-internal-media-remoting-toast-message get font-family: sans-serif;.

Verified by rebuilding and re-running the identical scenario: server-log-with-fix.txt shows the session starting and the interstitial rendering the name (see proof-screenshot-with-fix.png, now in sans-serif) with zero probe fetches over the full 45 s — the oracle is closed, including for the (spoofable) toast and picture-in-picture messages.

Files

file purpose
poc.html attacker page: probes + MSE playback + Resource Timing read-back
probes.css, gen_probes.py 159 single-codepoint @font-face probes (+ generator)
server.py attacker server; logs each probe fetch (the oracle) and the page’s recovery
media.webm VP8 video-only clip (Chromium media/test/data/bear-320x240-video-only.webm)
run_poc.sh end-to-end driver
chromium-changes.patch browser-process simulation of an established tab-cast session (POC scaffolding, not part of the bug)
bandaid-fix.patch UA-stylesheet fix, verified above
server-log.txt, proof-screenshot.png evidence, vulnerable build
server-log-with-fix.txt, proof-screenshot-with-fix.png evidence, fixed build

Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker