blob: 2a3bbce9d8d1169566eca9b72a92b3367f178d33 [file] [log] [blame] [edit]
// 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