Medium watch CVSS 6.5 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in MediaCapture
DescriptionInformation leak in MediaCapture
ComponentMediaCapture
Sub-componentWebRTC
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker518100026
Fix commitcaf99694b721 (chromium/src) +107/-9
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-30 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-09-01
Patch gap42 days — fix public in source before the advisory

Background

`MediaStreamPowerLogger`
a browser-process helper that injects system power events (suspend, resume, thermal, speed-limit changes) into WebRTC text logs for debugging call issues, defined in content/browser/renderer_host/media/media_stream_power_logger.cc.
WebRTC text logs
diagnostic log entries that are collected via the native log callback (MediaStreamManager::RegisterNativeLogCallback) and can be surfaced to and exported by less-privileged contexts, so their contents are not fully trusted output.
`%p` pointer formatting
printing a raw object address (this) with base::StringPrintf, which reveals a live heap/pointer value and thereby defeats ASLR.
`base::UnguessableToken`
a random, non-address-derived 128-bit identifier that provides a stable per-object label without exposing any memory layout information.

Root Cause Analysis

The logging methods OnSuspend(), OnResume(), OnThermalStateChange(), and OnSpeedLimitChange() formatted their log lines with base::StringPrintf("...[this=%p]...", this), embedding the raw address of the MediaStreamPowerLogger object into WebRTC text logs. Because those logs are accessible to lower-privilege consumers, the raw pointer leaked a browser-process heap address, which undermines Address Space Layout Randomization and can aid exploitation of a separate memory-corruption bug.

The fix adds a const base::UnguessableToken id_ member initialized in the constructor via base::UnguessableToken::Create() and replaces every %p/this in the log strings with id_.ToString().c_str(), so log entries retain a per-instance identifier for debugging without disclosing any address. The shipped regression test LogMessagesDoNotContainPointers asserts that all four messages contain the [id= form and that none contain the substring 0x, encoding the intended no-pointer behavior. The header change also adds CONTENT_EXPORT and the base::unguessable_token.h/content_export.h includes to support the new member and test visibility.

Key insight
The core mistake was logging a raw object pointer (this via %p) into diagnostics that reach lower-privilege contexts, defeating ASLR; the fix substitutes a random base::UnguessableToken identifier that is equally useful for correlation but reveals nothing about memory layout.

Attack Path

  1. Cause power events during a call An attacker (or ordinary system activity the attacker can influence, e.g. suspend/resume or thermal changes) triggers MediaStreamPowerLogger callbacks while a media/WebRTC session is active.
  2. Log the raw address Pre-patch, each callback formats [this=%p] and calls SendLogMessage, writing the browser-process address of the logger object into the WebRTC text log.
  3. Read the leaked pointer The attacker obtains the log contents through a context that can access WebRTC native/text logs (inferred) and recovers the raw heap address.
  4. Defeat ASLR The disclosed address reveals browser-process memory layout, which can be combined with a separate memory-corruption vulnerability to build a reliable exploit (inferred).

Impact Assessment

An attacker gains disclosure of a browser-process heap/object address (an ASLR bypass primitive) by reading WebRTC text logs that recorded the MediaStreamPowerLogger object’s raw pointer. The advisory classifies this as an information leak in MediaCapture; it does not report exploitation in the wild, and using the leak to achieve memory corruption or code execution is (inferred). The precondition is the ability to trigger the logged power events and to obtain the resulting log output.

Changed Functions

FunctionChangeNotes
MediaStreamPowerLogger
content/browser/renderer_host/media/media_stream_power_logger.cc
modified
MediaStreamPowerLogger
content/browser/renderer_host/media/media_stream_power_logger.h
modified
CONTENT_EXPORT
content/browser/renderer_host/media/media_stream_power_logger.h
modified
MediaStreamPowerLoggerTest
content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
modified
TEST_F
content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
modified
ASSERT_TRUE
content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/media/media_stream_power_logger.h
  • content/test/BUILD.gn
  • content/browser/renderer_host/media/media_stream_power_logger.cc
  • content/browser/renderer_host/media/media_stream_power_logger_unittest.cc

Audit Directions

  • Raw pointer formatting in logs
    Grep for %p, this, and pointer casts in StringPrintf/logging calls, especially in browser-process code whose logs are exportable or reachable by renderers/lower-privilege UI, and replace them with base::UnguessableToken or other non-address identifiers.
  • Diagnostic sinks crossing trust boundaries
    Review WebRTC, net-internals, and other *-internals/text-log producers for any value that encodes a real memory address, since those logs are frequently accessible outside the browser process.
  • Object identity in debug output
    Where code needs a stable per-object label for correlation, verify it derives from a random token rather than the object address, and add regression assertions (like the shipped Not(HasSubstr("0x")) check) to prevent reintroduction.
From caf99694b721dc79d25341d6d3b975e4ef32966c Mon Sep 17 00:00:00 2001
From: henrika <henrika@google.com>
Date: Tue, 21 Jul 2026 00:38:45 -0700
Subject: [PATCH] webrtc: Replace raw pointer logging with tokens in MediaStreamPowerLogger

Bug: 518100026
Change-Id: I8b442d5648f2cab333a07a60093defd15c11975f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8124755
Reviewed-by: Thomas Guilbert <tguilbert@chromium.org>
Commit-Queue: Henrik Andreasson <henrika@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1665243}
---
diff --git a/content/browser/renderer_host/media/media_stream_power_logger.cc b/content/browser/renderer_host/media/media_stream_power_logger.cc
index 6e92929..30f3f632 100644
--- a/content/browser/renderer_host/media/media_stream_power_logger.cc
+++ b/content/browser/renderer_host/media/media_stream_power_logger.cc
@@ -17,7 +17,8 @@
 }
 }  // namespace
 
