| // 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"; |
| |
| 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; |
| constexpr int DMSETUP_TIMEOUT_SECONDS = 3; |
| |
| // 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, |
| ¶meters_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, |
| ¶meters); |
| *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_environ = 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(DMSETUP_TIMEOUT_SECONDS), &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) { |
| 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); |
| } |
| |
| 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|. 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) { |
| 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; |
| } |
| |
| 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; |
| |
| 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); |
| 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); |
| 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); |
| 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, ¶meters)) { |
| 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::DeleteFile(mount_point, true); |
| |
| // 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)); |
| 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 |