CVE-2026-11281
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/host/win/event_trace_data.cc |
modified | |
TEST_Fremoting/host/win/event_trace_data_unittest.cc |
modified |
Files Changed
remoting/host/win/event_trace_data.ccremoting/host/win/event_trace_data_unittest.cc
Patch
From b8b43e307495d9946f5a6dad02bbcff925d4eeb1 Mon Sep 17 00:00:00 2001
From: Joe Downing <joedow@google.com>
Date: Thu, 16 Apr 2026 23:12:07 -0700
Subject: [PATCH] CRD: Use safe buffer access in EventTraceData::Create
The Chrome Remote Desktop daemon on Windows parses ETW logs.
An integer truncation bug in EventTraceData::Create allowed a
malicious ETW event to specify an out-of-bounds read offset,
potentially leading to heap memory leakage into log files.
This patch refactors the parsing logic to use base::span and
base::SpanReader. This prevents truncation by using safe arithmetic
(base::CheckedNumeric) and ensures all reads are bounds-checked by
the underlying abstractions.
Key changes:
- Replaced raw pointer arithmetic with base::SpanReader for
sequential field extraction.
- Used base::CheckedNumeric to safely calculate stack data skip
sizes, preventing truncation and overflow on 32-bit/64-bit
systems.
- Replaced strnlen_s and manual char pointer casts with
base::as_string_view and std::string_view::find for safer string
parsing.
- Added an integrity check (DCHECK) to ensure the entire MofData
buffer is consumed.
- Added a regression test case to verify safe handling of malformed
ETW payloads with excessive stack depth.
Bug: 501900366
Change-Id: If10ca8b965be8c648cc2a4acc785a5d231a5e183
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7769113
Reviewed-by: Yuwei Huang <yuweih@chromium.org>
Commit-Queue: Joe Downing <joedow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1616350}
---
diff --git a/remoting/host/win/event_trace_data.cc b/remoting/host/win/event_trace_data.cc
index 4ae3b2d..26ca871 100644
--- a/remoting/host/win/event_trace_data.cc
+++ b/remoting/host/win/event_trace_data.cc
@@ -4,12 +4,18 @@
#include "remoting/host/win/event_trace_data.h"
+#include <string_view>
+
#include "base/check.h"
#include "base/compiler_specific.h"
+#include "base/containers/span.h"
+#include "base/containers/span_reader.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/logging_win.h"
#include "base/notreached.h"
+#include "base/numerics/checked_math.h"
+#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
namespace remoting {
@@ -70,38 +76,60 @@
// - For LOG_MESSAGE_FULL events, the MofData buffer is comprised of 5 fields
// which must be parsed (or skipped) in sequence.
if (data.event_type == logging::LOG_MESSAGE) {
- data.message.assign(reinterpret_cast<const char*>(event->MofData),
- event->MofLength);
+ // SAFETY: `event->MofData` and `event->MofLength` are provided by the
+ // Windows ETW subsystem. We trust these values to define the valid memory
+ // range for the event payload.
+ auto message_span = UNSAFE_BUFFERS(base::span(
+ reinterpret_cast<const uint8_t*>(event->MofData), event->MofLength));
+ std::string_view message_view = base::as_string_view(message_span);
+ data.message.assign(message_view.substr(0, message_view.find('\0')));
} else if (data.event_type == logging::LOG_MESSAGE_FULL) {
- const uint8_t* mof_data = reinterpret_cast<const uint8_t*>(event->MofData);
- uint32_t offset = 0;
+ // SAFETY: `event->MofData` and `event->MofLength` are provided by the
+ // Windows ETW subsystem. We trust these values to define the valid memory
+ // range for the event payload.
+ base::SpanReader reader(UNSAFE_BUFFERS(base::span(
+ reinterpret_cast<const uint8_t*>(event->MofData), event->MofLength)));
// Read the size, skip past the stack info, and move the cursor.
- DWORD stack_depth = *reinterpret_cast<const DWORD*>(mof_data);
- int bytes_to_skip = sizeof(DWORD) + stack_depth * sizeof(intptr_t);
- offset += bytes_to_skip;
+ uint32_t stack_depth;
+ if (!reader.ReadU32NativeEndian(stack_depth)) {
+ return data;
+ }
+ base::CheckedNumeric<size_t> bytes_to_skip = stack_depth;
+ bytes_to_skip *= sizeof(intptr_t);
+ if (!bytes_to_skip.IsValid() || !reader.Skip(bytes_to_skip.ValueOrDie())) {
+ return data;
+ }
// Read the line info and move the cursor.
- data.line =
- *reinterpret_cast<const int32_t*>(UNSAFE_TODO(mof_data + offset));
- offset += sizeof(int32_t);
+ if (!reader.ReadI32NativeEndian(data.line)) {
+ return data;
+ }
// Read the file info and move the cursor.
- const char* file_info =
- reinterpret_cast<const char*>(UNSAFE_TODO(mof_data + offset));
- size_t str_len = strnlen_s(file_info, event->MofLength - offset);
- base::FilePath file_path(base::UTF8ToWide(file_info));
+ std::string_view file_info_view =
+ base::as_string_view(reader.remaining_span());
+ size_t nul_pos = file_info_view.find('\0');
+ if (nul_pos == std::string_view::npos) {
+ return data;
+ }
+ base::FilePath file_path(
+ base::UTF8ToWide(file_info_view.substr(0, nul_pos)));
data.file_name = base::WideToUTF8(file_path.BaseName().value());
- offset += (str_len + 1);
+ reader.Skip(nul_pos + 1);
// Read the message and move the cursor.
- const char* message =
- reinterpret_cast<const char*>(UNSAFE_TODO(mof_data + offset));
- str_len = strnlen_s(message, event->MofLength - offset);
- data.message.assign(message);
- offset += (str_len + 1);
+ std::string_view message_view =
+ base::as_string_view(reader.remaining_span());
+ nul_pos = message_view.find('\0');
+ if (nul_pos == std::string_view::npos) {
+ return data;
+ }
+ data.message.assign(message_view.substr(0, nul_pos));
+ reader.Skip(nul_pos + 1);
- DCHECK_EQ(event->MofLength, offset);
+ // Ensure that the entire buffer was consumed.
+ DCHECK_EQ(reader.remaining(), 0u);
} else {
NOTREACHED() << "Unknown event type: " << data.event_type;
}
diff --git a/remoting/host/win/event_trace_data_unittest.cc b/remoting/host/win/event_trace_data_unittest.cc
index f47447a..243c0ac6 100644
--- a/remoting/host/win/event_trace_data_unittest.cc
+++ b/remoting/host/win/event_trace_data_unittest.cc
@@ -4,6 +4,9 @@
#include "remoting/host/win/event_trace_data.h"
+#include <string>
+#include <vector>
+
#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
@@ -27,15 +30,13 @@
protected:
void InitForLogMessage();
void InitForLogMessageFull();
+ void InitSharedFields(uint8_t type);
size_t ReserveBufferSpace(size_t space_needed);
FILETIME time_ = {};
EVENT_TRACE event_trace_ = {};
std::vector<uint8_t> buffer_;
-
- private:
- void InitSharedFields(uint8_t type);
};
void EventTraceDataTest::InitSharedFields(uint8_t type) {
@@ -110,6 +111,7 @@
EXPECT_EQ(data.process_id, kProcessId);
EXPECT_EQ(data.thread_id, kThreadId);
EXPECT_STREQ(data.message.c_str(), kTestLogMessage);
+ EXPECT_EQ(data.message.length(), strlen(kTestLogMessage));
// File and line data should not be filled in for this log message type.
EXPECT_EQ(data.file_name, std::string());
@@ -131,4 +133,29 @@
EXPECT_STREQ(data.message.c_str(), kTestLogMessage);
}
+TEST_F(EventTraceDataTest, LogFullMessage_LargeStackDepth) {
+ InitSharedFields(static_cast<uint8_t>(logging::LOG_MESSAGE_FULL));
+
+ // A large stack depth that would require more memory than is available in
+ // the buffer should be handled safely.
+ DWORD large_stack_depth = 0x3FFFFFFE;
+ size_t data_size = sizeof(DWORD);
+ size_t offset = ReserveBufferSpace(data_size);
+ UNSAFE_TODO(memcpy(buffer_.data() + offset, &large_stack_depth, data_size));
+
+ // Set the MofLength to the current buffer size, which only contains the
+ // large stack depth value.
+ event_trace_.MofData = buffer_.data();
+ event_trace_.MofLength = buffer_.size();
+
Regression Test / PoC
diff --git a/remoting/host/win/event_trace_data_unittest.cc b/remoting/host/win/event_trace_data_unittest.cc
index f47447a..243c0ac6 100644
--- a/remoting/host/win/event_trace_data_unittest.cc
+++ b/remoting/host/win/event_trace_data_unittest.cc
@@ -4,6 +4,9 @@
#include "remoting/host/win/event_trace_data.h"
+#include <string>
+#include <vector>
+
#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
@@ -27,15 +30,13 @@
protected:
void InitForLogMessage();
void InitForLogMessageFull();
+ void InitSharedFields(uint8_t type);
size_t ReserveBufferSpace(size_t space_needed);
FILETIME time_ = {};
EVENT_TRACE event_trace_ = {};
std::vector<uint8_t> buffer_;
-
- private:
- void InitSharedFields(uint8_t type);
};
void EventTraceDataTest::InitSharedFields(uint8_t type) {
@@ -110,6 +111,7 @@
EXPECT_EQ(data.process_id, kProcessId);
EXPECT_EQ(data.thread_id, kThreadId);
EXPECT_STREQ(data.message.c_str(), kTestLogMessage);
+ EXPECT_EQ(data.message.length(), strlen(kTestLogMessage));
// File and line data should not be filled in for this log message type.
EXPECT_EQ(data.file_name, std::string());
@@ -131,4 +133,29 @@
EXPECT_STREQ(data.message.c_str(), kTestLogMessage);
}
+TEST_F(EventTraceDataTest, LogFullMessage_LargeStackDepth) {
+ InitSharedFields(static_cast<uint8_t>(logging::LOG_MESSAGE_FULL));
+
+ // A large stack depth that would require more memory than is available in
+ // the buffer should be handled safely.
+ DWORD large_stack_depth = 0x3FFFFFFE;
+ size_t data_size = sizeof(DWORD);
+ size_t offset = ReserveBufferSpace(data_size);
+ UNSAFE_TODO(memcpy(buffer_.data() + offset, &large_stack_depth, data_size));
+
+ // Set the MofLength to the current buffer size, which only contains the
+ // large stack depth value.
+ event_trace_.MofData = buffer_.data();
+ event_trace_.MofLength = buffer_.size();
+
+ // The malformed data should be detected and handled without crashing or
+ // reading out of bounds.
+ EventTraceData data = EventTraceData::Create(&event_trace_);
+
+ // Since the payload is malformed, the line number and message should not
+ // be populated.
+ EXPECT_EQ(data.line, 0);
+ EXPECT_TRUE(data.message.empty());
+}
+
} // namespace remoting
Original Bug Report
Potential OOB Heap Read in CRD SYSTEM Daemon via Malicious ETW Events
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 without the Chrome Security team.
Overview: The Chrome Remote Desktop (CRD) daemon on Windows contains a potential out-of-bounds heap read vulnerability when parsing Event Tracing for Windows (ETW) logs. An integer truncation bug in EventTraceData::Create allows a malicious ETW event to specify an out-of-bounds read offset, leading to heap memory leakage into CRD log files. A compromised low-privilege CRD process could exploit this to bypass ASLR by reading the generated logs.
Affected files:
remoting/host/win/event_trace_data.ccremoting/host/win/etw_trace_consumer.ccremoting/host/win/etw_trace_controller.ccremoting/host/daemon_process_win.cc
Estimated timestamp from git blame: 2025-11-16
Vulnerability Overview
The Chrome Remote Desktop (CRD) SYSTEM daemon (remoting_host.exe) on Windows is potentially vulnerable to an out-of-bounds (OOB) heap read. The issue resides in EventTraceData::Create when parsing LOG_MESSAGE_FULL ETW events. Missing bounds checks and an integer truncation vulnerability allow an attacker-controlled stack_depth value to generate a massive, out-of-bounds offset. Subsequent string operations then read arbitrary heap memory until a null byte is encountered.
If host logging is enabled (e.g., via the LogToFile registry key), this leaked memory is written to log files located in %ProgramFiles%. Because a compromised CRD network process running under LocalService has both the ability to inject ETW events and read files in %ProgramFiles%, this creates a SYSTEM-to-LocalService information leak, which can be used to defeat Address Space Layout Randomization (ASLR).
Technical Details
When the CRD daemon receives an ETW event, EtwTraceConsumerImpl::Core::DispatchEvent calls EventTraceData::Create(event) (remoting/host/win/event_trace_data.cc:54). If the event type is logging::LOG_MESSAGE_FULL, the MofData buffer is parsed:
- Missing Length Check: At line 80, a
DWORD(stack_depth) is read directly frommof_datawithout verifying thatevent->MofLength >= sizeof(DWORD). - Integer Truncation: At line 81,
int bytes_to_skip = sizeof(DWORD) + stack_depth * sizeof(intptr_t);is calculated. On a 64-bit system,sizeof(intptr_t)is 8. The multiplication evaluates as a 64-bitsize_t. If an attacker setsstack_depthto a crafted value like0x3FFFFFFE, the result0x1FFFFFFF0 + 4(0x1FFFFFFF4) is truncated when assigned to the signed 32-bitint bytes_to_skip, becoming-12(or0xFFFFFFF4). - Out-of-Bounds Offset: At line 82,
offset += bytes_to_skip;. Sinceoffsetis auint32_tinitialized to 0, adding-12results inoffset = 0xFFFFFFF4due to unsigned wrap-around. There is no bounds check to verifyoffset <= event->MofLength. - OOB Memory Reads: The code then attempts to read the line number, file name, and message using this massive offset.
- At line 85,
data.linereads 4 bytes frommof_data + 0xFFFFFFF4. - At line 92,
strnlen_s(file_info, event->MofLength - offset)is called. Sinceoffsetis huge, the subtraction underflows to a massive length, neutering thestrnlen_slimit. - At line 93,
base::UTF8ToWide(file_info)is called with aconst char*. This implicitly constructs astd::string_view, which callsstrlen(file_info), causing a continuous read from the out-of-bounds pointer until a null byte is found. - This same unbounded read via
strlenoccurs at line 101 fordata.message.assign(message).
- At line 85,
Potential Attack Scenario
These steps outline how an attacker might theoretically trigger the vulnerability. Note that these steps have not been verified with a functional proof-of-concept.
- Prerequisite: The target system has CRD installed and host logging enabled via
HKLM\SOFTWARE\Google\Chrome Remote Desktop\logging\LogToFile. - An attacker compromises the CRD network process, which runs with a restricted
LocalServicetoken. - The attacker uses the network process to generate and send a crafted
EVENT_TRACEto the ETW sessionchrome_remote_desktop_host_logger, specifying thelogging::kLogEventIdGUID andlogging::LOG_MESSAGE_FULLtype. - The crafted
MofDatapayload contains astack_depthchosen to induce truncation and forceoffsetto point to a valid, predictable heap memory region relative to theMofDataallocation. - The SYSTEM daemon parses the event, reads out-of-bounds heap memory, and logs it to
chrome_remote_desktop_*.login%ProgramFiles%\Google\Chrome Remote Desktop\<version>\. - The
LocalServiceaccount has default read permissions for%ProgramFiles%. The attacker reads the log file to extract the leaked SYSTEM heap data.
Suggested Fix
- Bounds Checking: Ensure
event->MofLengthis checked before readingstack_depth. - Safe Arithmetic: Use
base::CheckedNumericorsize_tforbytes_to_skipandoffsetcalculations to prevent truncation and wrap-around. Explicitly check thatoffset <= event->MofLengthat each step. - Safe String Handling: Replace the raw pointer manipulation and
strnlen_slogic withbase::span<const uint8_t>. Use span slicing and bounded string constructors (e.g., passing explicit lengths tostd::stringandbase::UTF8ToWide) instead of relying on null-termination.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.