-MediaStreamPowerLogger::MediaStreamPowerLogger() {
+MediaStreamPowerLogger::MediaStreamPowerLogger()
+    : id_(base::UnguessableToken::Create()) {
   auto* power_monitor = base::PowerMonitor::GetInstance();
   power_monitor->AddPowerSuspendObserver(this);
   power_monitor->AddPowerThermalObserver(this);
@@ -30,24 +31,28 @@
 }
 
 void MediaStreamPowerLogger::OnSuspend() {
-  SendLogMessage(base::StringPrintf("OnSuspend([this=%p])", this));
+  SendLogMessage(
+      base::StringPrintf("OnSuspend([id=%s])", id_.ToString().c_str()));
 }
 
 void MediaStreamPowerLogger::OnResume() {
-  SendLogMessage(base::StringPrintf("OnResume([this=%p])", this));
+  SendLogMessage(
+      base::StringPrintf("OnResume([id=%s])", id_.ToString().c_str()));
 }
 
 void MediaStreamPowerLogger::OnThermalStateChange(
     base::PowerThermalObserver::DeviceThermalState new_state) {
   const char* state_name =
       base::PowerMonitorSource::DeviceThermalStateToString(new_state);
-  SendLogMessage(base::StringPrintf(
-      "OnThermalStateChange({this=%p}, {new_state=%s})", this, state_name));
+  SendLogMessage(
+      base::StringPrintf("OnThermalStateChange({id=%s}, {new_state=%s})",
+                         id_.ToString().c_str(), state_name));
 }
 
 void MediaStreamPowerLogger::OnSpeedLimitChange(int new_limit) {
-  SendLogMessage(base::StringPrintf(
-      "OnSpeedLimitChange({this=%p}, {new_limit=%d})", this, new_limit));
+  SendLogMessage(
+      base::StringPrintf("OnSpeedLimitChange({id=%s}, {new_limit=%d})",
+                         id_.ToString().c_str(), new_limit));
 }
 
 }  // namespace content
diff --git a/content/browser/renderer_host/media/media_stream_power_logger.h b/content/browser/renderer_host/media/media_stream_power_logger.h
index e268a57..d990629 100644
--- a/content/browser/renderer_host/media/media_stream_power_logger.h
+++ b/content/browser/renderer_host/media/media_stream_power_logger.h
@@ -6,14 +6,17 @@
 #define CONTENT_BROWSER_RENDERER_HOST_MEDIA_MEDIA_STREAM_POWER_LOGGER_H_
 
 #include "base/power_monitor/power_observer.h"
