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(