| // Copyright 2019 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cros-disks/fuse_mounter.h" |
| |
| #include <sys/mount.h> |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include <brillo/process/process_reaper.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "cros-disks/error_logger.h" |
| #include "cros-disks/mount_options.h" |
| #include "cros-disks/mount_point.h" |
| #include "cros-disks/platform.h" |
| #include "cros-disks/sandboxed_process.h" |
| |
| namespace cros_disks { |
| |
| namespace { |
| |
| using testing::_; |
| using testing::ElementsAre; |
| using testing::Invoke; |
| using testing::IsEmpty; |
| using testing::Return; |
| using testing::StartsWith; |
| |
| const uid_t kMountUID = 200; |
| const gid_t kMountGID = 201; |
| const uid_t kFilesUID = 700; |
| const uid_t kFilesGID = 701; |
| const uid_t kFilesAccessGID = 1501; |
| const char kMountUser[] = "fuse-fuse"; |
| const char kFUSEType[] = "fuse"; |
| const char kMountProgram[] = "dummy"; |
| const char kSomeSource[] = "/dev/dummy"; |
| const char kMountDir[] = "/mnt"; |
| const int kPasswordNeededCode = 42; |
| |
| // Mock Platform implementation for testing. |
| class MockFUSEPlatform : public Platform { |
| public: |
| MockFUSEPlatform() { |
| ON_CALL(*this, GetUserAndGroupId(_, _, _)) |
| .WillByDefault(Invoke(this, &MockFUSEPlatform::GetUserAndGroupIdImpl)); |
| ON_CALL(*this, GetGroupId(_, _)) |
| .WillByDefault(Invoke(this, &MockFUSEPlatform::GetGroupIdImpl)); |
| ON_CALL(*this, PathExists(_)).WillByDefault(Return(true)); |
| ON_CALL(*this, SetOwnership(_, _, _)).WillByDefault(Return(true)); |
| ON_CALL(*this, SetPermissions(_, _)).WillByDefault(Return(true)); |
| } |
| |
| MOCK_METHOD(bool, |
| GetUserAndGroupId, |
| (const std::string&, uid_t*, gid_t*), |
| (const, override)); |
| MOCK_METHOD(bool, |
| GetGroupId, |
| (const std::string&, gid_t*), |
| (const, override)); |
| MOCK_METHOD(MountErrorType, |
| Mount, |
| (const std::string&, |
| const std::string&, |
| const std::string&, |
| uint64_t, |
| const std::string&), |
| (const, override)); |
| MOCK_METHOD(MountErrorType, |
| Unmount, |
| (const std::string&, int), |
| (const, override)); |
| MOCK_METHOD(bool, PathExists, (const std::string&), (const, override)); |
| MOCK_METHOD(bool, |
| RemoveEmptyDirectory, |
| (const std::string&), |
| (const, override)); |
| MOCK_METHOD(bool, |
| SetOwnership, |
| (const std::string&, uid_t, gid_t), |
| (const, override)); |
| MOCK_METHOD(bool, |
| GetOwnership, |
| (const std::string&, uid_t*, gid_t*), |
| (const, override)); |
| MOCK_METHOD(bool, |
| SetPermissions, |
| (const std::string&, mode_t), |
| (const, override)); |
| |
| private: |
| bool GetUserAndGroupIdImpl(const std::string& user, |
| uid_t* user_id, |
| gid_t* group_id) const { |
| if (user == "chronos") { |
| if (user_id) |
| *user_id = kFilesUID; |
| if (group_id) |
| *group_id = kFilesGID; |
| return true; |
| } |
| if (user == kMountUser) { |
| if (user_id) |
| *user_id = kMountUID; |
| if (group_id) |
| *group_id = kMountGID; |
| return true; |
| } |
| return false; |
| } |
| |
| bool GetGroupIdImpl(const std::string& group, gid_t* group_id) const { |
| if (group == "chronos-access") { |
| if (group_id) |
| *group_id = kFilesAccessGID; |
| return true; |
| } |
| return false; |
| } |
| }; |
| |
| class MockSandboxedProcess : public SandboxedProcess { |
| public: |
| MockSandboxedProcess() = default; |
| MOCK_METHOD(pid_t, |
| StartImpl, |
| (base::ScopedFD, base::ScopedFD, base::ScopedFD), |
| (override)); |
| MOCK_METHOD(int, WaitImpl, (), (override)); |
| MOCK_METHOD(int, WaitNonBlockingImpl, (), (override)); |
| }; |
| |
| class FUSEMounterForTesting : public FUSEMounter { |
| public: |
| FUSEMounterForTesting(const Platform* platform, |
| brillo::ProcessReaper* process_reaper) |
| : FUSEMounter({.filesystem_type = kFUSEType, |
| .mount_program = kMountProgram, |
| .mount_user = kMountUser, |
| .password_needed_codes = {kPasswordNeededCode}, |
| .platform = platform, |
| .process_reaper = process_reaper}) {} |
| |
| MOCK_METHOD(int, OnInput, (const std::string&), (const)); |
| MOCK_METHOD(int, InvokeMountTool, (const std::vector<std::string>&), (const)); |
| |
| mutable std::vector<std::string> environment; |
| |
| private: |
| std::unique_ptr<SandboxedProcess> CreateSandboxedProcess() const override { |
| auto mock = std::make_unique<MockSandboxedProcess>(); |
| const SandboxedProcess* const process = mock.get(); |
| ON_CALL(*mock, StartImpl(_, _, _)).WillByDefault(Return(123)); |
| ON_CALL(*mock, WaitNonBlockingImpl()) |
| .WillByDefault(Invoke([this, process]() { |
| const std::string& input = process->input(); |
| if (!input.empty()) |
| OnInput(input); |
| |
| return InvokeMountTool(process->arguments()); |
| })); |
| return mock; |
| } |
| }; |
| |
| } // namespace |
| |
| class FUSEMounterTest : public ::testing::Test { |
| public: |
| FUSEMounterTest() : mounter_(&platform_, &process_reaper_) { |
| ON_CALL(platform_, Mount(kSomeSource, kMountDir, _, _, _)) |
| .WillByDefault(Return(MOUNT_ERROR_NONE)); |
| } |
| |
| protected: |
| // Sets up mock expectations for a successful mount. |
| void SetupMountExpectations() { |
| EXPECT_CALL(mounter_, InvokeMountTool(ElementsAre( |
| kMountProgram, "-o", MountOptions().ToString(), |
| kSomeSource, StartsWith("/dev/fd/")))) |
| .WillOnce(Return(0)); |
| EXPECT_CALL(platform_, PathExists(kMountProgram)).WillOnce(Return(true)); |
| EXPECT_CALL(platform_, SetOwnership(kSomeSource, getuid(), kMountGID)) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(platform_, SetPermissions(kSomeSource, S_IRUSR | S_IWUSR | |
| S_IRGRP | S_IWGRP)) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(platform_, SetOwnership(kMountDir, _, _)).Times(0); |
| EXPECT_CALL(platform_, SetPermissions(kMountDir, _)).Times(0); |
| } |
| |
| MockFUSEPlatform platform_; |
| brillo::ProcessReaper process_reaper_; |
| FUSEMounterForTesting mounter_; |
| }; |
| |
| TEST_F(FUSEMounterTest, Sandboxing_Unprivileged) { |
| SetupMountExpectations(); |
| // The MountPoint returned by Mount() will unmount when it is destructed. |
| EXPECT_CALL(platform_, Unmount(kMountDir, 0)) |
| .WillOnce(Return(MOUNT_ERROR_NONE)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = |
| mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error); |
| EXPECT_TRUE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_NONE, error); |
| } |
| |
| TEST_F(FUSEMounterTest, MountPoint_UnmountTwice) { |
| SetupMountExpectations(); |
| // Even though Unmount() is called twice, the underlying unmount should only |
| // be done once. |
| EXPECT_CALL(platform_, Unmount(kMountDir, 0)) |
| .WillOnce(Return(MOUNT_ERROR_NONE)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = |
| mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error); |
| EXPECT_TRUE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_NONE, error); |
| |
| EXPECT_EQ(MOUNT_ERROR_NONE, mount_point->Unmount()); |
| EXPECT_EQ(MOUNT_ERROR_PATH_NOT_MOUNTED, mount_point->Unmount()); |
| } |
| |
| TEST_F(FUSEMounterTest, MountPoint_UnmountFailure) { |
| SetupMountExpectations(); |
| // If an Unmount fails, we should be able to retry. |
| EXPECT_CALL(platform_, Unmount(kMountDir, 0)) |
| .WillOnce(Return(MOUNT_ERROR_UNKNOWN)) |
| .WillOnce(Return(MOUNT_ERROR_NONE)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = |
| mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error); |
| EXPECT_TRUE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_NONE, error); |
| |
| EXPECT_EQ(MOUNT_ERROR_UNKNOWN, mount_point->Unmount()); |
| EXPECT_EQ(MOUNT_ERROR_NONE, mount_point->Unmount()); |
| } |
| |
| TEST_F(FUSEMounterTest, MountPoint_UnmountBusy) { |
| SetupMountExpectations(); |
| EXPECT_CALL(platform_, Unmount(kMountDir, 0)) |
| .WillOnce(Return(MOUNT_ERROR_PATH_ALREADY_MOUNTED)); |
| EXPECT_CALL(platform_, Unmount(kMountDir, MNT_FORCE | MNT_DETACH)) |
| .WillOnce(Return(MOUNT_ERROR_NONE)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = |
| mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error); |
| EXPECT_TRUE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_NONE, error); |
| |
| EXPECT_EQ(MOUNT_ERROR_NONE, mount_point->Unmount()); |
| } |
| |
| TEST_F(FUSEMounterTest, AppFailed) { |
| EXPECT_CALL(platform_, Unmount(_, _)).WillOnce(Return(MOUNT_ERROR_NONE)); |
| EXPECT_CALL(mounter_, InvokeMountTool(_)).WillOnce(Return(1)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = |
| mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error); |
| EXPECT_FALSE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_MOUNT_PROGRAM_FAILED, error); |
| } |
| |
| TEST_F(FUSEMounterTest, AppNeedsPassword) { |
| EXPECT_CALL(platform_, Unmount(_, _)).WillOnce(Return(MOUNT_ERROR_NONE)); |
| EXPECT_CALL(mounter_, InvokeMountTool(_)) |
| .WillOnce(Return(kPasswordNeededCode)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = |
| mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error); |
| EXPECT_FALSE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_NEED_PASSWORD, error); |
| } |
| |
| TEST_F(FUSEMounterTest, WithPassword) { |
| const std::string password = "My Password"; |
| |
| SetupMountExpectations(); |
| EXPECT_CALL(mounter_, OnInput(password)).Times(1); |
| // The MountPoint returned by Mount() will unmount when it is destructed. |
| EXPECT_CALL(platform_, Unmount(kMountDir, 0)) |
| .WillOnce(Return(MOUNT_ERROR_NONE)); |
| |
| MountErrorType error = MOUNT_ERROR_UNKNOWN; |
| auto mount_point = mounter_.Mount(kSomeSource, base::FilePath(kMountDir), |
| {"password=" + password}, &error); |
| EXPECT_TRUE(mount_point); |
| EXPECT_EQ(MOUNT_ERROR_NONE, error); |
| } |
| |
| TEST(FUSEMounterPasswordTest, NoPassword) { |
| const FUSEMounter mounter({.password_needed_codes = {kPasswordNeededCode}}); |
| SandboxedProcess process; |
| mounter.CopyPassword( |
| { |
| "Password=1", // Options are case sensitive |
| "password =2", // Space is significant |
| " password=3", // Space is significant |
| "password", // Not a valid option |
| }, |
| &process); |
| EXPECT_EQ(process.input(), ""); |
| } |
| |
| TEST(FUSEMounterPasswordTest, CopiesPassword) { |
| const FUSEMounter mounter({.password_needed_codes = {kPasswordNeededCode}}); |
| for (const std::string password : { |
| "", |
| " ", |
| "=", |
| "simple", |
| R"( !@#$%^&*()_-+={[}]|\:;"'<,>.?/ )", |
| }) { |
| SandboxedProcess process; |
| mounter.CopyPassword({"password=" + password}, &process); |
| EXPECT_EQ(process.input(), password); |
| } |
| } |
| |
| TEST(FUSEMounterPasswordTest, FirstPassword) { |
| const FUSEMounter mounter({.password_needed_codes = {kPasswordNeededCode}}); |
| SandboxedProcess process; |
| mounter.CopyPassword({"other1=value1", "password=1", "password=2", |
| "other2=value2", "password=3"}, |
| &process); |
| EXPECT_EQ(process.input(), "1"); |
| } |
| |
| TEST(FUSEMounterPasswordTest, IgnoredIfNotNeeded) { |
| const FUSEMounter mounter({}); |
| SandboxedProcess process; |
| mounter.CopyPassword({"password=dummy"}, &process); |
| EXPECT_EQ(process.input(), ""); |
| } |
| |
| } // namespace cros_disks |