+#include "base/unguessable_token.h"
+#include "content/common/content_export.h"
 
 namespace content {
 
 // Injects system power event log entries into the WebRTC text logs, for
 // debugging of unexpected call ending and performance changes eg when calls end
 // due to a laptop's lid being closed.
-class MediaStreamPowerLogger : public base::PowerSuspendObserver,
-                               public base::PowerThermalObserver {
+class CONTENT_EXPORT MediaStreamPowerLogger
+    : public base::PowerSuspendObserver,
+      public base::PowerThermalObserver {
  public:
   MediaStreamPowerLogger();
   ~MediaStreamPowerLogger() override;
@@ -26,6 +29,9 @@
   void OnThermalStateChange(
       base::PowerThermalObserver::DeviceThermalState new_state) override;
   void OnSpeedLimitChange(int new_limit) override;
+
+ private:
+  const base::UnguessableToken id_;
 };
 }  // namespace content
 
diff --git a/content/browser/renderer_host/media/media_stream_power_logger_unittest.cc b/content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
new file mode 100644
index 0000000..648ad08
--- /dev/null
+++ b/content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
@@ -0,0 +1,86 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/media/media_stream_power_logger.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/functional/bind.h"
+#include "base/test/run_until.h"
+#include "content/browser/renderer_host/media/media_stream_manager.h"
+#include "content/browser/renderer_host/media/mock_video_capture_provider.h"
+#include "content/public/test/browser_task_environment.h"
+#include "media/audio/audio_system_impl.h"
+#include "media/audio/mock_audio_manager.h"
+#include "media/audio/test_audio_thread.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+namespace {
+
+using ::testing::Contains;
+using ::testing::Each;
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+constexpr int kRenderProcessId = 1;
+
+class MediaStreamPowerLoggerTest : public ::testing::Test {
+ public:
+  MediaStreamPowerLoggerTest()
+      : task_environment_(BrowserTaskEnvironment::IO_MAINLOOP) {
+    audio_manager_ = std::make_unique<media::MockAudioManager>(
+        std::make_unique<media::TestAudioThread>());
+    audio_system_ =
+        std::make_unique<media::AudioSystemImpl>(audio_manager_.get());
+    media_stream_manager_ = std::make_unique<MediaStreamManager>(
+        audio_system_.get(), std::make_unique<MockVideoCaptureProvider>());
+
+    MediaStreamManager::RegisterNativeLogCallback(
+        kRenderProcessId,
+        base::BindRepeating(&MediaStreamPowerLoggerTest::OnLogMessage,
+                            base::Unretained(this)));
+  }
+
+  ~MediaStreamPowerLoggerTest() override {
+    MediaStreamManager::UnregisterNativeLogCallback(kRenderProcessId);
+    audio_manager_->Shutdown();
+  }
+
+ protected:
+  void OnLogMessage(const std::string& message) {
+    messages_.push_back(message);
+  }
+
+  std::vector<std::string> messages_;
+  std::unique_ptr<media::MockAudioManager> audio_manager_;
+  std::unique_ptr<media::AudioSystem> audio_system_;
+  // `media_stream_manager_` must outlive `task_environment_` because it is a
+  // CurrentThread::DestructionObserver.
+  std::unique_ptr<MediaStreamManager> media_stream_manager_;
+  BrowserTaskEnvironment task_environment_;
+};
+
+TEST_F(MediaStreamPowerLoggerTest, LogMessagesDoNotContainPointers) {
+  MediaStreamPowerLogger logger;
+  logger.OnSuspend();
+  logger.OnResume();
+  logger.OnThermalStateChange(
+      base::PowerThermalObserver::DeviceThermalState::kNominal);
+  logger.OnSpeedLimitChange(50);
+  ASSERT_TRUE(base::test::RunUntil([&]() { return messages_.size() == 4; }));
+
+  EXPECT_THAT(messages_, Contains(HasSubstr("MSPL::OnSuspend([id=")));
+  EXPECT_THAT(messages_, Contains(HasSubstr("MSPL::OnResume([id=")));
+  EXPECT_THAT(messages_,
+              Contains(HasSubstr("MSPL::OnThermalStateChange({id=")));
+  EXPECT_THAT(messages_, Contains(HasSubstr("MSPL::OnSpeedLimitChange({id=")));
+  EXPECT_THAT(messages_, Each(Not(HasSubstr("0x"))));
+}
+
+}  // namespace
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 3c13273..c5596ac 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2868,6 +2868,7 @@
     "../browser/renderer_host/media/media_devices_manager_unittest.cc",
     "../browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc",
     "../browser/renderer_host/media/media_stream_manager_unittest.cc",
