| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "rmad/executor/mount.h" |
| |
| #include <fcntl.h> |
| #include <linux/loop.h> |
| #include <sys/ioctl.h> |
| |
| #include <utility> |
| |
| #include <base/files/file.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/files/scoped_temp_dir.h> |
| #include <base/logging.h> |
| #include <base/process/launch.h> |
| #include <base/strings/stringprintf.h> |
| #include <brillo/file_utils.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "rmad/udev/mock_udev_device.h" |
| #include "rmad/udev/mock_udev_utils.h" |
| |
| using testing::_; |
| using testing::NiceMock; |
| using testing::Return; |
| |
| namespace { |
| |
| constexpr char kLoopControlPath[] = "/dev/loop-control"; |
| constexpr char kVfatImagePath[] = "executor/testdata/vfat.bin"; |
| constexpr char kDefaultMountPoint[] = "mount_point"; |
| |
| } // namespace |
| |
| namespace rmad { |
| |
| class MountTest : public testing::Test { |
| public: |
| MountTest() { |
| // Set up a loop device with VFAT file system. |
| CreateTempDir(); |
| SetUpImage(); |
| SetUpLoopDevice(); |
| } |
| ~MountTest() override { |
| // Release the loop device. |
| if (loop_fd_.is_valid()) { |
| EXPECT_EQ(0, ioctl(loop_fd_.get(), LOOP_CLR_FD, 0)); |
| } |
| } |
| |
| private: |
| void CreateTempDir() { EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); } |
| |
| void SetUpImage() { |
| image_path_ = base::FilePath(kVfatImagePath); |
| image_fd_ = base::ScopedFD(open(image_path_.value().c_str(), O_RDONLY)); |
| if (!image_fd_.is_valid()) { |
| FAIL() << "Failed to open " << image_path_.value(); |
| } |
| } |
| |
| void SetUpLoopDevice() { |
| constexpr int kMaxRetry = 5; |
| for (int i = 0; i < kMaxRetry; ++i) { |
| bool retry = false; |
| if (SetUpLoopDeviceInternal(&retry)) { |
| return; |
| } |
| if (!retry) { |
| break; |
| } |
| DLOG(INFO) << "LoopMountInternal failed with EBUSY. Try again."; |
| } |
| FAIL() << "LoopMountInternal failed " << kMaxRetry << " tries"; |
| } |
| |
| bool SetUpLoopDeviceInternal(bool* retry) { |
| *retry = false; |
| |
| // There's a race condition in resource query (LOOP_CTL_GET_FREE) and |
| // resource allocation (LOOP_SET_FD) when multiple tests are running in |
| // parallel. A possible solution might be using loopfs to create private |
| // loop devices per test, but that's not supported in chroot yet. Hence we |
| // set |retry| to true if we failed to set up the loop device with EBUSY |
| // error, meaning the loop device is already taken by another process. |
| base::ScopedFD loopctl_fd(open(kLoopControlPath, O_RDONLY)); |
| if (!loopctl_fd.is_valid()) { |
| PLOG(ERROR) << "Failed to open " << kLoopControlPath; |
| return false; |
| } |
| |
| int dev_num = ioctl(loopctl_fd.get(), LOOP_CTL_GET_FREE); |
| if (dev_num < 0) { |
| PLOG(ERROR) << "Failed to allocate loop device"; |
| return false; |
| } |
| |
| auto loop_path = base::FilePath(base::StringPrintf("/dev/loop%d", dev_num)); |
| auto loop_fd = base::ScopedFD(open(loop_path.value().c_str(), O_RDONLY)); |
| if (!loop_fd.is_valid()) { |
| PLOG(ERROR) << "Failed to open " << loop_path.value(); |
| return false; |
| } |
| |
| if (ioctl(loop_fd.get(), LOOP_SET_FD, image_fd_.get()) < 0) { |
| PLOG(ERROR) << "Failed to associate " << image_path_.value() << " with " |
| << loop_path.value(); |
| *retry = (errno == EBUSY); |
| return false; |
| } |
| |
| loop_path_ = std::move(loop_path); |
| loop_fd_ = std::move(loop_fd); |
| return true; |
| } |
| |
| protected: |
| std::unique_ptr<UdevUtils> CreateMockUdevUtils(const std::string& fs_type, |
| bool is_removable) { |
| auto udev_utils = std::make_unique<NiceMock<MockUdevUtils>>(); |
| ON_CALL(*udev_utils, GetBlockDeviceFromDevicePath(_, _)) |
| .WillByDefault([fs_type, is_removable]( |
| const std::string&, |
| std::unique_ptr<UdevDevice>* dev) { |
| auto mock_dev = std::make_unique<NiceMock<MockUdevDevice>>(); |
| ON_CALL(*mock_dev, GetFileSystemType()) |
| .WillByDefault(Return(fs_type)); |
| ON_CALL(*mock_dev, IsRemovable()).WillByDefault(Return(is_removable)); |
| dev->reset(mock_dev.release()); |
| return true; |
| }); |
| return udev_utils; |
| } |
| |
| base::ScopedTempDir temp_dir_; |
| base::FilePath image_path_; |
| base::ScopedFD image_fd_; |
| base::FilePath loop_path_; |
| base::ScopedFD loop_fd_; |
| }; |
| |
| TEST_F(MountTest, RunAsRoot_Success) { |
| const base::FilePath mount_point = |
| temp_dir_.GetPath().Append(kDefaultMountPoint); |
| EXPECT_TRUE(base::CreateDirectory(mount_point)); |
| |
| Mount mount(loop_path_, mount_point, "vfat", true, |
| CreateMockUdevUtils("vfat", true)); |
| EXPECT_TRUE(mount.IsValid()); |
| } |
| |
| TEST_F(MountTest, RunAsRoot_NotRemovable) { |
| const base::FilePath mount_point = |
| temp_dir_.GetPath().Append(kDefaultMountPoint); |
| EXPECT_TRUE(base::CreateDirectory(mount_point)); |
| |
| Mount mount(loop_path_, mount_point, "vfat", true, |
| CreateMockUdevUtils("vfat", false)); |
| EXPECT_FALSE(mount.IsValid()); |
| } |
| |
| TEST_F(MountTest, RunAsRoot_UnsupportedFileSystem) { |
| const base::FilePath mount_point = |
| temp_dir_.GetPath().Append(kDefaultMountPoint); |
| EXPECT_TRUE(base::CreateDirectory(mount_point)); |
| |
| Mount mount(loop_path_, mount_point, "vfat", true, |
| CreateMockUdevUtils("abc", true)); |
| EXPECT_FALSE(mount.IsValid()); |
| } |
| |
| TEST_F(MountTest, RunAsRoot_MountPointNotDirectory) { |
| const base::FilePath mount_point = |
| temp_dir_.GetPath().Append(kDefaultMountPoint); |
| EXPECT_TRUE(brillo::TouchFile(mount_point)); |
| |
| Mount mount(loop_path_, mount_point, "vfat", true, |
| CreateMockUdevUtils("vfat", true)); |
| EXPECT_FALSE(mount.IsValid()); |
| } |
| |
| TEST_F(MountTest, RunAsRoot_MountPointAlreadyMounted) { |
| const base::FilePath mount_point = |
| temp_dir_.GetPath().Append(kDefaultMountPoint); |
| EXPECT_TRUE(base::CreateDirectory(mount_point)); |
| |
| // First mount succeeds. |
| Mount mount(loop_path_, mount_point, "vfat", true, |
| CreateMockUdevUtils("vfat", true)); |
| EXPECT_TRUE(mount.IsValid()); |
| |
| // Second mount on the same mount point fails. |
| Mount mount2(loop_path_, mount_point, "vfat", true, |
| CreateMockUdevUtils("vfat", true)); |
| EXPECT_FALSE(mount2.IsValid()); |
| } |
| |
| } // namespace rmad |