High watch CVSS 2.7 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
2.7
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Chromoting
DescriptionOut of bounds read in Chromoting
ComponentChromoting
Bug ClassOOB
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.
Tracker517336350
Fix commitfbce880a1848 (chromium/src) +166/-15
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-08-21 (public git)
Disclosed2026-09-08
Patch gap18 days — fix public in source before the advisory

Background

Chromoting file chooser
The Windows remoting host prompts for a file by having the SYSTEM-privileged remoting_desktop process spawn a low-privileged remoting_host.exe --type=file_chooser child 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. declared num_bytes and payload pointers against the actual buffer) before the payload is trusted.
`DeserializeFromMessage`
The generated mojom::FileChooserResult::DeserializeFromMessage entry point that parses a mojo::Message into a FileChooser::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.

Key insight
The single core mistake was deserializing attacker-controlled pipe bytes across a privilege boundary without first validating the Mojo message header, so untrusted size/offset fields were trusted. The fix inserts a mojo::MessageHeaderValidator::Accept() check ahead of DeserializeFromMessage and bails out safely when it fails.

Attack Path

  1. Compromise the child process An attacker gains control of the low-privileged remoting_host.exe --type=file_chooser child that the SYSTEM remoting_desktop process spawns to prompt for a file. (inferred)
  2. 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 oversized num_bytes or an out-of-range relative payload pointer, as exercised by RejectMalformedHeaderWithInvalidNumBytes and RejectMalformedHeaderWithV2PayloadPointerOOB).
  3. Unvalidated wrap and deserialize In the pre-patch path the SYSTEM process wraps those bytes in a mojo::Message and calls mojom::FileChooserResult::DeserializeFromMessage with no header validation, so the deserializer follows the attacker’s size/offset fields.
  4. 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

Per the advisory this is an out-of-bounds read in Chromoting; the read occurs in the SYSTEM-privileged 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

FunctionChangeNotes
FileChooserWinTest
remoting/host/file_transfer/file_chooser_win_unittest.cc
modified
TEST_F
remoting/host/file_transfer/file_chooser_win_unittest.cc
modified
for
remoting/host/file_transfer/file_chooser_win_unittest.cc
modified
if
remoting/host/file_transfer/BUILD.gn
modified

Files Changed

  • remoting/host/file_transfer/file_chooser_win_unittest.cc
  • remoting/host/file_transfer/file_chooser_win.cc
  • remoting/host/file_transfer/file_chooser_common_win.h
  • remoting/host/file_transfer/file_chooser_common_win.cc
  • remoting/host/file_transfer/BUILD.gn

Audit Directions

  • Manual Mojo deserialization across trust boundaries
    Search for direct mojo::Message construction from raw bytes followed by DeserializeFromMessage/Deserialize without a preceding mojo::MessageHeaderValidator::Accept(), especially where the bytes originate from a lower-privilege process or pipe.
  • DCHECK-only bounds enforcement
    Flag 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 pipes
    Review 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.
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" ]
     }
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • remoting/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:

  1. Vector A (Relative Pointer Manipulation): For messages with version >= 2, Message::payload() calls header_v2()->payload.Get(), which invokes DecodePointer to 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.
  2. Vector B (Payload Size Integer Underflow): For messages with version < 2, payload_num_bytes() calculates data_num_bytes() - header()->num_bytes. If header()->num_bytes is larger than the number of bytes read from the pipe, this subtraction underflows, initializing the ValidationContext with 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.

  1. A local attacker in the user session initiates a remote file transfer request.
  2. The high-privilege parent process launches the child file chooser process under the user’s token.
  3. The attacker terminates or injects into the newly created child process, seizing control of its STDOUT handle.
  4. The attacker writes a crafted 32-byte Mojo message header to STDOUT where version is set to 2, and the payload offset points to a targeted relative location on the parent process heap.
  5. The child exits successfully, triggering OnObjectSignaled in the parent.
  6. The parent process reads the malformed message, validates the layout against the controlled OOB memory, and extracts strings from the OOB location.
  7. The parent returns the extracted strings back to the remote client within a FileTransfer_Error structure, 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.

View on issue tracker