+    "../browser/renderer_host/media/media_stream_power_logger_unittest.cc",
     "../browser/renderer_host/media/media_stream_ui_proxy_unittest.cc",
     "../browser/renderer_host/media/mock_preferred_audio_output_device_manager.cc",
     "../browser/renderer_host/media/mock_preferred_audio_output_device_manager.h",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/media/media_stream_power_logger_unittest.cc b/content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
new file mode 100644
index 0000000..648ad08
--- /dev/null
+++ b/content/browser/renderer_host/media/media_stream_power_logger_unittest.cc
@@ -0,0 +1,86 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/media/media_stream_power_logger.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/functional/bind.h"
+#include "base/test/run_until.h"
+#include "content/browser/renderer_host/media/media_stream_manager.h"
+#include "content/browser/renderer_host/media/mock_video_capture_provider.h"
+#include "content/public/test/browser_task_environment.h"
+#include "media/audio/audio_system_impl.h"
+#include "media/audio/mock_audio_manager.h"
+#include "media/audio/test_audio_thread.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+namespace {
+
+using ::testing::Contains;
+using ::testing::Each;
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+constexpr int kRenderProcessId = 1;
+
+class MediaStreamPowerLoggerTest : public ::testing::Test {
+ public:
+  MediaStreamPowerLoggerTest()
+      : task_environment_(BrowserTaskEnvironment::IO_MAINLOOP) {
+    audio_manager_ = std::make_unique<media::MockAudioManager>(
+        std::make_unique<media::TestAudioThread>());
+    audio_system_ =
+        std::make_unique<media::AudioSystemImpl>(audio_manager_.get());
+    media_stream_manager_ = std::make_unique<MediaStreamManager>(
+        audio_system_.get(), std::make_unique<MockVideoCaptureProvider>());
+
+    MediaStreamManager::RegisterNativeLogCallback(
+        kRenderProcessId,
+        base::BindRepeating(&MediaStreamPowerLoggerTest::OnLogMessage,
+                            base::Unretained(this)));
+  }
+
+  ~MediaStreamPowerLoggerTest() override {
+    MediaStreamManager::UnregisterNativeLogCallback(kRenderProcessId);
+    audio_manager_->Shutdown();
+  }
+
+ protected:
+  void OnLogMessage(const std::string& message) {
+    messages_.push_back(message);
+  }
+
+  std::vector<std::string> messages_;
+  std::unique_ptr<media::MockAudioManager> audio_manager_;
+  std::unique_ptr<media::AudioSystem> audio_system_;
+  // `media_stream_manager_` must outlive `task_environment_` because it is a
+  // CurrentThread::DestructionObserver.
+  std::unique_ptr<MediaStreamManager> media_stream_manager_;
+  BrowserTaskEnvironment task_environment_;
+};
+
+TEST_F(MediaStreamPowerLoggerTest, LogMessagesDoNotContainPointers) {
+  MediaStreamPowerLogger logger;
+  logger.OnSuspend();
+  logger.OnResume();
+  logger.OnThermalStateChange(
+      base::PowerThermalObserver::DeviceThermalState::kNominal);
+  logger.OnSpeedLimitChange(50);
+  ASSERT_TRUE(base::test::RunUntil([&]() { return messages_.size() == 4; }));
+
+  EXPECT_THAT(messages_, Contains(HasSubstr("MSPL::OnSuspend([id=")));
+  EXPECT_THAT(messages_, Contains(HasSubstr("MSPL::OnResume([id=")));
+  EXPECT_THAT(messages_,
+              Contains(HasSubstr("MSPL::OnThermalStateChange({id=")));
+  EXPECT_THAT(messages_, Contains(HasSubstr("MSPL::OnSpeedLimitChange({id=")));
+  EXPECT_THAT(messages_, Each(Not(HasSubstr("0x"))));
+}
+
+}  // namespace
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 3c13273..c5596ac 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2868,6 +2868,7 @@
     "../browser/renderer_host/media/media_devices_manager_unittest.cc",
     "../browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc",
     "../browser/renderer_host/media/media_stream_manager_unittest.cc",
