CVE-2026-84325
Overview
Background
- `exo`
- The ChromeOS Wayland compositor server (
components/exo) that brokers windowing, drag-drop, and clipboard traffic between the Chrome host and guest environments such as Crostini and ARC. - ARC VM (`arc::kArcVmName`)
- The Android runtime on ChromeOS running inside a virtual machine, identified as endpoint
ui::EndpointType::kArc, whose file namespace is not directly mounted into the host. - `SecurityDelegate`
- The
exointerface (ChromeSecurityDelegate) responsible for validating and translating guest-supplied data, including turning guest VM file URIs into hostFileInfopaths. - `FileInfo`
- A
ui::FileInforecord carrying a filesystempath(and optionaldisplay_name) that flows into the browser’s clipboard and drag-drop data as a real host file reference.
Root Cause Analysis
When a guest transferred data over exo via clipboard or drag-drop, ash::TranslateVMToHost (reached through ChromeSecurityDelegate::GetFilenames) parsed the text/uri-list payload and produced host FileInfo paths for every VM endpoint, including ui::EndpointType::kArc.
The invariant that was violated is that a guest should only be able to name paths within its own shared, host-mapped namespace: for the ARC VM there is no legitimate exo mechanism to send file paths, so any file:// URI it supplied was passed through and interpreted as a raw host path (e.g. file:///file1 became host path /file1) rather than being rejected.
The fix adds an early if (vm_name == arc::kArcVmName ...) return {}; guard, gated by the default-enabled kChromeSecurityDelegateIgnoreArcVm feature, so ARC-originated paths are dropped before any host translation occurs. This works because ARC has no supported path-transfer flow today, so returning an empty vector removes the attacker-controlled path injection surface without breaking a real feature, while the feature flag leaves a kill switch if support is added later.
TranslateVMToHost to return no paths for arc::kArcVmName.Attack Path
- Guest positioning
A malicious or compromised Android app running in the ARC VM initiates a drag-drop or clipboard operation targeting the Chrome host through
exo. - Craft URI payload
The app supplies a
text/uri-listclipboard/drag payload containing attacker-chosenfile://URIs such asfile:///file1, naming host paths outside any ARC-shared directory. - Bypass translation
ChromeSecurityDelegate::GetFilenamesinvokesTranslateVMToHostfor thekArcendpoint, which (pre-fix) accepted the URIs and emittedFileInfoentries pointing at raw host paths. - Deliver forged file references
The resulting host
FileInfopaths are handed to the browser’s clipboard/drag-drop machinery as if they were legitimately shared files, so a subsequent paste or drop references attacker-named host files.
Impact Assessment
exo SecurityDelegate, and requires an already-present malicious or compromised Android app in ARC plus a user-visible clipboard/drag interaction to land the forged paths. The gain is improper input validation leading to unauthorized host file path exposure, not a direct memory-safety corruption.Files Changed
chrome/browser/ash/exo/chrome_security_delegate.ccchrome/browser/ash/exo/chrome_security_delegate.hchrome/browser/ash/exo/chrome_security_delegate_unittest.cc
Audit Directions
- Endpoint-specific validationAudit every
SecurityDelegatepath-translation routine (TranslateVMToHost,TranslateVMPathsToHost,GetFilenames) to confirm eachui::EndpointTypeonly yields paths within its own host-mapped shared namespace and rejects endpoints that lack a legitimate transfer channel. - Guest-to-host path trustReview clipboard and drag-drop ingestion of
text/uri-listandfile://URIs acrossexofor places that convert guest-supplied strings into hostFileInfo/FilePathwithout checking against registered shared-path roots. - Default-on feature guardsVerify that security-hardening feature flags like
kChromeSecurityDelegateIgnoreArcVmremainFEATURE_ENABLED_BY_DEFAULTand are not silently disabled, and that disabling them cannot re-open the path-injection surface in production.
Patch
From dfc786d36aee91a37fa0b111051788d8eabf7942 Mon Sep 17 00:00:00 2001
From: Joel Hockey <joelhockey@chromium.org>
Date: Thu, 27 Aug 2026 16:28:08 -0700
Subject: [PATCH] Ignore paths from kArcVm in TranslateVMToHost()
Arc doesn't currently support sending file paths in drag-drop or
clipboard via exo. If support is ever added we must be sure to
map paths correctly.
Bug: 553117928
Change-Id: Ifc145c0868e11ac8d1715626039a141019a4f1e1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8303173
Reviewed-by: maciek swiech <drmasquatch@google.com>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1687650}
---
diff --git a/chrome/browser/ash/exo/chrome_security_delegate.cc b/chrome/browser/ash/exo/chrome_security_delegate.cc
index e337d2a..fb2f8eb 100644
--- a/chrome/browser/ash/exo/chrome_security_delegate.cc
+++ b/chrome/browser/ash/exo/chrome_security_delegate.cc
@@ -41,6 +41,9 @@
namespace ash {
+BASE_FEATURE(kChromeSecurityDelegateIgnoreArcVm,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+
namespace {
constexpr char kUriListSeparator[] = "\r\n";
@@ -111,6 +114,12 @@
// Translate |vm_paths| from |source| VM to host paths.
std::vector<FileInfo> TranslateVMToHost(const std::string& vm_name,
std::vector<ui::FileInfo> vm_paths) {
+ // Arc doesn't currently support drag-drop or clipboard via exo.
+ // If Arc ever adds support, we must map paths correctly like other VMs.
+ if (vm_name == arc::kArcVmName &&
+ base::FeatureList::IsEnabled(kChromeSecurityDelegateIgnoreArcVm)) {
+ return {};
+ }
std::vector<FileInfo> file_infos;
Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
bool is_crostini = vm_name == crostini::kCrostiniDefaultVmName;
diff --git a/chrome/browser/ash/exo/chrome_security_delegate.h b/chrome/browser/ash/exo/chrome_security_delegate.h
index c89a659a..78ecc654 100644
--- a/chrome/browser/ash/exo/chrome_security_delegate.h
+++ b/chrome/browser/ash/exo/chrome_security_delegate.h
@@ -5,11 +5,14 @@
#ifndef CHROME_BROWSER_ASH_EXO_CHROME_SECURITY_DELEGATE_H_
#define CHROME_BROWSER_ASH_EXO_CHROME_SECURITY_DELEGATE_H_
+#include "base/feature_list.h"
#include "components/exo/security_delegate.h"
#include "storage/browser/file_system/file_system_url.h"
namespace ash {
+BASE_DECLARE_FEATURE(kChromeSecurityDelegateIgnoreArcVm);
+
// Translate paths from |source| VM to valid paths in the host. Invalid paths
// are ignored.
std::vector<base::FilePath> TranslateVMPathsToHost(
diff --git a/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc b/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc
index 06e147e..fab2c5f3 100644
--- a/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc
+++ b/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc
@@ -13,6 +13,7 @@
#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_security_delegate.h"
@@ -198,24 +199,40 @@
guest_os_share_path->RegisterSharedPath(bruschetta::kBruschettaVmName,
shared_path);
- // Multiple lines should be parsed.
- // Arc should not translate paths.
- std::vector<ui::FileInfo> files = security_delegate.GetFilenames(
- ui::EndpointType::kArc,
- Data("\n\tfile:///file1\t\r\n#ignore\r\nfile:///file2\r\n"));
- EXPECT_EQ(2u, files.size());
- EXPECT_EQ("/file1", files[0].path.value());
- EXPECT_EQ("", files[0].display_name.value());
- EXPECT_EQ("/file2", files[1].path.value());
- EXPECT_EQ("", files[1].display_name.value());
+ std::vector<ui::FileInfo> files;
+ // When kChromeSecurityDelegateIgnoreArcVm is enabled, Arc paths should be
+ // ignored.
+ {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(kChromeSecurityDelegateIgnoreArcVm);
+ files = security_delegate.GetFilenames(
+ ui::EndpointType::kArc, Data("file:///file1\r\nfile:///file2"));
+ EXPECT_TRUE(files.empty());
+ }
+
+ // When feature is disabled, Arc should not translate paths.
+ {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(kChromeSecurityDelegateIgnoreArcVm);
+ files = security_delegate.GetFilenames(
+ ui::EndpointType::kArc, Data("file:///file1\r\nfile:///file2"));
+ EXPECT_EQ(2u, files.size());
+ EXPECT_EQ("/file1", files[0].path.value());
+ EXPECT_EQ("", files[0].display_name.value());
+ EXPECT_EQ("/file2", files[1].path.value());
+ EXPECT_EQ("", files[1].display_name.value());
+ }
// Crostini shared paths should be mapped.
+ // Multiple lines should be parsed.
guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina");
files = crostini_security_delegate.GetFilenames(
ui::EndpointType::kCrostini,
- Data("file:///mnt/chromeos/MyFiles/shared/file"));
- EXPECT_EQ(1u, files.size());
- EXPECT_EQ(shared_path.Append("file"), files[0].path);
+ Data("\n\tfile:///mnt/chromeos/MyFiles/shared/file1\t\r\n"
+ "#ignore\r\nfile:///mnt/chromeos/MyFiles/shared/file2\r\n"));
+ EXPECT_EQ(2u, files.size());
+ EXPECT_EQ(shared_path.Append("file1"), files[0].path);
+ EXPECT_EQ(shared_path.Append("file2"), files[1].path);
// Crostini homedir should be mapped.
files = crostini_security_delegate.GetFilenames(
Regression Test / PoC
diff --git a/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc b/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc
index 06e147e..fab2c5f3 100644
--- a/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc
+++ b/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc
@@ -13,6 +13,7 @@
#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_security_delegate.h"
@@ -198,24 +199,40 @@
guest_os_share_path->RegisterSharedPath(bruschetta::kBruschettaVmName,
shared_path);
- // Multiple lines should be parsed.
- // Arc should not translate paths.
- std::vector<ui::FileInfo> files = security_delegate.GetFilenames(
- ui::EndpointType::kArc,
- Data("\n\tfile:///file1\t\r\n#ignore\r\nfile:///file2\r\n"));
- EXPECT_EQ(2u, files.size());
- EXPECT_EQ("/file1", files[0].path.value());
- EXPECT_EQ("", files[0].display_name.value());
- EXPECT_EQ("/file2", files[1].path.value());
- EXPECT_EQ("", files[1].display_name.value());
+ std::vector<ui::FileInfo> files;
+ // When kChromeSecurityDelegateIgnoreArcVm is enabled, Arc paths should be
+ // ignored.
+ {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(kChromeSecurityDelegateIgnoreArcVm);
+ files = security_delegate.GetFilenames(
+ ui::EndpointType::kArc, Data("file:///file1\r\nfile:///file2"));
+ EXPECT_TRUE(files.empty());
+ }
+
+ // When feature is disabled, Arc should not translate paths.
+ {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(kChromeSecurityDelegateIgnoreArcVm);
+ files = security_delegate.GetFilenames(
+ ui::EndpointType::kArc, Data("file:///file1\r\nfile:///file2"));
+ EXPECT_EQ(2u, files.size());
+ EXPECT_EQ("/file1", files[0].path.value());
+ EXPECT_EQ("", files[0].display_name.value());
+ EXPECT_EQ("/file2", files[1].path.value());
+ EXPECT_EQ("", files[1].display_name.value());
+ }
// Crostini shared paths should be mapped.
+ // Multiple lines should be parsed.
guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina");
files = crostini_security_delegate.GetFilenames(
ui::EndpointType::kCrostini,
- Data("file:///mnt/chromeos/MyFiles/shared/file"));
- EXPECT_EQ(1u, files.size());
- EXPECT_EQ(shared_path.Append("file"), files[0].path);
+ Data("\n\tfile:///mnt/chromeos/MyFiles/shared/file1\t\r\n"
+ "#ignore\r\nfile:///mnt/chromeos/MyFiles/shared/file2\r\n"));
+ EXPECT_EQ(2u, files.size());
+ EXPECT_EQ(shared_path.Append("file1"), files[0].path);
+ EXPECT_EQ(shared_path.Append("file2"), files[1].path);
// Crostini homedir should be mapped.
files = crostini_security_delegate.GetFilenames(
Original Bug Report
Host path spoofing in ChromeOS exo allows ARCVM to expose host files on paste
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: In ChromeOS exo, TranslateVMToHost() explicitly exempts ARCVM from VM-to-host path translation, IsPathShared() validation, and vmfile: anti-spoofing prefixes. An ARCVM guest could potentially inject arbitrary host file paths into Ash’s clipboard or drag-and-drop operations as FilenamesType. A subsequent paste or drop gesture on a web page grants the renderer process read access to chronos-readable host profile files.
Affected files:
chrome/browser/ash/exo/chrome_security_delegate.cc
Estimated timestamp from git blame: 2024-02-29
Summary
TranslateVMToHost() in chrome/browser/ash/exo/chrome_security_delegate.cc is responsible for translating guest VM file paths to valid host paths and preventing VMs from spoofing host filesystem paths. However, an explicit exemption for ARCVM bypasses all path translation, path-sharing verification, and anti-spoofing prefixing. Consequently, a compromised ARCVM guest or malicious Android app could potentially plant arbitrary host file paths (e.g., /home/chronos/user/Login Data or /home/chronos/user/Cookies) into Ash’s system clipboard as FilenamesType. When a user pastes (Ctrl+V) or drops onto a web page, the browser grants the renderer ChildProcessSecurityPolicy read permissions and FileSystemAccess tokens for those host files.
Root Cause Analysis
In chrome/browser/ash/exo/chrome_security_delegate.cc:112-143:
std::vector<FileInfo> TranslateVMToHost(const std::string& vm_name,
std::vector<ui::FileInfo> vm_paths) {
std::vector<FileInfo> file_infos;
Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
bool is_crostini = vm_name == crostini::kCrostiniDefaultVmName;
for (ui::FileInfo& info : vm_paths) {
base::FilePath path = std::move(info.path);
storage::FileSystemURL url;
// Convert the VM path to a path in the host if possible. Otherwise, prefix
// it with 'vmfile:<vm_name>:' to avoid VMs spoofing host paths.
// E.g. crostini /etc/mime.types => vmfile:termina:/etc/mime.types.
if (!vm_name.empty() && vm_name != arc::kArcVmName) {
if (file_manager::util::ConvertPathInsideVMToFileSystemURL(
primary_profile, path, GetVmMount(vm_name),
/*map_crostini_home=*/is_crostini, &url)) {
path = url.path();
// Only allow write to clipboard for paths that are shared.
if (!IsPathShared(primary_profile, vm_name, is_crostini, path)) {
LOG(WARNING) << "Unshared file path: " << path;
continue;
}
} else {
path = base::FilePath(
base::StrCat({kVmFileScheme, ":", vm_name, ":", path.value()}));
}
}
file_infos.push_back({std::move(path), std::move(url)});
}
return file_infos;
}
ChromeSecurityDelegate::GetVmName(ui::EndpointType::kArc)(chrome_security_delegate.cc:376-381) returnsarc::kArcVmName("arcvm").- When clipboard data originating from ARC is processed,
TranslateVMToHost()checksif (!vm_name.empty() && vm_name != arc::kArcVmName). Becausevm_name == arc::kArcVmName, the entire security check is skipped. - The raw path parsed from the guest-supplied
text/uri-listpayload viaui::URIListToFileInfos()(ui/base/clipboard/file_info.cc:91-103) is pushed directly tofile_infosat line 140. Seat::OnFilenamesRead()(components/exo/seat.cc:334) commits these paths to the Ash clipboard viawriter->WriteFilenames().
Potential Impact via Web Paste or Drag & Drop
When a user pastes into a web page:
- Blink calls
SystemClipboard::ReadFiles()(third_party/blink/renderer/core/clipboard/system_clipboard.cc:350-365), sendingmojom::ClipboardHost::ReadFilesto the browser. - In
ClipboardHostImpl::OnReadFilesPolicyResult()(content/browser/renderer_host/clipboard_host_impl.cc:514-561),PrepareDataTransferFilenamesForChildProcess()(content/browser/file_system/browser_file_system_helper.cc:279-333) callssecurity_policy->GrantReadFile(child_id, filename.path)and registers an isolated file system. FileSystemAccessManagerImpl::ResolveDataTransferTokenWithFileType()(content/browser/file_system_access/file_system_access_manager_impl.cc:990-996) explicitly bypasses sensitive directory checks for files (HandleType::kFile).- The renderer is granted read access to the specified host files (such as
/home/chronos/user/Login Dataand/home/chronos/user/Cookies), allowing an attacker’s website to read sensitive profile information upon receiving a paste or drop event.
Potential Steps to Trigger
(Note: These are suggested/potential steps based on static code analysis; our tooling agent does not have the ability to run live code in the target environment.)
- From code running within the ARCVM guest (e.g., an untrusted Android application with a focused Wayland surface), create a
wl_data_sourceadvertisingtext/uri-list. - Set the clipboard selection by calling
wl_data_device.set_selection. - When Exo requests the data, supply file URIs targeting host files:
file:///home/chronos/user/Login%20Data\r\nfile:///home/chronos/user/Cookies\r\n. - Due to the exemption in
TranslateVMToHost, Ash stores these literal paths on the clipboard asFilenamesType. - Navigate the user’s browser to
https://attacker.example(e.g., via anACTION_VIEWintent). - When the user pastes (Ctrl+V) on the page, the browser grants
ChildProcessSecurityPolicyread access, and the page’s paste event handler reads the file contents via standard File / FileSystemAccess APIs.
Suggested Fix
Ensure ARCVM cannot pass unvalidated host paths to the clipboard or drag-and-drop operations. ARCVM paths should either be validated, converted using appropriate ARC URL translation mechanisms, or prefixed with vmfile:arcvm: / blocked if they do not correspond to legitimately shared directories.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.
Raised in root component due to access or custom field issues on 658562