| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/exo/chrome_security_delegate.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.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" |
| #include "chrome/browser/ash/crostini/crostini_test_helper.h" |
| #include "chrome/browser/ash/crostini/crostini_util.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/guest_os/guest_os_security_delegate.h" |
| #include "chrome/browser/ash/guest_os/guest_os_share_path.h" |
| #include "chrome/browser/ash/plugin_vm/plugin_vm_util.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chromeos/ash/components/dbus/chunneld/chunneld_client.h" |
| #include "chromeos/ash/components/dbus/cicerone/cicerone_client.h" |
| #include "chromeos/ash/components/dbus/concierge/concierge_client.h" |
| #include "chromeos/ash/components/dbus/seneschal/fake_seneschal_client.h" |
| #include "chromeos/ash/components/dbus/seneschal/seneschal_client.h" |
| #include "chromeos/ui/base/app_types.h" |
| #include "chromeos/ui/base/window_properties.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "storage/browser/file_system/external_mount_points.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/client/window_types.h" |
| #include "ui/aura/test/test_window_delegate.h" |
| #include "ui/aura/test/test_windows.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/clipboard/file_info.h" |
| #include "ui/base/data_transfer_policy/data_transfer_endpoint.h" |
| #include "ui/compositor/layer_type.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| std::vector<uint8_t> Data(const std::string& s) { |
| return std::vector<uint8_t>(s.begin(), s.end()); |
| } |
| |
| void Capture(std::string* result, scoped_refptr<base::RefCountedMemory> data) { |
| *result = std::string(base::as_string_view(*data)); |
| } |
| |
| void CaptureUTF16(std::string* result, |
| scoped_refptr<base::RefCountedMemory> data) { |
| base::span<const uint8_t> bytes = *data; |
| std::u16string str(bytes.size() / 2u, u'\0'); |
| base::as_writable_byte_span(str).copy_from(bytes); |
| *result = base::UTF16ToUTF8(str); |
| } |
| |
| } // namespace |
| |
| class ChromeSecurityDelegateTest : public testing::Test { |
| public: |
| void SetUp() override { |
| ChunneldClient::InitializeFake(); |
| CiceroneClient::InitializeFake(); |
| ConciergeClient::InitializeFake(); |
| SeneschalClient::InitializeFake(); |
| |
| profile_ = std::make_unique<TestingProfile>(); |
| test_helper_ = |
| std::make_unique<crostini::CrostiniTestHelper>(profile_.get()); |
| |
| // Setup CrostiniManager for testing. |
| crostini::CrostiniManager* crostini_manager = |
| crostini::CrostiniManager::GetForProfile(profile_.get()); |
| crostini_manager->AddRunningVmForTesting(crostini::kCrostiniDefaultVmName); |
| crostini_manager->AddRunningContainerForTesting( |
| crostini::kCrostiniDefaultVmName, |
| crostini::ContainerInfo(crostini::kCrostiniDefaultContainerName, |
| "testuser", "/home/testuser", |
| "PLACEHOLDER_IP")); |
| |
| // Register MyFiles and Crostini. |
| mount_points_ = storage::ExternalMountPoints::GetSystemInstance(); |
| // Downloads-test%40example.com-hash |
| myfiles_mount_name_ = |
| file_manager::util::GetDownloadsMountPointName(profile_.get()); |
| // $HOME/Downloads |
| myfiles_dir_ = |
| file_manager::util::GetMyFilesFolderForProfile(profile_.get()); |
| mount_points_->RegisterFileSystem( |
| myfiles_mount_name_, storage::kFileSystemTypeLocal, |
| storage::FileSystemMountOption(), myfiles_dir_); |
| // crostini_test_termina_penguin |
| crostini_mount_name_ = |
| file_manager::util::GetCrostiniMountPointName(profile_.get()); |
| // /media/fuse/crostini_test_termina_penguin |
| crostini_dir_ = |
| file_manager::util::GetCrostiniMountDirectory(profile_.get()); |
| mount_points_->RegisterFileSystem( |
| crostini_mount_name_, storage::kFileSystemTypeLocal, |
| storage::FileSystemMountOption(), crostini_dir_); |
| } |
| |
| void TearDown() override { |
| mount_points_->RevokeAllFileSystems(); |
| test_helper_.reset(); |
| profile_.reset(); |
| SeneschalClient::Shutdown(); |
| ConciergeClient::Shutdown(); |
| CiceroneClient::Shutdown(); |
| ChunneldClient::Shutdown(); |
| } |
| |
| protected: |
| Profile* profile() { return profile_.get(); } |
| |
| content::BrowserTaskEnvironment task_environment_; |
| std::unique_ptr<TestingProfile> profile_; |
| std::unique_ptr<crostini::CrostiniTestHelper> test_helper_; |
| |
| raw_ptr<storage::ExternalMountPoints> mount_points_; |
| std::string myfiles_mount_name_; |
| base::FilePath myfiles_dir_; |
| std::string crostini_mount_name_; |
| base::FilePath crostini_dir_; |
| }; |
| |
| TEST_F(ChromeSecurityDelegateTest, CanLockPointer) { |
| auto security_delegate = std::make_unique<ChromeSecurityDelegate>(); |
| aura::Window container_window(nullptr, aura::client::WINDOW_TYPE_NORMAL); |
| container_window.Init(ui::LAYER_NOT_DRAWN); |
| aura::test::TestWindowDelegate delegate; |
| |
| // CanLockPointer should be allowed for arc and lacros, but not others. |
| std::unique_ptr<aura::Window> arc_toplevel( |
| aura::test::CreateTestWindowWithDelegate(&delegate, 0, gfx::Rect(), |
| &container_window)); |
| arc_toplevel->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP); |
| EXPECT_TRUE(security_delegate->CanLockPointer(arc_toplevel.get())); |
| |
| std::unique_ptr<aura::Window> lacros_toplevel( |
| aura::test::CreateTestWindowWithDelegate(&delegate, 0, gfx::Rect(), |
| &container_window)); |
| lacros_toplevel->SetProperty(chromeos::kAppTypeKey, |
| chromeos::AppType::LACROS); |
| EXPECT_TRUE(security_delegate->CanLockPointer(lacros_toplevel.get())); |
| |
| std::unique_ptr<aura::Window> crostini_toplevel( |
| aura::test::CreateTestWindowWithDelegate(&delegate, 0, gfx::Rect(), |
| &container_window)); |
| crostini_toplevel->SetProperty(chromeos::kAppTypeKey, |
| chromeos::AppType::CROSTINI_APP); |
| EXPECT_FALSE(security_delegate->CanLockPointer(crostini_toplevel.get())); |
| } |
| |
| TEST_F(ChromeSecurityDelegateTest, GetFilenames) { |
| ChromeSecurityDelegate security_delegate; |
| base::FilePath shared_path = myfiles_dir_.Append("shared"); |
| auto* guest_os_share_path = |
| guest_os::GuestOsSharePath::GetForProfile(profile()); |
| guest_os_share_path->RegisterSharedPath(crostini::kCrostiniDefaultVmName, |
| shared_path); |
| guest_os_share_path->RegisterSharedPath(plugin_vm::kPluginVmName, |
| shared_path); |
| 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()); |
| |
| // Crostini shared paths should be mapped. |
| 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); |
| |
| // Crostini homedir should be mapped. |
| files = crostini_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, Data("file:///home/testuser/file")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ(crostini_dir_.Append("file"), files[0].path); |
| |
| // Crostini internal paths should be mapped. |
| files = crostini_security_delegate.GetFilenames(ui::EndpointType::kCrostini, |
| Data("file:///etc/hosts")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ("vmfile:termina:/etc/hosts", files[0].path.value()); |
| |
| // Unshared paths should fail. |
| files = crostini_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, |
| Data("file:///mnt/chromeos/MyFiles/unshared/file")); |
| EXPECT_EQ(0u, files.size()); |
| files = crostini_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, |
| Data("file:///mnt/chromeos/MyFiles/shared/file1\r\n" |
| "file:///mnt/chromeos/MyFiles/unshared/file2")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ(shared_path.Append("file1"), files[0].path); |
| |
| // file:/path should fail. |
| files = crostini_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, Data("file:/mnt/chromeos/MyFiles/file")); |
| EXPECT_EQ(0u, files.size()); |
| |
| // file:path should fail. |
| files = crostini_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, Data("file:mnt/chromeos/MyFiles/file")); |
| EXPECT_EQ(0u, files.size()); |
| |
| // file:// should fail. |
| files = crostini_security_delegate.GetFilenames(ui::EndpointType::kCrostini, |
| Data("file://")); |
| EXPECT_EQ(0u, files.size()); |
| |
| // file:/// maps to internal root. |
| files = crostini_security_delegate.GetFilenames(ui::EndpointType::kCrostini, |
| Data("file:///")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ("vmfile:termina:/", files[0].path.value()); |
| |
| // /path should fail. |
| files = crostini_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, Data("/mnt/chromeos/MyFiles/file")); |
| EXPECT_EQ(0u, files.size()); |
| |
| // Plugin VM shared paths should be mapped. |
| files = security_delegate.GetFilenames( |
| ui::EndpointType::kPluginVm, Data("file://ChromeOS/MyFiles/shared/file")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ(shared_path.Append("file"), files[0].path); |
| |
| // Plugin VM internal paths should be mapped. |
| files = security_delegate.GetFilenames( |
| ui::EndpointType::kPluginVm, Data("file:///C:/WINDOWS/notepad.exe")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ("vmfile:PvmDefault:C:/WINDOWS/notepad.exe", files[0].path.value()); |
| |
| // Unshared paths should fail. |
| files = security_delegate.GetFilenames( |
| ui::EndpointType::kPluginVm, |
| Data("file://ChromeOS/MyFiles/unshared/file")); |
| EXPECT_EQ(0u, files.size()); |
| files = security_delegate.GetFilenames( |
| ui::EndpointType::kPluginVm, |
| Data("file://ChromeOS/MyFiles/shared/file1\r\n" |
| "file://ChromeOS/MyFiles/unshared/file2")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ(shared_path.Append("file1"), files[0].path); |
| |
| // Bruschetta shared paths should be mapped. |
| guest_os::GuestOsSecurityDelegate bru_security_delegate("bru"); |
| files = bru_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, |
| Data("file:///mnt/shared/MyFiles/shared/file")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ(shared_path.Append("file"), files[0].path); |
| |
| // Bruschetta homedir is mapped as an internal path. |
| files = bru_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, Data("file:///home/testuser/file")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ("vmfile:bru:/home/testuser/file", files[0].path.value()); |
| |
| // Bruschetta internal paths should be mapped. |
| files = bru_security_delegate.GetFilenames(ui::EndpointType::kCrostini, |
| Data("file:///etc/hosts")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ("vmfile:bru:/etc/hosts", files[0].path.value()); |
| |
| // Unshared paths should fail. |
| files = bru_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, |
| Data("file:///mnt/shared/MyFiles/unshared/file")); |
| EXPECT_EQ(0u, files.size()); |
| files = bru_security_delegate.GetFilenames( |
| ui::EndpointType::kCrostini, |
| Data("file:///mnt/shared/MyFiles/shared/file1\r\n" |
| "file:///mnt/shared/MyFiles/unshared/file2")); |
| EXPECT_EQ(1u, files.size()); |
| EXPECT_EQ(shared_path.Append("file1"), files[0].path); |
| } |
| |
| TEST_F(ChromeSecurityDelegateTest, SendFileInfoConvertPaths) { |
| ChromeSecurityDelegate security_delegate; |
| ui::FileInfo file1(myfiles_dir_.Append("file1"), base::FilePath()); |
| ui::FileInfo file2(myfiles_dir_.Append("file2"), base::FilePath()); |
| auto* guest_os_share_path = |
| guest_os::GuestOsSharePath::GetForProfile(profile()); |
| guest_os_share_path->RegisterSharedPath(plugin_vm::kPluginVmName, |
| myfiles_dir_); |
| |
| // Arc should convert path to UTF16 URL. |
| std::string data; |
| security_delegate.SendFileInfo(ui::EndpointType::kArc, {file1}, |
| base::BindOnce(&CaptureUTF16, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ( |
| "content://org.chromium.arc.volumeprovider/" |
| "0000000000000000000000000000CAFEF00D2019/file1", |
| data); |
| |
| // Arc should join lines with CRLF. |
| security_delegate.SendFileInfo(ui::EndpointType::kArc, {file1, file2}, |
| base::BindOnce(&CaptureUTF16, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ( |
| "content://org.chromium.arc.volumeprovider/" |
| "0000000000000000000000000000CAFEF00D2019/file1" |
| "\r\n" |
| "content://org.chromium.arc.volumeprovider/" |
| "0000000000000000000000000000CAFEF00D2019/file2", |
| data); |
| |
| // Crostini should convert path to inside VM, and share the path. |
| guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina"); |
| crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///mnt/chromeos/MyFiles/file1", data); |
| |
| // Crostini should join lines with CRLF. |
| crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, |
| {file1, file2}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ( |
| "file:///mnt/chromeos/MyFiles/file1" |
| "\r\n" |
| "file:///mnt/chromeos/MyFiles/file2", |
| data); |
| |
| // Plugin VM should convert path to inside VM. |
| security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file://ChromeOS/MyFiles/file1", data); |
| |
| // Bruschetta should convert path to inside VM, and share the path. |
| guest_os::GuestOsSecurityDelegate bru_security_delegate("bru"); |
| bru_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///mnt/shared/MyFiles/file1", data); |
| |
| // Crostini should handle vmfile:termina:/etc/hosts. |
| file1.path = base::FilePath("vmfile:termina:/etc/hosts"); |
| crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///etc/hosts", data); |
| |
| // Crostini should ignore vmfile:PvmDefault:C:/WINDOWS/notepad.exe. |
| file1.path = base::FilePath("vmfile:PvmDefault:C:/WINDOWS/notepad.exe"); |
| crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("", data); |
| |
| // Plugin VM should handle vmfile:PvmDefault:C:/WINDOWS/notepad.exe. |
| file1.path = base::FilePath("vmfile:PvmDefault:C:/WINDOWS/notepad.exe"); |
| security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///C:/WINDOWS/notepad.exe", data); |
| |
| // Crostini should handle vmfile:termina:/etc/hosts. |
| file1.path = base::FilePath("vmfile:termina:/etc/hosts"); |
| security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("", data); |
| |
| // Bruschetta should handle vmfile:bru:/etc/hosts. |
| file1.path = base::FilePath("vmfile:bru:/etc/hosts"); |
| bru_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///etc/hosts", data); |
| |
| // Bruschetta should ignore vmfile:termina:/etc/hosts. |
| file1.path = base::FilePath("vmfile:termina:/etc/hosts"); |
| bru_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("", data); |
| } |
| |
| TEST_F(ChromeSecurityDelegateTest, SendFileInfoSharePathsCrostini) { |
| guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina"); |
| |
| // A path which is already shared should not be shared again. |
| base::FilePath shared_path = myfiles_dir_.Append("shared"); |
| auto* guest_os_share_path = |
| guest_os::GuestOsSharePath::GetForProfile(profile()); |
| guest_os_share_path->RegisterSharedPath(crostini::kCrostiniDefaultVmName, |
| shared_path); |
| ui::FileInfo file(shared_path, base::FilePath()); |
| EXPECT_FALSE(FakeSeneschalClient::Get()->share_path_called()); |
| std::string data; |
| crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///mnt/chromeos/MyFiles/shared", data); |
| EXPECT_FALSE(FakeSeneschalClient::Get()->share_path_called()); |
| |
| // A path which is not already shared should be shared. |
| file = ui::FileInfo(myfiles_dir_.Append("file"), base::FilePath()); |
| crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("file:///mnt/chromeos/MyFiles/file", data); |
| EXPECT_TRUE(FakeSeneschalClient::Get()->share_path_called()); |
| } |
| |
| TEST_F(ChromeSecurityDelegateTest, SendFileInfoSharePathsPluginVm) { |
| ChromeSecurityDelegate security_delegate; |
| |
| // Plugin VM should send empty data and not share path if not already shared. |
| ui::FileInfo file(myfiles_dir_.Append("file"), base::FilePath()); |
| std::string data; |
| security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file}, |
| base::BindOnce(&Capture, &data)); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ("", data); |
| EXPECT_FALSE(FakeSeneschalClient::Get()->share_path_called()); |
| } |
| } // namespace ash |