blob: 6a408a38ba0069dd2166616835196889271a1431 [file] [log] [blame]
// Copyright 2016 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 "imageloader/verity_mounter.h"
#include <algorithm>
#include <memory>
#include <utility>
#include </usr/include/linux/magic.h>
#include <fcntl.h>
#include <libdevmapper.h>
#include <linux/loop.h>
#include <mntent.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/vfs.h>
#include <base/command_line.h>
#include <base/containers/adapters.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/logging.h>
#include <base/process/launch.h>
#include <base/rand_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include "imageloader/component.h"
#include "imageloader/verity_mounter_impl.h"
namespace imageloader {
namespace {
// Path to devices created by device mapper.
constexpr char kDeviceMapperPath[] = "/dev/mapper";
// Path to the loop control device.
constexpr char kDevLoopControl[] = "/dev/loop-control";
using dm_task_ptr = std::unique_ptr<dm_task, void (*)(dm_task*)>;
enum class MountStatus { FAIL, RETRY, SUCCESS };
constexpr int GET_LOOP_DEVICE_MAX_RETRY = 5;
// Fetches the device mapper table entry with the specified name and writes the
// type and parameters into the provided pointers.
// Returns true on success.
bool MapperGetEntry(const std::string& name,
std::string* type,
std::string* parameters) {
auto task = dm_task_ptr(dm_task_create(DM_DEVICE_TABLE), &dm_task_destroy);
struct dm_info info;
if (!task) {
LOG(ERROR) << "dm_task_create failed!";
return false;
}
if (!dm_task_set_name(task.get(), name.c_str())) {
LOG(ERROR) << "dm_task_set_name failed!";
return false;
}
if (!dm_task_run(task.get())) {
LOG(ERROR) << "dm_task_run failed!";
return false;
}
if (!dm_task_get_info(task.get(), &info)) {
LOG(ERROR) << "dm_task_get_info failed!";
return false;
}
void* next = nullptr;
uint64_t start;
uint64_t length;
char* type_cstr;
char* parameters_cstr;
next = dm_get_next_target(task.get(), next, &start, &length, &type_cstr,
&parameters_cstr);
if (type != nullptr) {
*type = std::string(type_cstr);
}
if (parameters != nullptr) {
*parameters = std::string(parameters_cstr);
}
return true;
}
// Fetches the length of the device mapper table entries for the specified name.
bool MapperTableLength(const std::string& name, uint64_t* length) {
auto task = dm_task_ptr(dm_task_create(DM_DEVICE_TABLE), &dm_task_destroy);
struct dm_info info;
if (!task) {
LOG(ERROR) << "dm_task_create failed!";
return false;
}
if (!dm_task_set_name(task.get(), name.c_str())) {
LOG(ERROR) << "dm_task_set_name failed!";
return false;
}
if (!dm_task_run(task.get())) {
LOG(ERROR) << "dm_task_run failed!";
return false;
}
if (!dm_task_get_info(task.get(), &info)) {
LOG(ERROR) << "dm_task_get_info failed!";
return false;
}
*length = 0;
void* next = nullptr;
uint64_t start;
uint64_t length_part;
char* type;
char* parameters;
do {
next = dm_get_next_target(task.get(), next, &start, &length_part, &type,
&parameters);
*length += length_part;
} while (next);
return true;
}
// Executes the equivalent of: dmsetup wipe_table <name>
// Returns true on success.
bool MapperWipeTable(const std::string& name) {
uint64_t length;
if (!MapperTableLength(name, &length)) {
return false;
}
auto task = dm_task_ptr(dm_task_create(DM_DEVICE_RELOAD), &dm_task_destroy);
if (!task) {
LOG(ERROR) << "dm_task_create failed!";
return false;
}
if (!dm_task_set_name(task.get(), name.c_str())) {
LOG(ERROR) << "dm_task_set_name failed!";
return false;
}
if (!dm_task_add_target(task.get(), 0, length, "error", "")) {
LOG(ERROR) << "dm_task_add_target failed!";
return false;
}
if (!dm_task_run(task.get())) {
LOG(ERROR) << "dm_task_run failed!";
return false;
}
return true;
}
// Executes the equivalent of: dmsetup remove <name>
// Returns true on success.
bool MapperRemove(const std::string& name) {
auto task = dm_task_ptr(dm_task_create(DM_DEVICE_REMOVE), &dm_task_destroy);
if (!task) {
LOG(ERROR) << "dm_task_create failed!";
return false;
}
if (!dm_task_set_name(task.get(), name.c_str())) {
LOG(ERROR) << "dm_task_set_name failed!";
return false;
}
if (!dm_task_run(task.get())) {
LOG(ERROR) << "dm_task_run failed!";
return false;
}
return true;
}
// |argv| should include all the commands and table to dmsetup, but not
// include the path to the binary.
bool RunDMSetup(const std::vector<std::string>& argv) {
base::LaunchOptions options;
options.clear_environment = true;
std::vector<std::string> full_argv(argv);
full_argv.insert(full_argv.begin(), "/sbin/dmsetup");
base::Process process = base::LaunchProcess(full_argv, options);
if (!process.IsValid()) {
LOG(ERROR) << "Failed to launch dmsetup process";
return false;
}
int exit_code;
if (!process.WaitForExitWithTimeout(
base::TimeDelta::FromSeconds(kDMSetupTimeoutSeconds), &exit_code)) {
LOG(ERROR) << "Failed to wait for dmsetup process.";
return false;
}
return exit_code == 0;
}
bool LaunchDMCreate(const std::string& name, const std::string& table) {
const std::vector<std::string> argv = {"create", name, "--table",
table.c_str(), "--readonly"};
return RunDMSetup(argv);
}
// Clear the /dev/mapper/<foo> verity device.
void ClearVerityDevice(const std::string& name) {
// Per the man page, wipe_table:
// Wait for any I/O in-flight through the device to complete, then replace the
// table with a new table that fails any new I/O sent to the device. If
// successful, this should release any devices held open by the device's
// table(s).
MapperWipeTable(name);
// Now remove the actual device.
MapperRemove(name);
}
// Clear the file descriptor behind a loop device.
void ClearLoopDevice(const std::string& device_path, int loop_device_num) {
base::ScopedFD loop_device_fd(
open(device_path.c_str(), O_RDONLY | O_CLOEXEC));
if (loop_device_fd.is_valid())
ioctl(loop_device_fd.get(), LOOP_CLR_FD, 0);
base::ScopedFD control(
open(kDevLoopControl, O_RDWR | O_CLOEXEC | O_NOCTTY | O_NONBLOCK));
if (!control.is_valid()) {
PLOG(WARNING) << "Failed to open " << kDevLoopControl;
return;
}
if (ioctl(control.get(), LOOP_CTL_REMOVE, loop_device_num) < 0)
PLOG(WARNING) << "ioctl LOOP_CTL_REMOVE failed";
}
bool SetupDeviceMapper(const std::string& device_path,
const std::string& table,
std::string* dev_name) {
// Now setup the dmsetup table.
std::string final_table = table;
if (!VerityMounter::SetupTable(&final_table, device_path))
return false;
// Generate a name with a random string of 32 characters: we consider this to
// have sufficiently low chance of collision to assume the name isn't taken.
std::vector<uint8_t> rand_bytes(32);
base::RandBytes(rand_bytes.data(), rand_bytes.size());
std::string name = base::HexEncode(rand_bytes.data(), rand_bytes.size());
if (!LaunchDMCreate(name, final_table)) {
LOG(ERROR) << "Failed to run dmsetup.";
return false;
}
const base::FilePath dev_mapper(kDeviceMapperPath);
dev_name->assign(dev_mapper.Append(name).value().c_str());
return true;
}
bool CreateDirectoryWithMode(const base::FilePath& full_path, int mode) {
std::vector<base::FilePath> subpaths;
// Collect a list of all parent directories.
base::FilePath last_path = full_path;
subpaths.push_back(full_path);
for (base::FilePath path = full_path.DirName();
path.value() != last_path.value(); path = path.DirName()) {
subpaths.push_back(path);
last_path = path;
}
// Iterate through the parents and create the missing ones.
for (const auto& subpath : base::Reversed(subpaths)) {
if (base::DirectoryExists(subpath))
continue;
if (mkdir(subpath.value().c_str(), mode) == 0)
continue;
// Mkdir failed, but it might have failed with EEXIST, or some other error
// due to the directory appearing out of thin air. This can occur if two
// processes are trying to create the same file system tree at the same
// time. Check to see if it exists and make sure it is a directory.
if (!base::DirectoryExists(subpath)) {
PLOG(ERROR) << "Failed to create directory: " << subpath.value();
return false;
}
}
return true;
}
bool CreateMountPointIfNeeded(const base::FilePath& mount_point,
bool* already_mounted) {
*already_mounted = false;
// Is this mount point somehow already taken?
struct stat st;
if (lstat(mount_point.value().c_str(), &st) == 0) {
if (!S_ISDIR(st.st_mode)) {
LOG(ERROR) << "Mount point exists but is not a directory.";
return false;
}
base::FilePath mount_parent = mount_point.DirName();
struct stat st2;
if (stat(mount_parent.value().c_str(), &st2) != 0) {
PLOG(ERROR) << "Could not stat the mount point parent";
return false;
}
if (st.st_dev != st2.st_dev) {
struct statfs st_fs;
if (statfs(mount_point.value().c_str(), &st_fs) != 0) {
PLOG(ERROR) << "statfs";
return false;
}
if ((st_fs.f_type != SQUASHFS_MAGIC &&
st_fs.f_type != EXT4_SUPER_MAGIC) ||
!(st_fs.f_flags & ST_NODEV) || !(st_fs.f_flags & ST_NOSUID) ||
!(st_fs.f_flags & ST_RDONLY)) {
LOG(ERROR) << "File system is not the expected type.";
return false;
}
*already_mounted = true;
return true;
}
} else if (!CreateDirectoryWithMode(mount_point, kComponentDirPerms)) {
LOG(ERROR) << "Failed to create mount point: " << mount_point.value();
return false;
}
return true;
}
// Reserves a loop device and associates it with |image_fd|. The path to the
// loop device is returned in |device_path_out| and the int value is in
// |loop_device_num|. When the loop device is no
// longer being used, free the resource with ClearLoopDevice().
MountStatus GetLoopDevice(const base::ScopedFD& image_fd,
std::string* device_path_out,
int* loop_device_num) {
DCHECK(device_path_out);
DCHECK(loop_device_num);
base::ScopedFD loopctl_fd(open("/dev/loop-control", O_RDONLY | O_CLOEXEC));
if (!loopctl_fd.is_valid()) {
PLOG(ERROR) << "loopctl_fd";
return MountStatus::FAIL;
}
int device_free_number = ioctl(loopctl_fd.get(), LOOP_CTL_GET_FREE);
if (device_free_number < 0) {
PLOG(ERROR) << "ioctl : LOOP_CTL_GET_FREE";
return MountStatus::FAIL;
}
*loop_device_num = device_free_number;
std::string device_path =
base::StringPrintf("/dev/loop%d", device_free_number);
base::ScopedFD loop_device_fd(
open(device_path.c_str(), O_RDONLY | O_CLOEXEC));
if (!loop_device_fd.is_valid()) {
PLOG(ERROR) << "Failed to open loop device: " << device_path;
return MountStatus::FAIL;
}
if (ioctl(loop_device_fd.get(), LOOP_SET_FD, image_fd.get()) == -1) {
if (errno != EBUSY) {
PLOG(ERROR) << "ioctl: LOOP_SET_FD";
ioctl(loop_device_fd.get(), LOOP_CLR_FD, 0);
return MountStatus::FAIL;
}
return MountStatus::RETRY;
}
device_path_out->assign(device_path);
return MountStatus::SUCCESS;
}
} // namespace
// static
bool VerityMounter::SetupTable(std::string* table,
const std::string& device_path) {
// Make sure there is only one entry in the device mapper table.
if (std::count(table->begin(), table->end(), '\n') > 1)
return false;
// Remove all newlines from the table. This is to workaround the server
// incorrectly inserting a newline when writing out the table.
table->erase(std::remove(table->begin(), table->end(), '\n'), table->end());
// Replace in the actual loop device name.
base::ReplaceSubstringsAfterOffset(table, 0, "ROOT_DEV", device_path);
base::ReplaceSubstringsAfterOffset(table, 0, "HASH_DEV", device_path);
// If the table does not specify an error condition, use the default (eio).
// This is critical because the default behavior is to panic the device and
// force a system recovery. Do not do this for component corruption.
if (table->find("error_behavior") == std::string::npos)
table->append(" error_behavior=eio");
return true;
}
bool VerityMounter::Mount(const base::ScopedFD& image_fd,
const base::FilePath& mount_point,
const std::string& fs_type,
const std::string& table) {
// First check if the component is already mounted and avoid unnecessary work.
bool already_mounted = false;
if (!CreateMountPointIfNeeded(mount_point, &already_mounted))
return false;
if (already_mounted)
return true;
int loop_device_num;
std::string loop_device_path;
// We need to retry because another program could grap the loop device,
// resulting in an EBUSY error. If that happens, run again and grab a new
// device.
int retries = GET_LOOP_DEVICE_MAX_RETRY;
while (true) {
MountStatus status =
GetLoopDevice(image_fd, &loop_device_path, &loop_device_num);
if (status == MountStatus::FAIL ||
(status == MountStatus::RETRY && retries == 0)) {
LOG(ERROR) << "GetLoopDevice failed, mount_point: "
<< mount_point.value();
return false;
} else if (status == MountStatus::SUCCESS) {
break;
}
--retries;
}
std::string dev_name;
if (!SetupDeviceMapper(loop_device_path, table, &dev_name)) {
LOG(ERROR) << "mount_point: " << mount_point.value();
ClearLoopDevice(loop_device_path, loop_device_num);
return false;
}
if (mount(dev_name.c_str(), mount_point.value().c_str(), fs_type.c_str(),
MS_RDONLY | MS_NOSUID | MS_NODEV, "") < 0) {
PLOG(ERROR) << "mount, mount_point " << mount_point.value();
ClearVerityDevice(dev_name);
ClearLoopDevice(loop_device_path, loop_device_num);
return false;
}
return true;
}
// Takes a device mapper name and determines the loop device number.
bool MapperNameToLoop(const std::string& name, int32_t* loop) {
std::string type;
std::string parameters;
if (!MapperGetEntry(name, &type, &parameters)) {
return false;
}
if (type != "verity") {
LOG(ERROR) << "Encountered unexpected mapping \"" << type
<< "\" instead of \"verity\".";
return false;
}
return MapperParametersToLoop(parameters, loop);
}
bool Unmount(const base::FilePath& mount_point) {
return umount(mount_point.value().c_str()) == 0;
}
// Returns (mount point, source path) pairs visible to this process. The order
// can be reversed to help with unmounting.
std::vector<std::pair<std::string, std::string>> GetAllMountPaths(
bool reverse) {
std::vector<std::pair<std::string, std::string>> mount_paths;
base::ScopedFILE mountinfo(fopen("/proc/self/mounts", "re"));
if (!mountinfo) {
PLOG(ERROR) << "Failed to open \"/proc/self/mounts\".";
return mount_paths;
}
// MNT_LINE_MAX from sys/mnttab.h is 1024, extra bytes are for null
// termination of strings.
static char buffer[1024 + 4];
struct mntent mount_entry;
while (getmntent_r(mountinfo.get(), &mount_entry, buffer, sizeof(buffer))) {
mount_paths.emplace(reverse ? mount_paths.end() : mount_paths.begin(),
mount_entry.mnt_dir, mount_entry.mnt_fsname);
}
return mount_paths;
}
// Performs the a cleanup of a given mount point which should be tied to the
// given source path. This entails unmounting the mount point, removing the
// device mapper table entry, deleting the mount point folder, and freeing the
// loop device.
// Returns true on success.
bool CleanupImpl(const base::FilePath& mount_point,
const base::FilePath source_path) {
const base::FilePath dev_mapper(kDeviceMapperPath);
if (!IsAncestor(dev_mapper, source_path)) {
LOG(ERROR) << source_path.value() << " is not device mapped!";
return false;
}
// Lookup loop info.
int32_t loop = -1;
if (!MapperNameToLoop(source_path.BaseName().value(), &loop) || loop < 0) {
LOG(ERROR) << "Unable to determine loop device for " << source_path.value();
return false;
}
// Unmount the image.
if (!Unmount(mount_point)) {
PLOG(ERROR) << "Unmount failed";
return false;
}
// Delete mount target folder
base::DeletePathRecursively(mount_point);
// Clear Verity device.
if (!MapperWipeTable(source_path.value())) {
PLOG(ERROR) << "Device mapper wipe table failed";
return false;
}
if (!MapperRemove(source_path.value())) {
PLOG(ERROR) << "Device mapper remove failed";
return false;
}
// Clear loop device.
ClearLoopDevice(base::StringPrintf("/dev/loop%d", loop), loop);
return true;
}
// Unmounts the given path, cleans up the device mapper entry, and frees the
// loop device for the specified mount point.
// Returns true on success.
bool VerityMounter::Cleanup(const base::FilePath& mount_point) {
bool ret = true;
for (const auto& mount_path : GetAllMountPaths(true)) {
if (mount_point.value() == mount_path.first) {
if (!CleanupImpl(mount_point, base::FilePath(mount_path.second))) {
ret = false;
}
}
}
return ret;
}
// Performs a cleanup for all mount points under parent_dir.
// All successfully cleaned up mount points will be appended to "paths".
// Returns false if cleanup fails for any mount point.
bool VerityMounter::CleanupAll(bool dry_run,
const base::FilePath& parent_dir,
std::vector<base::FilePath>* paths) {
bool ret = true;
for (const auto& mount_path : GetAllMountPaths(true)) {
base::FilePath fp_mount_path(mount_path.first);
// It is not enough to check if fp_mount_path is a direct child because
// some mount points, such as printer drivers, are mounted under a sub
// folder.
if (IsAncestor(parent_dir, fp_mount_path)) {
if (dry_run) {
if (paths) {
paths->push_back(fp_mount_path);
}
} else if (CleanupImpl(fp_mount_path,
base::FilePath(mount_path.second))) {
if (paths) {
paths->push_back(fp_mount_path);
}
} else {
LOG(ERROR) << "Failed to cleanup \"" << mount_path.first << "\"";
ret = false;
}
}
}
return ret;
}
} // namespace imageloader