CVE-2026-84348
Overview
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) withbase::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.
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
- Cause power events during a call
An attacker (or ordinary system activity the attacker can influence, e.g. suspend/resume or thermal changes) triggers
MediaStreamPowerLoggercallbacks while a media/WebRTC session is active. - Log the raw address
Pre-patch, each callback formats
[this=%p]and callsSendLogMessage, writing the browser-process address of the logger object into the WebRTC text log. - 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.
- 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
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
| Function | Change | Notes |
|---|---|---|
MediaStreamPowerLoggercontent/browser/renderer_host/media/media_stream_power_logger.cc |
modified | |
MediaStreamPowerLoggercontent/browser/renderer_host/media/media_stream_power_logger.h |
modified | |
CONTENT_EXPORTcontent/browser/renderer_host/media/media_stream_power_logger.h |
modified | |
MediaStreamPowerLoggerTestcontent/browser/renderer_host/media/media_stream_power_logger_unittest.cc |
modified | |
TEST_Fcontent/browser/renderer_host/media/media_stream_power_logger_unittest.cc |
modified | |
ASSERT_TRUEcontent/browser/renderer_host/media/media_stream_power_logger_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/media/media_stream_power_logger.hcontent/test/BUILD.gncontent/browser/renderer_host/media/media_stream_power_logger.cccontent/browser/renderer_host/media/media_stream_power_logger_unittest.cc
Audit Directions
- Raw pointer formatting in logsGrep for
%p,this, and pointer casts inStringPrintf/logging calls, especially in browser-process code whose logs are exportable or reachable by renderers/lower-privilege UI, and replace them withbase::UnguessableTokenor other non-address identifiers. - Diagnostic sinks crossing trust boundariesReview 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 outputWhere 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.
Patch
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",
Regression Test / PoC
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",
Original Bug Report
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.cccontent/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
- Unconditional Log Callback Registration:
In
chrome/browser/media/webrtc/webrtc_text_log_handler.cc,WebRtcTextLogHandler::OnGetNetworkInterfaceListFinishunconditionally 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)));
- Log Broadcast of Raw Pointers:
MediaStreamManager::AddLogMessageOnIOThreadbroadcasts 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.
- Predictable Local Storage:
When logging is finished via
finishDiagnosticLoggingwithshould_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:
- An attacker origin allowlisted by enterprise policy (or a compromised renderer invoking the
blink::mojom::RTCLoggingDispatcherMojo interface directly) initiates WebRTC diagnostic logging. - 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
thispointer heap address ofMediaStreamPowerLoggerto the native log. - The page stops diagnostic logging, causing the browser to compress the accumulated logs into
<profile>/WebRTC Web API Logs/<epoch>.gz. - 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.