CVE-2026-87525
Overview
Background
- Chromoting file chooser
- The Windows remoting host prompts for a file by having the SYSTEM-privileged
remoting_desktopprocess spawn a low-privilegedremoting_host.exe --type=file_chooserchild and read its serialized result back over an anonymous pipe. - `mojo::Message`
- A Mojo IPC message object that can be constructed directly from a raw byte span (as in
mojo::Message serialized_message(response_bytes, ...)), carrying a header that declares sizes and payload offsets. - `mojo::MessageHeaderValidator`
- A Mojo validator whose
Accept()method checks a message header for internal consistency (e.g. declarednum_bytesand payload pointers against the actual buffer) before the payload is trusted. - `DeserializeFromMessage`
- The generated
mojom::FileChooserResult::DeserializeFromMessageentry point that parses amojo::Messageinto aFileChooser::Result, relying on the header being well-formed.
Root Cause Analysis
In the pre-patch code (in file_chooser_win.cc), the SYSTEM process wrapped the raw pipe bytes directly in a mojo::Message and immediately called mojom::FileChooserResult::DeserializeFromMessage on them, with no prior header validation. As the commit message states, manual deserialization bypasses the standard mojo::MessageHeaderValidator, and the remaining payload bounds checks rely on DCHECKs that are compiled out in release builds. A compromised child process could therefore return crafted header fields — for example an oversized num_bytes or a manipulated relative payload pointer — that the trusting deserializer would follow, producing an out-of-bounds heap read in the SYSTEM process.
The fix extracts ParseFileChooserResponse() into file_chooser_common_win.cc, which first runs mojo::MessageHeaderValidator::Accept() on the message and returns a FileTransfer_Error (FileTransfer_Error_Type_UNEXPECTED_ERROR) if validation fails, so DeserializeFromMessage only runs on a header-consistent message. This works because the added validator.Accept() guard executes before the deserialization sink, rejecting malformed headers (the shipped tests cover invalid num_bytes, an OOB V2 payload pointer, an empty buffer, and truncated headers) rather than parsing them.
mojo::MessageHeaderValidator::Accept() check ahead of DeserializeFromMessage and bails out safely when it fails.Attack Path
- Compromise the child process
An attacker gains control of the low-privileged
remoting_host.exe --type=file_chooserchild that the SYSTEMremoting_desktopprocess spawns to prompt for a file. (inferred) - Return crafted response bytes
Instead of a well-formed
FileChooserResult, the child writes crafted bytes to the anonymous pipe with malformed Mojo header fields (e.g. an oversizednum_bytesor an out-of-range relative payload pointer, as exercised byRejectMalformedHeaderWithInvalidNumBytesandRejectMalformedHeaderWithV2PayloadPointerOOB). - Unvalidated wrap and deserialize
In the pre-patch path the SYSTEM process wraps those bytes in a
mojo::Messageand callsmojom::FileChooserResult::DeserializeFromMessagewith no header validation, so the deserializer follows the attacker’s size/offset fields. - Out-of-bounds read in SYSTEM process Following the crafted header causes the deserializer to read heap memory outside the received buffer in the SYSTEM-privileged process; the precise OOB read location lives inside the Mojo deserialization code, which is not shown in the diff. (inferred)
Impact Assessment
remoting_desktop process, triggered by a compromised low-privileged file-chooser child returning crafted response bytes over the pipe. The preconditions are that file-chooser prompting is active on Windows and that the attacker already controls (or can impersonate) the child process feeding the pipe. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 2.7. The advisory reports no exploited-in-the-wild status or severity rating.Changed Functions
| Function | Change | Notes |
|---|---|---|
FileChooserWinTestremoting/host/file_transfer/file_chooser_win_unittest.cc |
modified | |
TEST_Fremoting/host/file_transfer/file_chooser_win_unittest.cc |
modified | |
forremoting/host/file_transfer/file_chooser_win_unittest.cc |
modified | |
ifremoting/host/file_transfer/BUILD.gn |
modified |
Files Changed
remoting/host/file_transfer/file_chooser_win_unittest.ccremoting/host/file_transfer/file_chooser_win.ccremoting/host/file_transfer/file_chooser_common_win.hremoting/host/file_transfer/file_chooser_common_win.ccremoting/host/file_transfer/BUILD.gn
Audit Directions
- Manual Mojo deserialization across trust boundariesSearch for direct
mojo::Messageconstruction from raw bytes followed byDeserializeFromMessage/Deserializewithout a precedingmojo::MessageHeaderValidator::Accept(), especially where the bytes originate from a lower-privilege process or pipe. - DCHECK-only bounds enforcementFlag deserialization or parsing paths whose bounds/size safety depends on
DCHECKs (compiled out in release) rather than runtime checks, since release builds then trust attacker-controlled sizes and offsets. - Privilege-boundary raw pipesReview other SYSTEM/elevated host components (particularly under
remoting/host) that read serialized payloads from spawned low-privilege children over anonymous pipes, per the CL’s own TODO to replace raw pipe serialization with a standard Mojo IPC channel.
Patch
From fbce880a18489ef2eae947127722f97dfb9a7062 Mon Sep 17 00:00:00 2001 From: Joe Downing <[email protected]> Date: Fri, 21 Aug 2026 16:50:04 -0700 Subject: [PATCH] Validate Mojo message header in FileChooserWindows When prompting the user for a file on Windows, the SYSTEM-privileged remoting_desktop process spawns a low-privileged child process (remoting_host.exe --type=file_chooser) and reads the serialized FileChooserResult response over an anonymous pipe. Previously, the SYSTEM process wrapped the raw pipe bytes in a mojo::Message and called mojom::FileChooserResult::DeserializeFromMessage directly. Because manual deserialization bypasses the standard Mojo MessageHeaderValidator and payload bounds checks rely on DCHECKs in release builds, a compromised child could supply crafted Mojo headers (such as invalid num_bytes or manipulated relative payload pointers) leading to out-of-bounds heap reads in the SYSTEM process. This CL: 1. Extracts ParseFileChooserResponse() to validate incoming message bytes with mojo::MessageHeaderValidator before deserialization. 2. Returns a FileTransfer_Error if validation fails, preventing OOB reads. 3. Adds FileChooserWinTest to verify that valid responses deserialize correctly and malformed headers are rejected safely. 4. Adds a TODO to replace raw pipe serialization with a standard Mojo IPC channel across this privilege boundary. Bug: 517336350 Change-Id: Ic65bbf494e6058a7b01aed74effa8a55203ce551 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8281486 Reviewed-by: Yuwei Huang <[email protected]> Commit-Queue: Joe Downing <[email protected]> Cr-Commit-Position: refs/heads/main@{#1684315} --- diff --git a/remoting/host/file_transfer/file_chooser_common_win.cc b/remoting/host/file_transfer/file_chooser_common_win.cc new file mode 100644 index 0000000..f65677c --- /dev/null +++ b/remoting/host/file_transfer/file_chooser_common_win.cc @@ -0,0 +1,43 @@ +// 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 "remoting/host/file_transfer/file_chooser_common_win.h" + +#include <utility> + +#include "base/logging.h" +#include "mojo/public/cpp/bindings/message.h" +#include "mojo/public/cpp/bindings/message_header_validator.h" +#include "remoting/host/mojom/desktop_session.mojom.h" +#include "remoting/protocol/file_transfer_helpers.h" + +namespace remoting { + +FileChooser::Result ParseFileChooserResponse( + base::span<const uint8_t> response_bytes) { + // TODO(crbug.com/517336350): Replace raw pipe serialization with a standard + // Mojo IPC channel to avoid manual deserialization across privilege + // boundaries. + mojo::Message serialized_message(response_bytes, + base::span<mojo::ScopedHandle>()); + + mojo::MessageHeaderValidator validator; + if (!validator.Accept(&serialized_message)) { + LOG(ERROR) << "Failed to validate message header from file chooser."; + return protocol::MakeFileTransferError( + FROM_HERE, protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR); + } + + FileChooser::Result result; + if (!mojom::FileChooserResult::DeserializeFromMessage( + std::move(serialized_message), &result)) { + LOG(ERROR) << "Failed to deserialize response."; + return protocol::MakeFileTransferError( + FROM_HERE, protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR); + } + + return result; +} + +} // namespace remoting diff --git a/remoting/host/file_transfer/file_chooser_common_win.h b/remoting/host/file_transfer/file_chooser_common_win.h index 5d82d5c..cf97266 100644 --- a/remoting/host/file_transfer/file_chooser_common_win.h +++ b/remoting/host/file_transfer/file_chooser_common_win.h @@ -6,6 +6,10 @@ #define REMOTING_HOST_FILE_TRANSFER_FILE_CHOOSER_COMMON_WIN_H_ #include <cstddef> +#include <cstdint> + +#include "base/containers/span.h" +#include "remoting/host/file_transfer/file_chooser.h" namespace remoting { @@ -17,6 +21,12 @@ // likely to see on Windows. constexpr std::size_t kFileChooserPipeBufferSize = 4096; +// Parses and deserializes the response bytes received from the file chooser +// child process. Validates the Mojo message header to ensure safe handling +// across privilege boundaries. +FileChooser::Result ParseFileChooserResponse( + base::span<const uint8_t> response_bytes); + } // namespace remoting #endif // REMOTING_HOST_FILE_TRANSFER_FILE_CHOOSER_COMMON_WIN_H_ diff --git a/remoting/host/file_transfer/file_chooser_win.cc b/remoting/host/file_transfer/file_chooser_win.cc index 5c5915eb..40e3ce0b 100644 --- a/remoting/host/file_transfer/file_chooser_win.cc +++ b/remoting/host/file_transfer/file_chooser_win.cc @@ -23,11 +23,9 @@ #include "base/task/sequenced_task_runner.h" #include "base/win/object_watcher.h" #include "base/win/scoped_handle.h" -#include "mojo/public/cpp/bindings/message.h" #include "remoting/host/base/host_exit_codes.h" #include "remoting/host/base/switches.h" #include "remoting/host/file_transfer/file_chooser_common_win.h" -#include "remoting/host/mojom/desktop_session.mojom.h" namespace remoting { @@ -171,18 +169,8 @@ return; } - mojo::Message serialized_message(base::span(response_bytes).first(bytes_read), - base::span<mojo::ScopedHandle>()); - - FileChooser::Result result; - if (!mojom::FileChooserResult::DeserializeFromMessage( - std::move(serialized_message), &result)) { - LOG(ERROR) << "Failed to deserialize response."; - std::move(callback_).Run(MakeFileTransferError( - FROM_HERE, protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR)); - return; - } - + FileChooser::Result result = + ParseFileChooserResponse(base::span(response_bytes).first(bytes_read)); std::move(callback_).Run(std::move(result)); } diff --git a/remoting/host/file_transfer/file_chooser_win_unittest.cc b/remoting/host/file_transfer/file_chooser_win_unittest.cc new file mode 100644 index 0000000..364d40a --- /dev/null +++ b/remoting/host/file_transfer/file_chooser_win_unittest.cc @@ -0,0 +1,106 @@ +// 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 "remoting/host/file_transfer/file_chooser.h" + +#include <cstdint> +#include <vector> + +#include "base/containers/span.h" +#include "base/files/file_path.h" +#include "base/test/task_environment.h" +#include "mojo/public/cpp/bindings/message.h" +#include "remoting/host/file_transfer/file_chooser_common_win.h" +#include "remoting/host/mojom/desktop_session.mojom.h" +#include "remoting/protocol/file_transfer_helpers.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace remoting { + +class FileChooserWinTest : public testing::Test { + protected: + base::test::TaskEnvironment task_environment_; +}; + +TEST_F(FileChooserWinTest, ParseValidSuccessResponse) { + base::FilePath test_path(FILE_PATH_LITERAL("C:\\test\\file.txt")); + FileChooser::Result input(test_path); + mojo::Message serialized_message = + mojom::FileChooserResult::SerializeAsMessage(&input); + + FileChooser::Result result = + ParseFileChooserResponse(serialized_message.data_as_span()); + + ASSERT_TRUE(result.is_success()); + EXPECT_EQ(result.success(), test_path); +} + +TEST_F(FileChooserWinTest, ParseValidErrorResponse) { + protocol::FileTransfer_Error error = MakeFileTransferError( + FROM_HERE, protocol::FileTransfer_Error_Type_CANCELED); + FileChooser::Result input(error); + mojo::Message serialized_message = + mojom::FileChooserResult::SerializeAsMessage(&input); + + FileChooser::Result result = + ParseFileChooserResponse(serialized_message.data_as_span()); + + ASSERT_TRUE(result.is_error()); + EXPECT_EQ(result.error().type(), protocol::FileTransfer_Error_Type_CANCELED); +} + +TEST_F(FileChooserWinTest, RejectMalformedHeaderWithInvalidNumBytes) { + // Vector A: 8-byte V0 header with num_bytes (0x28 = 40) larger than buffer. + const uint8_t malformed_bytes[8] = {0x28, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00}; + FileChooser::Result result = ParseFileChooserResponse(malformed_bytes); + + ASSERT_TRUE(result.is_error()); + EXPECT_EQ(result.error().type(), + protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR); +} + +TEST_F(FileChooserWinTest, RejectMalformedHeaderWithV2PayloadPointerOOB) { + // Vector B: 48-byte V2 header with payload offset pointing outside buffer. + uint8_t malformed_bytes[48] = {0}; + // num_bytes = 48 + malformed_bytes[0] = 48; + // version = 2 + malformed_bytes[4] = 2; + // payload offset pointing 0x1000 bytes away + malformed_bytes[32] = 0x00; + malformed_bytes[33] = 0x10; + malformed_bytes[40] = 0x00; + malformed_bytes[41] = 0x10; + + FileChooser::Result result = ParseFileChooserResponse(malformed_bytes); + + ASSERT_TRUE(result.is_error()); + EXPECT_EQ(result.error().type(), + protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR); +} + +TEST_F(FileChooserWinTest, RejectEmptyBuffer) { + FileChooser::Result result = + ParseFileChooserResponse(base::span<const uint8_t>()); + + ASSERT_TRUE(result.is_error()); + EXPECT_EQ(result.error().type(), + protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR); +} + +TEST_F(FileChooserWinTest, RejectTruncatedHeaders) { + // Test various truncated buffer sizes smaller than full Mojo message headers. + for (size_t size : {1, 4, 7, 16, 24}) { + std::vector<uint8_t> truncated_bytes(size, 0); + FileChooser::Result result = ParseFileChooserResponse(truncated_bytes); + + ASSERT_TRUE(result.is_error()) << "Failed for size: " << size; + EXPECT_EQ(result.error().type(), + protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR) + << "Failed for size: " << size; + } +} + +} // namespace remoting diff --git a/remoting/host/file_transfer/BUILD.gn b/remoting/host/file_transfer/BUILD.gn index 0775683..fcf54e2 100644 --- a/remoting/host/file_transfer/BUILD.gn +++ b/remoting/host/file_transfer/BUILD.gn @@ -20,7 +20,6 @@ frameworks = [ "AppKit.framework" ] } else if (is_win) { sources += [ - "file_chooser_common_win.h", "file_chooser_main_win.cc", "file_chooser_win.cc", ] @@ -91,9 +90,11 @@ "ensure_user_mac.cc", ] } else if (is_win) { + public += [ "file_chooser_common_win.h" ] sources += [ "directory_helpers_win.cc", "ensure_user_win.cc", + "file_chooser_common_win.cc", ] } else if (is_chromeos) { sources += [ @@ -182,6 +183,9 @@ ] } else { sources += [ "local_file_operations_unittest.cc" ] + if (is_win) { + sources += [ "file_chooser_win_unittest.cc" ] + } if (remoting_multi_process) { sources += [ "ipc_file_operations_unittest.cc" ] }
Regression Test / PoC
diff --git a/remoting/host/file_transfer/file_chooser_win_unittest.cc b/remoting/host/file_transfer/file_chooser_win_unittest.cc
new file mode 100644
index 0000000..364d40a
--- /dev/null
+++ b/remoting/host/file_transfer/file_chooser_win_unittest.cc
@@ -0,0 +1,106 @@
+// 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 "remoting/host/file_transfer/file_chooser.h"
+
+#include <cstdint>
+#include <vector>
+
+#include "base/containers/span.h"
+#include "base/files/file_path.h"
+#include "base/test/task_environment.h"
+#include "mojo/public/cpp/bindings/message.h"
+#include "remoting/host/file_transfer/file_chooser_common_win.h"
+#include "remoting/host/mojom/desktop_session.mojom.h"
+#include "remoting/protocol/file_transfer_helpers.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace remoting {
+
+class FileChooserWinTest : public testing::Test {
+ protected:
+ base::test::TaskEnvironment task_environment_;
+};
+
+TEST_F(FileChooserWinTest, ParseValidSuccessResponse) {
+ base::FilePath test_path(FILE_PATH_LITERAL("C:\\test\\file.txt"));
+ FileChooser::Result input(test_path);
+ mojo::Message serialized_message =
+ mojom::FileChooserResult::SerializeAsMessage(&input);
+
+ FileChooser::Result result =
+ ParseFileChooserResponse(serialized_message.data_as_span());
+
+ ASSERT_TRUE(result.is_success());
+ EXPECT_EQ(result.success(), test_path);
+}
+
+TEST_F(FileChooserWinTest, ParseValidErrorResponse) {
+ protocol::FileTransfer_Error error = MakeFileTransferError(
+ FROM_HERE, protocol::FileTransfer_Error_Type_CANCELED);
+ FileChooser::Result input(error);
+ mojo::Message serialized_message =
+ mojom::FileChooserResult::SerializeAsMessage(&input);
+
+ FileChooser::Result result =
+ ParseFileChooserResponse(serialized_message.data_as_span());
+
+ ASSERT_TRUE(result.is_error());
+ EXPECT_EQ(result.error().type(), protocol::FileTransfer_Error_Type_CANCELED);
+}
+
+TEST_F(FileChooserWinTest, RejectMalformedHeaderWithInvalidNumBytes) {
+ // Vector A: 8-byte V0 header with num_bytes (0x28 = 40) larger than buffer.
+ const uint8_t malformed_bytes[8] = {0x28, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00};
+ FileChooser::Result result = ParseFileChooserResponse(malformed_bytes);
+
+ ASSERT_TRUE(result.is_error());
+ EXPECT_EQ(result.error().type(),
+ protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR);
+}
+
+TEST_F(FileChooserWinTest, RejectMalformedHeaderWithV2PayloadPointerOOB) {
+ // Vector B: 48-byte V2 header with payload offset pointing outside buffer.
+ uint8_t malformed_bytes[48] = {0};
+ // num_bytes = 48
+ malformed_bytes[0] = 48;
+ // version = 2
+ malformed_bytes[4] = 2;
+ // payload offset pointing 0x1000 bytes away
+ malformed_bytes[32] = 0x00;
+ malformed_bytes[33] = 0x10;
+ malformed_bytes[40] = 0x00;
+ malformed_bytes[41] = 0x10;
+
+ FileChooser::Result result = ParseFileChooserResponse(malformed_bytes);
+
+ ASSERT_TRUE(result.is_error());
+ EXPECT_EQ(result.error().type(),
+ protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR);
+}
+
+TEST_F(FileChooserWinTest, RejectEmptyBuffer) {
+ FileChooser::Result result =
+ ParseFileChooserResponse(base::span<const uint8_t>());
+
+ ASSERT_TRUE(result.is_error());
+ EXPECT_EQ(result.error().type(),
+ protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR);
+}
+
+TEST_F(FileChooserWinTest, RejectTruncatedHeaders) {
+ // Test various truncated buffer sizes smaller than full Mojo message headers.
+ for (size_t size : {1, 4, 7, 16, 24}) {
+ std::vector<uint8_t> truncated_bytes(size, 0);
+ FileChooser::Result result = ParseFileChooserResponse(truncated_bytes);
+
+ ASSERT_TRUE(result.is_error()) << "Failed for size: " << size;
+ EXPECT_EQ(result.error().type(),
+ protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR)
+ << "Failed for size: " << size;
+ }
+}
+
+} // namespace remoting
Original Bug Report
Potential unvalidated Mojo header deserialization in CRD Windows host leads to SYSTEM OOB read
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 Chrome Remote Desktop Windows host service running as SYSTEM manually deserializes Mojo message bytes received over a pipe from a low-privileged user-session child process. This manual deserialization bypasses standard Mojo message header validation, potentially allowing a compromised child process to trigger out-of-bounds heap reads in the SYSTEM process.
Affected files:
remoting/host/file_transfer/file_chooser_win.ccremoting/host/file_transfer/local_file_operations.cc
Estimated timestamp from git blame: 2022-08-09
Summary and Privilege Boundary
In Chrome Remote Desktop (CRD) on Windows, the high-privilege integration service (remoting_desktop.exe) runs with local SYSTEM privileges. To prompt the user with a file picker, it spawns a child process (remoting_host.exe --type=file_chooser) running in the context of the logged-in user.
The parent process reads serialized data from the child’s STDOUT pipe and deserializes it into a mojom::FileChooserResult union. However, this is done manually without standard Mojo validation, creating a potential out-of-bounds (OOB) heap read vulnerability in the SYSTEM-privileged parent process.
Vulnerability Analysis
In remoting/host/file_transfer/file_chooser_win.cc (lines 174-179), the parent reads the response from the pipe into a local buffer and attempts to deserialize it directly:
mojo::Message serialized_message(base::span(response_bytes).first(bytes_read),
base::span<mojo::ScopedHandle>());
FileChooser::Result result;
if (!mojom::FileChooserResult::DeserializeFromMessage(
std::move(serialized_message), &result)) {
// ...
}
Standard Mojo IPC channels route incoming messages through mojo::internal::MessageHeaderValidator, which ensures the message header, version, and offsets are safe before any parsing or dispatching occurs. In contrast, manual deserialization via DeserializeFromMessage bypasses MessageHeaderValidator entirely.
In production/official builds, the safety checks in mojo::Message::payload() and mojo::Message::payload_num_bytes() rely on DCHECK macros which are compiled out. An attacker who can control the output of the low-privilege child process can craft a malformed Mojo message:
- Vector A (Relative Pointer Manipulation): For messages with
version >= 2,Message::payload()callsheader_v2()->payload.Get(), which invokesDecodePointerto resolve the payload’s location using a relative 64-bit offset. An attacker can set this offset to an arbitrary relative distance, pointing the payload pointer directly into other areas of the parent process’s heap. - Vector B (Payload Size Integer Underflow): For messages with
version < 2,payload_num_bytes()calculatesdata_num_bytes() - header()->num_bytes. Ifheader()->num_bytesis larger than the number of bytes read from the pipe, this subtraction underflows, initializing theValidationContextwith a massive size. The validator then treats the OOB heap memory as valid space.
If the OOB data structure successfully validates as a FileChooserResult with an error tag, the remoting::mojom::FileTransferError traits will extract arbitrary null-terminated strings from the OOB memory to populate the error’s function and source_file fields. This error object is subsequently sent back to the remote client, potentially exposing sensitive SYSTEM-level heap contents (such as ASLR-defeating pointers or secret data) to the attacker.
Potential Attack Steps
Note: These are suggested/potential steps modeled from static code analysis; our tooling does not currently run active proofs of concept.
- A local attacker in the user session initiates a remote file transfer request.
- The high-privilege parent process launches the child file chooser process under the user’s token.
- The attacker terminates or injects into the newly created child process, seizing control of its STDOUT handle.
- The attacker writes a crafted 32-byte Mojo message header to STDOUT where
versionis set to 2, and thepayloadoffset points to a targeted relative location on the parent process heap. - The child exits successfully, triggering
OnObjectSignaledin the parent. - The parent process reads the malformed message, validates the layout against the controlled OOB memory, and extracts strings from the OOB location.
- The parent returns the extracted strings back to the remote client within a
FileTransfer_Errorstructure, leaking the contents of the parent’s heap.
Suggested Remediation
Instead of manually constructing and parsing raw bytes over an anonymous pipe, the file-chooser communication should be refactored to use a standard, validated Mojo IPC channel (such as an associated receiver/remote pair passed during process launch).
If manual deserialization must be maintained, the incoming message must be explicitly validated using a mojo::internal::MessageHeaderValidator or manual bounds checks prior to accessing the message’s payload or calling DeserializeFromMessage.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.