+    "../browser/renderer_host/media/media_stream_power_logger_unittest.cc",
     "../browser/renderer_host/media/media_stream_ui_proxy_unittest.cc",
     "../browser/renderer_host/media/mock_preferred_audio_output_device_manager.cc",
     "../browser/renderer_host/media/mock_preferred_audio_output_device_manager.h",
Loading diff…

Original Bug Report

reported by vm...@google.com

Browser process heap address disclosure via WebRTC diagnostic logging

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 WebRTC diagnostic logging mechanism registers a native log callback that captures global events across the browser process and audio service. Some components, such as MediaStreamPowerLogger and system audio outputs, write raw C++ heap pointers using %p formatting into these native log messages. These log files are subsequently stored at predictable local paths, potentially allowing an attacker to bypass ASLR if chained with a file-read vulnerability.

Affected files:

  • chrome/browser/media/webrtc/webrtc_text_log_handler.cc
  • content/browser/renderer_host/media/media_stream_power_logger.cc

Estimated timestamp from git blame: 2026-03-05

Potential Browser Process Heap Address Disclosure via WebRTC Diagnostic Logging

Root Cause Analysis

  1. Unconditional Log Callback Registration: In chrome/browser/media/webrtc/webrtc_text_log_handler.cc, WebRtcTextLogHandler::OnGetNetworkInterfaceListFinish unconditionally registers a native log callback when starting text logging, regardless of whether it was triggered by a privileged extension or via the Web API:
auto log_message_callback =
    base::BindRepeating(&ForwardMessageViaTaskRunner,
                        base::SequencedTaskRunner::GetCurrentDefault(),
                        base::BindRepeating(&WebRtcTextLogHandler::LogMessage,
                                            weak_factory_.GetWeakPtr()));
content::GetIOThreadTaskRunner({})->PostTask(
    FROM_HERE,
    base::BindOnce(&content::WebRtcLog::SetLogMessageCallback,
                   render_process_id_, std::move(log_message_callback)));
  1. Log Broadcast of Raw Pointers: MediaStreamManager::AddLogMessageOnIOThread broadcasts all native log messages to every registered callback:
void MediaStreamManager::AddLogMessageOnIOThread(const std::string& message) {
  for (const auto& callback : log_callbacks_) {
    callback.second.Run(message);
  }
}

Multiple components in the browser process and audio service log raw heap pointers to the native log. For example, MediaStreamPowerLogger (content/browser/renderer_host/media/media_stream_power_logger.cc) logs raw this pointer heap addresses using the %p format specifier:

void MediaStreamPowerLogger::OnSuspend() {
  SendLogMessage(base::StringPrintf("OnSuspend([this=%p])", this));
}

Similarly, some low-latency audio inputs and outputs (e.g., PulseAudioOutputStream in media/audio/pulse/pulse_output.cc) write raw this pointers into their log callbacks.

  1. Predictable Local Storage: When logging is finished via finishDiagnosticLogging with should_upload_on_stop=false, the accumulated log buffer is compressed and written to a predictable directory inside the user’s profile: <profile>/WebRTC Web API Logs/<epoch>.gz

Potential Exploitation Scenario

While a web origin cannot directly read local files, this behavior acts as a potential browser-process ASLR bypass primitive for an attacker:

  1. An attacker origin allowlisted by enterprise policy (or a compromised renderer invoking the blink::mojom::RTCLoggingDispatcher Mojo interface directly) initiates WebRTC diagnostic logging.
  2. A system power or thermal event occurs (such as a device suspend/resume or CPU load thermal change), triggering the browser process to write the raw this pointer heap address of MediaStreamPowerLogger to the native log.
  3. The page stops diagnostic logging, causing the browser to compress the accumulated logs into <profile>/WebRTC Web API Logs/<epoch>.gz.
  4. Any separate directory traversal or local file-read vulnerability can be used to retrieve and extract the gzip file, exposing the exact layout of the browser process partition allocator.

(Note: These are potential steps; our tooling does not currently have the capability to execute proof-of-concept exploit code.)

Suggested Remediation

To resolve this issue, avoid printing raw C++ pointer addresses inside log messages. Pointers logged via %p format specifiers in MediaStreamPowerLogger, low-level audio/video wrappers, and other diagnostic emitters should be sanitized, hashed, or replaced with logical identifiers (such as unique integer IDs or object UUIDs) before being written to native logs.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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