| // 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 "verity_mounter.h" |
| |
| #include <algorithm> |
| #include <string> |
| |
| #include <fcntl.h> |
| #include <linux/loop.h> |
| #include <sys/ioctl.h> |
| #include <sys/stat.h> |
| #include <sys/mount.h> |
| |
| #include <base/command_line.h> |
| #include <base/files/file_util.h> |
| #include <base/process/launch.h> |
| #include <base/rand_util.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/strings/string_util.h> |
| #include <base/time/time.h> |
| |
| namespace imageloader { |
| |
| namespace { |
| |
| enum class MountStatus { FAIL, RETRY, SUCCESS }; |
| |
| constexpr int GET_LOOP_DEVICE_MAX_RETRY = 5; |
| constexpr int DMSETUP_TIMEOUT_SECONDS = 3; |
| |
| // |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). |
| const std::vector<std::string> wipe_argv = {"wipe_table", name}; |
| RunDMSetup(wipe_argv); |
| // Now remove the actual device. |
| const std::vector<std::string> remove_argv = {"remove", name}; |
| RunDMSetup(remove_argv); |
| } |
| |
| // 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); |
| } |
| |
| // 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; |
| } |
| |
| 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; |
| } |
| |
| dev_name->assign("/dev/mapper/" + name); |
| return true; |
| } |
| |
| } // 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& table) { |
| 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(), "squashfs", |
| 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; |
| } |
| |
| } // namespace imageloader |