| // 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. |
| |
| // Unit tests for OutOfProcessMountHelper. |
| |
| #include "cryptohome/storage/out_of_process_mount_helper.h" |
| |
| #include <fcntl.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/posix/eintr_wrapper.h> |
| #include <brillo/cryptohome.h> |
| #include <gtest/gtest.h> |
| |
| #include "cryptohome/crypto.h" |
| #include "cryptohome/filesystem_layout.h" |
| #include "cryptohome/mock_platform.h" |
| #include "cryptohome/storage/mount_utils.h" |
| |
| #include "cryptohome/namespace_mounter_ipc.pb.h" |
| |
| using base::FilePath; |
| using brillo::cryptohome::home::kGuestUserName; |
| using ::testing::_; |
| using ::testing::Eq; |
| using ::testing::NiceMock; |
| using ::testing::Return; |
| |
| namespace { |
| |
| constexpr char kChromeMountNamespace[] = "/run/namespaces/mnt_chrome"; |
| |
| constexpr pid_t kOOPHelperPid = 2; |
| |
| constexpr int kInvalidFd = -1; |
| |
| } // namespace |
| |
| namespace cryptohome { |
| |
| class OutOfProcessMountHelperTest : public ::testing::Test { |
| public: |
| OutOfProcessMountHelperTest() : crypto_(&platform_) {} |
| OutOfProcessMountHelperTest(const OutOfProcessMountHelperTest&) = delete; |
| OutOfProcessMountHelperTest& operator=(const OutOfProcessMountHelperTest&) = |
| delete; |
| |
| virtual ~OutOfProcessMountHelperTest() {} |
| |
| void SetUp() { |
| out_of_process_mounter_.reset(new OutOfProcessMountHelper( |
| true /* legacy_mount */, true /* bind_mount_downloads */, &platform_)); |
| } |
| |
| void TearDown() { |
| out_of_process_mounter_ = nullptr; |
| } |
| |
| bool CreatePipe(base::ScopedFD* read_end, base::ScopedFD* write_end) { |
| int pipe[2]; |
| bool success = base::CreateLocalNonBlockingPipe(pipe); |
| if (success) { |
| read_end->reset(pipe[0]); |
| write_end->reset(pipe[1]); |
| } |
| return success; |
| } |
| |
| base::ScopedFD GetDevNullFd() { |
| return base::ScopedFD(HANDLE_EINTR(open("/dev/null", O_WRONLY))); |
| } |
| |
| base::ScopedFD GetDevZeroFd() { |
| return base::ScopedFD(HANDLE_EINTR(open("/dev/zero", O_RDONLY))); |
| } |
| |
| protected: |
| NiceMock<MockPlatform> platform_; |
| Crypto crypto_; |
| std::unique_ptr<OutOfProcessMountHelper> out_of_process_mounter_; |
| }; |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOP) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*process, pid()).WillRepeatedly(Return(kOOPHelperPid)); |
| |
| // Allow reading from cryptohome's perspective. |
| base::ScopedFD read_end, write_end; |
| ASSERT_TRUE(CreatePipe(&read_end, &write_end)); |
| |
| EXPECT_CALL(*process, GetPipe(STDOUT_FILENO)) |
| .WillOnce(Return(read_end.get())); |
| |
| // Writing from cryptohome's perspective always succeeds. |
| base::ScopedFD dev_null = GetDevNullFd(); |
| ASSERT_TRUE(dev_null.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDIN_FILENO)).WillOnce(Return(dev_null.get())); |
| |
| FilePath legacy_home("/home/chronos/user"); |
| |
| OutOfProcessMountResponse resp; |
| resp.add_paths(legacy_home.value()); |
| ASSERT_TRUE(WriteProtobuf(write_end.get(), resp)); |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_NONE)); |
| |
| EXPECT_TRUE(out_of_process_mounter_->IsPathMounted(legacy_home)); |
| EXPECT_FALSE( |
| out_of_process_mounter_->IsPathMounted(FilePath("/invalid/path"))); |
| |
| EXPECT_CALL(*process, Kill(SIGTERM, _)).WillOnce(Return(true)); |
| out_of_process_mounter_->UnmountAll(); |
| } |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOPWriteProtobuf) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*process, pid()).WillRepeatedly(Return(kOOPHelperPid)); |
| |
| // Reading from the helper always succeeds. |
| base::ScopedFD dev_zero = GetDevZeroFd(); |
| ASSERT_TRUE(dev_zero.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDOUT_FILENO)) |
| .WillOnce(Return(dev_zero.get())); |
| |
| // Allow writing from cryptohome's perspective. |
| base::ScopedFD read_end, write_end; |
| ASSERT_TRUE(CreatePipe(&read_end, &write_end)); |
| EXPECT_CALL(*process, GetPipe(STDIN_FILENO)) |
| .WillOnce(Return(write_end.get())); |
| |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_NONE)); |
| |
| OutOfProcessMountRequest r; |
| ASSERT_TRUE(ReadProtobuf(read_end.get(), &r)); |
| EXPECT_EQ(r.username(), kGuestUserName); |
| EXPECT_EQ(r.mount_namespace_path(), kChromeMountNamespace); |
| |
| EXPECT_CALL(*process, Kill(SIGTERM, _)).WillOnce(Return(true)); |
| out_of_process_mounter_->UnmountAll(); |
| } |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOPFailsToStart) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(false)); |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_FATAL)); |
| } |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOPNonRootMountNamespace) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*process, pid()).WillRepeatedly(Return(kOOPHelperPid)); |
| EXPECT_CALL(*process, Kill(SIGTERM, _)).WillOnce(Return(true)); |
| |
| out_of_process_mounter_.reset(new OutOfProcessMountHelper( |
| true /* legacy_mount */, true /* bind_mount_downloads */, &platform_)); |
| |
| // Reading from the helper always succeeds. |
| base::ScopedFD dev_zero = GetDevZeroFd(); |
| ASSERT_TRUE(dev_zero.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDOUT_FILENO)) |
| .WillOnce(Return(dev_zero.get())); |
| |
| // Allow writing from cryptohome's perspective. |
| base::ScopedFD read_end, write_end; |
| ASSERT_TRUE(CreatePipe(&read_end, &write_end)); |
| EXPECT_CALL(*process, GetPipe(STDIN_FILENO)) |
| .WillOnce(Return(write_end.get())); |
| |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_NONE)); |
| |
| OutOfProcessMountRequest r; |
| ASSERT_TRUE(ReadProtobuf(read_end.get(), &r)); |
| EXPECT_EQ(r.username(), kGuestUserName); |
| EXPECT_EQ(r.mount_namespace_path(), kChromeMountNamespace); |
| |
| out_of_process_mounter_->UnmountAll(); |
| } |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOPFailsToWriteProtobuf) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(true)); |
| // After the PID is checked once and the process is killed, pid() should |
| // return 0. |
| EXPECT_CALL(*process, pid()) |
| .WillOnce(Return(kOOPHelperPid)) |
| .WillRepeatedly(Return(0)); |
| |
| // Writing the protobuf fails. |
| EXPECT_CALL(*process, GetPipe(STDIN_FILENO)).WillOnce(Return(kInvalidFd)); |
| |
| // Reading from the helper always succeeds. |
| base::ScopedFD dev_zero = GetDevZeroFd(); |
| ASSERT_TRUE(dev_zero.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDOUT_FILENO)) |
| .WillOnce(Return(dev_zero.get())); |
| |
| // If writing the protobuf fails, OOP mount helper should be killed. |
| EXPECT_CALL(*process, Kill(SIGTERM, _)).WillOnce(Return(true)); |
| |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_FATAL)); |
| } |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOPFailsToReadAck) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(true)); |
| // After the PID is checked once and the process is killed, pid() should |
| // return 0. |
| EXPECT_CALL(*process, pid()) |
| .WillOnce(Return(kOOPHelperPid)) |
| .WillRepeatedly(Return(0)); |
| |
| // Writing the protobuf succeeds. |
| base::ScopedFD dev_null = GetDevNullFd(); |
| ASSERT_TRUE(dev_null.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDIN_FILENO)).WillOnce(Return(dev_null.get())); |
| |
| // Reading the ack fails. |
| EXPECT_CALL(*process, GetPipe(STDOUT_FILENO)).WillOnce(Return(kInvalidFd)); |
| |
| // If reading the ack fails, OOP mount helper should be killed. |
| EXPECT_CALL(*process, Kill(SIGTERM, _)).WillOnce(Return(true)); |
| |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_FATAL)); |
| } |
| |
| TEST_F(OutOfProcessMountHelperTest, MountGuestUserDirOOPFailsToPoke) { |
| brillo::ProcessMock* process = platform_.mock_process(); |
| EXPECT_CALL(*process, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*process, pid()).WillRepeatedly(Return(kOOPHelperPid)); |
| |
| // Writing the protobuf succeeds. |
| base::ScopedFD write_to_helper = GetDevNullFd(); |
| ASSERT_TRUE(write_to_helper.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDIN_FILENO)) |
| .WillOnce(Return(write_to_helper.get())); |
| |
| // Reading from the helper always succeeds. |
| base::ScopedFD read_from_helper = GetDevZeroFd(); |
| ASSERT_TRUE(read_from_helper.is_valid()); |
| EXPECT_CALL(*process, GetPipe(STDOUT_FILENO)) |
| .WillOnce(Return(read_from_helper.get())); |
| |
| ASSERT_THAT(out_of_process_mounter_->PerformEphemeralMount(kGuestUserName, |
| base::FilePath()), |
| Eq(MOUNT_ERROR_NONE)); |
| |
| // Poking the helper fails. |
| EXPECT_CALL(*process, Kill(SIGTERM, _)).WillOnce(Return(false)); |
| // If poking fails, OOP mount helper should be killed with SIGKILL. |
| EXPECT_CALL(*process, Kill(SIGKILL, _)).WillOnce(Return(true)); |
| |
| out_of_process_mounter_->UnmountAll(); |
| } |
| |
| } // namespace cryptohome |