| // Copyright 2012 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cros-disks/disk_manager.h" |
| |
| #include <errno.h> |
| #include <inttypes.h> |
| #include <libudev.h> |
| #include <string.h> |
| #include <sys/mount.h> |
| #include <time.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include <base/bind.h> |
| #include <base/callback.h> |
| #include <base/check.h> |
| #include <base/logging.h> |
| #include <base/stl_util.h> |
| #include <base/strings/string_piece.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/time/time.h> |
| |
| #include "cros-disks/device_ejector.h" |
| #include "cros-disks/disk_monitor.h" |
| #include "cros-disks/fuse_mounter.h" |
| #include "cros-disks/metrics.h" |
| #include "cros-disks/mount_options.h" |
| #include "cros-disks/mount_point.h" |
| #include "cros-disks/platform.h" |
| #include "cros-disks/quote.h" |
| #include "cros-disks/system_mounter.h" |
| |
| namespace cros_disks { |
| |
| namespace { |
| |
| // Options passed to the mount syscall for various filesystem types. |
| constexpr char kMountOptionFlush[] = "flush"; |
| constexpr char kMountOptionUtf8[] = "utf8"; |
| |
| // Options passed to the FUSE module for various filesystem types. |
| constexpr char kFUSEOptionDirSync[] = "dirsync"; |
| constexpr char kFUSEOptionDmask[] = "dmask=0027"; // directory permissions 0750 |
| constexpr char kFUSEOptionFmask[] = "fmask=0027"; // file permissions 0750 |
| |
| // Implementation of FUSEMounter aimed at removable storage with |
| // exFAT or NTFS filesystems. |
| class DiskFUSEMounter : public FUSEMounter { |
| public: |
| DiskFUSEMounter(const Platform* platform, |
| brillo::ProcessReaper* reaper, |
| std::string filesystem_type, |
| const SandboxedProcessFactory* upstream_factory, |
| SandboxedExecutable executable, |
| OwnerUser run_as, |
| std::vector<std::string> options) |
| : FUSEMounter(platform, reaper, std::move(filesystem_type), {}), |
| upstream_factory_(upstream_factory), |
| sandbox_factory_(platform, |
| std::move(executable), |
| run_as, |
| false, // no network needed |
| {}, |
| {}), |
| options_(std::move(options)) {} |
| |
| private: |
| // FUSEMounter overrides: |
| bool CanMount(const std::string& source, |
| const std::vector<std::string>& params, |
| base::FilePath* suggested_name) const override { |
| if (suggested_name) |
| *suggested_name = base::FilePath("disk"); |
| return true; |
| } |
| |
| std::unique_ptr<SandboxedProcess> PrepareSandbox( |
| const std::string& source, |
| const base::FilePath&, |
| std::vector<std::string>, |
| MountErrorType* error) const override { |
| auto device = base::FilePath(source); |
| |
| if (!device.IsAbsolute() || device.ReferencesParent() || |
| !base::StartsWith(device.value(), "/dev/", |
| base::CompareCase::SENSITIVE)) { |
| LOG(ERROR) << "Device path " << quote(device) << " is invalid"; |
| *error = MOUNT_ERROR_INVALID_ARGUMENT; |
| return nullptr; |
| } |
| |
| if (!platform()->PathExists(device.value())) { |
| PLOG(ERROR) << "Cannot access device " << quote(device); |
| *error = MOUNT_ERROR_INVALID_DEVICE_PATH; |
| return nullptr; |
| } |
| |
| // Make sure the FUSE user can read-write to the device. |
| if (!platform()->SetOwnership(device.value(), getuid(), |
| sandbox_factory_.run_as().gid) || |
| !platform()->SetPermissions(source, |
| S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)) { |
| PLOG(ERROR) << "Cannot set up permissions on device " << quote(source); |
| *error = MOUNT_ERROR_INSUFFICIENT_PERMISSIONS; |
| return nullptr; |
| } |
| |
| std::unique_ptr<SandboxedProcess> sandbox; |
| // For tests we use injected factory. |
| if (upstream_factory_) { |
| sandbox = upstream_factory_->CreateSandboxedProcess(); |
| sandbox->AddArgument(sandbox_factory_.executable().value()); |
| } else { |
| sandbox = sandbox_factory_.CreateSandboxedProcess(); |
| } |
| |
| // Bind-mount the device into the sandbox. |
| if (!sandbox->BindMount(device.value(), device.value(), true, false)) { |
| PLOG(ERROR) << "Cannot bind-mount device " << quote(device) |
| << " into the sandbox"; |
| *error = MOUNT_ERROR_INTERNAL; |
| return nullptr; |
| } |
| |
| if (!options_.empty()) { |
| std::string options; |
| if (!JoinParamsIntoOptions(options_, &options)) { |
| *error = MOUNT_ERROR_INVALID_MOUNT_OPTIONS; |
| return nullptr; |
| } |
| sandbox->AddArgument("-o"); |
| sandbox->AddArgument(options); |
| } |
| |
| sandbox->AddArgument(device.value()); |
| |
| *error = MOUNT_ERROR_NONE; |
| return sandbox; |
| } |
| |
| // Used to inject mocks for testing. |
| const SandboxedProcessFactory* const upstream_factory_; |
| |
| const FUSESandboxedProcessFactory sandbox_factory_; |
| const std::vector<std::string> options_; |
| }; |
| |
| // Specialization of a system mounter which deals with FAT-specific |
| // mount options. |
| class FATMounter : public SystemMounter { |
| public: |
| FATMounter(const Platform* platform, std::vector<std::string> options) |
| : SystemMounter( |
| platform, "vfat", /* read_only= */ false, std::move(options)) {} |
| |
| private: |
| MountErrorType ParseParams( |
| std::vector<std::string> params, |
| std::vector<std::string>* mount_options) const override { |
| // FAT32 stores times as local time instead of UTC. By default, the vfat |
| // kernel module will use the kernel's time zone, which is set using |
| // settimeofday(), to interpret time stamps as local time. However, time |
| // zones are complicated and generally a user-space concern in modern Linux. |
| // The man page for {get,set}timeofday comments that the |timezone| fields |
| // of these functions is obsolete. Chrome OS doesn't appear to set these |
| // either. Instead, we pass the time offset explicitly as a mount option so |
| // that the user can see file time stamps as local time. This mirrors what |
| // the user will see in other operating systems. |
| time_t now = base::Time::Now().ToTimeT(); |
| struct tm timestruct; |
| // The time zone might have changed since cros-disks was started. Force a |
| // re-read of the time zone to ensure the local time is what the user |
| // expects. |
| tzset(); |
| localtime_r(&now, ×truct); |
| // tm_gmtoff is a glibc extension. |
| int64_t offset_minutes = static_cast<int64_t>(timestruct.tm_gmtoff) / 60; |
| std::string offset_option = |
| base::StringPrintf("time_offset=%" PRId64, offset_minutes); |
| |
| mount_options->push_back(offset_option); |
| |
| return SystemMounter::ParseParams(std::move(params), mount_options); |
| } |
| }; |
| |
| } // namespace |
| |
| DiskManager::DiskManager(const std::string& mount_root, |
| Platform* platform, |
| Metrics* metrics, |
| brillo::ProcessReaper* process_reaper, |
| DiskMonitor* disk_monitor, |
| DeviceEjector* device_ejector, |
| const SandboxedProcessFactory* test_sandbox_factory) |
| : MountManager(mount_root, platform, metrics, process_reaper), |
| disk_monitor_(disk_monitor), |
| device_ejector_(device_ejector), |
| test_sandbox_factory_(test_sandbox_factory), |
| eject_device_on_unmount_(true) {} |
| |
| DiskManager::~DiskManager() { |
| UnmountAll(); |
| } |
| |
| bool DiskManager::Initialize() { |
| OwnerUser run_as_exfat; |
| if (!platform()->GetUserAndGroupId("fuse-exfat", &run_as_exfat.uid, |
| &run_as_exfat.gid)) { |
| PLOG(ERROR) << "Cannot resolve fuse-exfat user"; |
| return false; |
| } |
| OwnerUser run_as_ntfs; |
| if (!platform()->GetUserAndGroupId("ntfs-3g", &run_as_ntfs.uid, |
| &run_as_ntfs.gid)) { |
| PLOG(ERROR) << "Cannot resolve ntfs-3g user"; |
| return false; |
| } |
| |
| std::string uid = base::StringPrintf("uid=%d", kChronosUID); |
| std::string gid = base::StringPrintf("gid=%d", kChronosAccessGID); |
| |
| // FAT32 - typical USB stick/SD card filesystem. |
| mounters_["vfat"] = std::make_unique<FATMounter>( |
| platform(), std::vector<std::string>{kMountOptionFlush, "shortname=mixed", |
| kMountOptionUtf8, uid, gid}); |
| |
| // Fancier newer version of FAT used for new big SD cards and USB sticks. |
| mounters_["exfat"] = std::make_unique<DiskFUSEMounter>( |
| platform(), process_reaper(), "exfat", test_sandbox_factory_, |
| SandboxedExecutable{base::FilePath("/usr/sbin/mount.exfat-fuse")}, |
| run_as_exfat, |
| std::vector<std::string>{kFUSEOptionDirSync, kFUSEOptionDmask, |
| kFUSEOptionFmask, uid, gid}); |
| |
| // External drives and some big USB sticks would likely have NTFS. |
| mounters_["ntfs"] = std::make_unique<DiskFUSEMounter>( |
| platform(), process_reaper(), "ntfs", test_sandbox_factory_, |
| SandboxedExecutable{base::FilePath("/usr/bin/ntfs-3g")}, run_as_ntfs, |
| std::vector<std::string>{kFUSEOptionDirSync, kFUSEOptionDmask, |
| kFUSEOptionFmask, uid, gid}); |
| |
| // Typical CD/DVD filesystem. Inherently read-only. |
| mounters_["iso9660"] = std::make_unique<SystemMounter>( |
| platform(), "iso9660", true, |
| std::vector<std::string>{kMountOptionUtf8, uid, gid}); |
| |
| // Newer DVD filesystem. Inherently read-only. |
| mounters_["udf"] = std::make_unique<SystemMounter>( |
| platform(), "udf", true, |
| std::vector<std::string>{kMountOptionUtf8, uid, gid}); |
| |
| // MacOS's HFS+ is not properly/officially supported, but sort of works, |
| // although with severe limitaions. |
| mounters_["hfsplus"] = std::make_unique<SystemMounter>( |
| platform(), "hfsplus", false, std::vector<std::string>{uid, gid}); |
| |
| // Have no reasonable explanation why would one have external media |
| // with a native Linux, filesystem and use CrOS to access it, given |
| // all the problems and limitations they would face, but for compatibility |
| // with previous versions we keep it unofficially supported. |
| mounters_["ext4"] = std::make_unique<SystemMounter>( |
| platform(), "ext4", false, std::vector<std::string>{}); |
| mounters_["ext3"] = std::make_unique<SystemMounter>( |
| platform(), "ext3", false, std::vector<std::string>{}); |
| mounters_["ext2"] = std::make_unique<SystemMounter>( |
| platform(), "ext2", false, std::vector<std::string>{}); |
| |
| return MountManager::Initialize(); |
| } |
| |
| bool DiskManager::CanMount(const std::string& source_path) const { |
| // The following paths can be mounted: |
| // /sys/... |
| // /devices/... |
| // /dev/... |
| return base::StartsWith(source_path, "/sys/", base::CompareCase::SENSITIVE) || |
| base::StartsWith(source_path, "/devices/", |
| base::CompareCase::SENSITIVE) || |
| base::StartsWith(source_path, "/dev/", base::CompareCase::SENSITIVE); |
| } |
| |
| std::unique_ptr<MountPoint> DiskManager::DoMount( |
| const std::string& source_path, |
| const std::string& filesystem_type, |
| const std::vector<std::string>& options, |
| const base::FilePath& mount_path, |
| MountErrorType* error) { |
| CHECK(!source_path.empty()) << "Invalid source path argument"; |
| CHECK(!mount_path.empty()) << "Invalid mount path argument"; |
| |
| Disk disk; |
| if (!disk_monitor_->GetDiskByDevicePath(base::FilePath(source_path), &disk)) { |
| LOG(ERROR) << quote(source_path) << " is not a valid device"; |
| *error = MOUNT_ERROR_INVALID_DEVICE_PATH; |
| return nullptr; |
| } |
| |
| if (disk.is_on_boot_device) { |
| LOG(ERROR) << quote(source_path) |
| << " is on boot device and not allowed to mount"; |
| *error = MOUNT_ERROR_INVALID_DEVICE_PATH; |
| return nullptr; |
| } |
| |
| if (disk.device_file.empty()) { |
| LOG(ERROR) << quote(source_path) << " does not have a device file"; |
| *error = MOUNT_ERROR_INVALID_DEVICE_PATH; |
| return nullptr; |
| } |
| |
| if (!platform()->PathExists(disk.device_file)) { |
| PLOG(ERROR) << quote(source_path) << " has device file " |
| << quote(disk.device_file) << " which is missing"; |
| *error = MOUNT_ERROR_INVALID_DEVICE_PATH; |
| return nullptr; |
| } |
| |
| std::string device_filesystem_type = |
| filesystem_type.empty() ? disk.filesystem_type : filesystem_type; |
| metrics()->RecordDeviceMediaType(disk.media_type); |
| metrics()->RecordFilesystemType(device_filesystem_type); |
| if (device_filesystem_type.empty()) { |
| LOG(ERROR) << "Cannot determine filesystem type of device " |
| << quote(source_path); |
| *error = MOUNT_ERROR_UNKNOWN_FILESYSTEM; |
| return nullptr; |
| } |
| |
| const auto it = mounters_.find(device_filesystem_type); |
| if (it == mounters_.end()) { |
| LOG(ERROR) << "Cannot handle filesystem type " |
| << quote(device_filesystem_type) << " of device " |
| << quote(source_path); |
| *error = MOUNT_ERROR_UNSUPPORTED_FILESYSTEM; |
| return nullptr; |
| } |
| |
| const Mounter* const mounter = it->second.get(); |
| |
| auto applied_options = options; |
| if (const bool media_read_only = disk.is_read_only || disk.IsOpticalDisk(); |
| media_read_only && !IsReadOnlyMount(applied_options)) { |
| applied_options.push_back("ro"); |
| } |
| |
| std::unique_ptr<MountPoint> mount_point = |
| mounter->Mount(disk.device_file, mount_path, applied_options, error); |
| if (*error != MOUNT_ERROR_NONE) { |
| DCHECK(!mount_point); |
| // Try to mount the filesystem read-only if mounting it read-write failed. |
| if (!IsReadOnlyMount(applied_options)) { |
| LOG(INFO) << "Trying to mount " << quote(source_path) << " read-only"; |
| applied_options.push_back("ro"); |
| mount_point = |
| mounter->Mount(disk.device_file, mount_path, applied_options, error); |
| } |
| } |
| |
| if (*error != MOUNT_ERROR_NONE) { |
| DCHECK(!mount_point); |
| return nullptr; |
| } |
| |
| return MaybeWrapMountPointForEject(std::move(mount_point), disk); |
| } |
| |
| std::string DiskManager::SuggestMountPath( |
| const std::string& source_path) const { |
| Disk disk; |
| disk_monitor_->GetDiskByDevicePath(base::FilePath(source_path), &disk); |
| // If GetDiskByDevicePath fails, disk.GetPresentationName() returns |
| // the fallback presentation name. |
| return mount_root().Append(disk.GetPresentationName()).value(); |
| } |
| |
| bool DiskManager::ShouldReserveMountPathOnError( |
| MountErrorType error_type) const { |
| return error_type == MOUNT_ERROR_UNKNOWN_FILESYSTEM || |
| error_type == MOUNT_ERROR_UNSUPPORTED_FILESYSTEM; |
| } |
| |
| bool DiskManager::EjectDevice(const std::string& device_file) { |
| if (eject_device_on_unmount_) { |
| return device_ejector_->Eject(device_file); |
| } |
| return true; |
| } |
| |
| std::unique_ptr<MountPoint> DiskManager::MaybeWrapMountPointForEject( |
| std::unique_ptr<MountPoint> mount_point, const Disk& disk) { |
| DCHECK(mount_point); |
| |
| if (disk.IsOpticalDisk()) { |
| mount_point->SetEject(base::BindOnce( |
| [](DiskManager* const disk_manager, const std::string& device_file) { |
| if (!disk_manager->EjectDevice(device_file)) |
| LOG(ERROR) << "Cannot eject device " << quote(device_file); |
| }, |
| this, disk.device_file)); |
| } |
| |
| return mount_point; |
| } |
| |
| void DiskManager::UnmountAll() { |
| // UnmountAll() is called when a user session ends. We do not want to eject |
| // devices in that situation and thus set |eject_device_on_unmount_| to |
| // false temporarily to prevent devices from being ejected upon unmount. |
| eject_device_on_unmount_ = false; |
| MountManager::UnmountAll(); |
| eject_device_on_unmount_ = true; |
| } |
| |
| } // namespace cros_disks |