blob: 0318175e5aec48c0e25e5ae6c2a1d3a774af40d4 [file] [log] [blame]
// Copyright (c) 2013 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 "cros-disks/fuse_mounter.h"
// Has to come before linux/fs.h due to conflicting definitions of MS_*
// constants.
#include <sys/mount.h>
#include <fcntl.h>
#include <linux/capability.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <base/bind.h>
#include <base/callback_helpers.h>
#include <base/files/file.h>
#include <base/logging.h>
#include <base/memory/weak_ptr.h>
#include <base/stl_util.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <brillo/process/process_reaper.h>
#include "cros-disks/error_logger.h"
#include "cros-disks/mount_point.h"
#include "cros-disks/platform.h"
#include "cros-disks/quote.h"
#include "cros-disks/sandboxed_process.h"
#include "cros-disks/uri.h"
namespace cros_disks {
namespace {
const mode_t kSourcePathPermissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
const char kFuseDeviceFile[] = "/dev/fuse";
template <typename T>
base::Optional<T> UnsetIfEmpty(T value) {
if (value.empty())
return base::Optional<T>();
return base::Optional<T>(std::move(value));
}
// TODO(dats): Remove when it's done beforehead by the caller.
OwnerUser ResolveUserOrDie(const Platform* platform,
const std::string& user,
const std::string& group) {
OwnerUser result;
PCHECK(platform->GetUserAndGroupId(user, &result.uid, &result.gid));
if (!group.empty()) {
PCHECK(platform->GetGroupId(group, &result.gid));
}
return result;
}
class FUSEMountPoint : public MountPoint {
public:
FUSEMountPoint(const base::FilePath& path, const Platform* platform)
: MountPoint(path), platform_(platform) {}
FUSEMountPoint(const FUSEMountPoint&) = delete;
FUSEMountPoint& operator=(const FUSEMountPoint&) = delete;
~FUSEMountPoint() override { DestructorUnmount(); }
base::WeakPtr<FUSEMountPoint> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
static void CleanUpCallback(const base::FilePath& mount_path,
base::WeakPtr<FUSEMountPoint> ptr,
const siginfo_t& info) {
CHECK_EQ(SIGCHLD, info.si_signo);
if (info.si_code != CLD_EXITED) {
LOG(WARNING) << "FUSE daemon for " << quote(mount_path)
<< " crashed with code " << info.si_code << " and status "
<< info.si_status;
} else if (info.si_status != 0) {
LOG(WARNING) << "FUSE daemon for " << quote(mount_path)
<< " exited with status " << info.si_status;
} else {
LOG(INFO) << "FUSE daemon for " << quote(mount_path)
<< " exited normally";
}
if (!ptr) {
// If the MountPoint instance has been deleted, it was
// already unmounted and cleaned up due to a
// request from the browser (or logout). In this
// case, there's nothing to do.
// TODO(dats): Consolidate this logic into centralized place (likely
// MountPoint base class).
return;
}
ptr->CleanUp();
}
private:
// MountPoint overrides:
MountErrorType UnmountImpl() override {
// We take a 2-step approach to unmounting FUSE filesystems. First, try a
// normal unmount. This lets the VFS flush any pending data and lets the
// filesystem shut down cleanly. If the filesystem is busy, force unmount
// the filesystem. This is done because there is no good recovery path the
// user can take, and these filesystem are sometimes unmounted implicitly on
// login/logout/suspend. This action is similar to native filesystems (i.e.
// FAT32, ext2/3/4, etc) which are lazy unmounted if a regular unmount fails
// because the filesystem is busy.
MountErrorType error = platform_->Unmount(path().value(), 0 /* flags */);
if (error != MOUNT_ERROR_PATH_ALREADY_MOUNTED) {
// MOUNT_ERROR_PATH_ALREADY_MOUNTED is returned on EBUSY.
return error;
}
// For FUSE filesystems, MNT_FORCE will cause the kernel driver to
// immediately close the channel to the user-space driver program and cancel
// all outstanding requests. However, if any program is still accessing the
// filesystem, the umount2() will fail with EBUSY and the mountpoint will
// still be attached. Since the mountpoint is no longer valid, use
// MNT_DETACH to also force the mountpoint to be disconnected.
LOG(WARNING) << "Mount point " << quote(path())
<< " is busy, using force unmount";
return platform_->Unmount(path().value(), MNT_FORCE | MNT_DETACH);
}
void CleanUp() {
MountErrorType unmount_error = Unmount();
LOG_IF(ERROR, unmount_error != MOUNT_ERROR_NONE)
<< "Cannot unmount FUSE mount point " << quote(path())
<< " after process exit: " << unmount_error;
if (!platform_->RemoveEmptyDirectory(path().value())) {
PLOG(ERROR) << "Cannot remove FUSE mount point " << quote(path().value())
<< " after process exit";
}
}
const Platform* platform_;
base::WeakPtrFactory<FUSEMountPoint> weak_factory_{this};
};
bool GetPhysicalBlockSize(const std::string& source, int* size) {
base::ScopedFD fd(open(source.c_str(), O_RDONLY | O_CLOEXEC));
*size = 0;
if (!fd.is_valid()) {
PLOG(WARNING) << "Couldn't open " << source;
return false;
}
if (ioctl(fd.get(), BLKPBSZGET, size) < 0) {
PLOG(WARNING) << "Failed to get block size for" << source;
return false;
}
return true;
}
} // namespace
FUSESandboxedProcessFactory::FUSESandboxedProcessFactory(
const Platform* platform,
SandboxedExecutable executable,
OwnerUser run_as,
bool has_network_access,
std::vector<gid_t> supplementary_groups,
base::Optional<base::FilePath> mount_namespace)
: platform_(platform),
executable_(std::move(executable.executable)),
seccomp_policy_(std::move(executable.seccomp_policy)),
run_as_(std::move(run_as)),
has_network_access_(has_network_access),
supplementary_groups_(std::move(supplementary_groups)),
mount_namespace_(std::move(mount_namespace)) {
CHECK(executable_.IsAbsolute());
if (seccomp_policy_) {
CHECK(seccomp_policy_.value().IsAbsolute());
}
if (mount_namespace_) {
CHECK(mount_namespace_.value().IsAbsolute());
}
}
FUSESandboxedProcessFactory::~FUSESandboxedProcessFactory() = default;
std::unique_ptr<SandboxedProcess>
FUSESandboxedProcessFactory::CreateSandboxedProcess() const {
auto sandbox = std::make_unique<SandboxedProcess>();
if (!ConfigureSandbox(sandbox.get()))
return nullptr;
return sandbox;
}
bool FUSESandboxedProcessFactory::ConfigureSandbox(
SandboxedProcess* sandbox) const {
sandbox->SetCapabilities(0);
sandbox->SetNoNewPrivileges();
// The FUSE mount program is put under a new mount namespace, so mounts
// inside that namespace don't normally propagate.
sandbox->NewMountNamespace();
sandbox->SkipRemountPrivate();
// TODO(benchan): Re-enable cgroup namespace when either Chrome OS
// kernel 3.8 supports it or no more supported devices use kernel
// 3.8.
// mount_process.NewCgroupNamespace();
sandbox->NewIpcNamespace();
sandbox->NewPidNamespace();
// Prepare mounts for pivot_root.
if (!sandbox->SetUpMinimalMounts()) {
LOG(ERROR) << "Cannot set up minijail mounts";
return false;
}
// /run is the place where mutable system configs are being kept.
// We don't expose them by default, but to be able to bind them when
// needed /run needs to be writeable.
if (!sandbox->Mount("tmpfs", "/run", "tmpfs", "mode=0755,size=1M")) {
LOG(ERROR) << "Cannot mount /run";
return false;
}
if (!has_network_access_) {
sandbox->NewNetworkNamespace();
} else {
// Network DNS configs are in /run/shill.
if (!sandbox->BindMount("/run/shill", "/run/shill", false, false)) {
LOG(ERROR) << "Cannot bind /run/shill";
return false;
}
// Hardcoded hosts are mounted into /etc/hosts.d when Crostini is enabled.
if (platform_->PathExists("/etc/hosts.d") &&
!sandbox->BindMount("/etc/hosts.d", "/etc/hosts.d", false, false)) {
LOG(ERROR) << "Cannot bind /etc/hosts.d";
return false;
}
}
if (!sandbox->EnterPivotRoot()) {
LOG(ERROR) << "Cannot pivot root";
return false;
}
if (seccomp_policy_) {
if (!platform_->PathExists(seccomp_policy_.value().value())) {
LOG(ERROR) << "Seccomp policy " << quote(seccomp_policy_.value())
<< " is missing";
return false;
}
sandbox->LoadSeccompFilterPolicy(seccomp_policy_.value().value());
}
sandbox->SetUserId(run_as_.uid);
sandbox->SetGroupId(run_as_.gid);
if (!supplementary_groups_.empty()) {
sandbox->SetSupplementaryGroupIds(supplementary_groups_);
}
// Enter mount namespace in the sandbox if necessary.
if (mount_namespace_) {
sandbox->EnterExistingMountNamespace(mount_namespace_.value().value());
}
if (!platform_->PathExists(executable_.value())) {
LOG(ERROR) << "Cannot find mount program " << quote(executable_);
return false;
}
sandbox->AddArgument(executable_.value());
return true;
}
FUSEMounter::FUSEMounter(const Platform* platform,
brillo::ProcessReaper* process_reaper,
std::string filesystem_type,
bool nosymfollow)
: platform_(platform),
process_reaper_(process_reaper),
filesystem_type_(std::move(filesystem_type)),
nosymfollow_(nosymfollow) {}
FUSEMounter::~FUSEMounter() = default;
std::unique_ptr<MountPoint> FUSEMounter::Mount(
const std::string& source,
const base::FilePath& target_path,
std::vector<std::string> params,
MountErrorType* error) const {
// Read-only is the only parameter that has any effect at this layer.
const bool read_only = IsReadOnlyMount(params);
const base::File fuse_file = base::File(
base::FilePath(kFuseDeviceFile),
base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE);
if (!fuse_file.IsValid()) {
LOG(ERROR) << "Unable to open FUSE device file. Error: "
<< fuse_file.error_details() << " "
<< base::File::ErrorToString(fuse_file.error_details());
*error = MOUNT_ERROR_INTERNAL;
return nullptr;
}
// Mount options for FUSE:
// fd - File descriptor for /dev/fuse.
// user_id/group_id - user/group for file access control. Essentially
// bypassed due to allow_other, but still required to be set.
// allow_other - Allows users other than user_id/group_id to access files
// on the file system. By default, FUSE prevents any process other than
// ones running under user_id/group_id to access files, regardless of
// the file's permissions.
// default_permissions - Enforce permission checking.
// rootmode - Mode bits for the root inode.
std::string fuse_mount_options = base::StringPrintf(
"fd=%d,user_id=%u,group_id=%u,allow_other,default_permissions,"
"rootmode=%o",
fuse_file.GetPlatformFile(), kChronosUID, kChronosAccessGID, S_IFDIR);
std::string fuse_type = "fuse";
std::string source_descr = source;
base::stat_wrapper_t statbuf = {0};
if (platform_->Lstat(source, &statbuf) && S_ISBLK(statbuf.st_mode)) {
int blksize = 0;
// TODO(crbug.com/931500): It's possible that specifying a block size equal
// to the file system cluster size (which might be larger than the physical
// block size) might be more efficient. Data would be needed to see what
// kind of performance benefit, if any, could be gained. At the very least,
// specify the block size of the underlying device. Without this, UFS cards
// with 4k sector size will fail to mount.
if (GetPhysicalBlockSize(source, &blksize) && blksize > 0)
fuse_mount_options.append(base::StringPrintf(",blksize=%d", blksize));
LOG(INFO) << "Source file " << quote(source)
<< " is a block device with block size " << blksize;
fuse_type = "fuseblk";
} else {
source_descr = "fuse:" + source;
}
if (!filesystem_type_.empty()) {
fuse_type += ".";
fuse_type += filesystem_type_;
}
*error = platform_->Mount(source_descr, target_path.value(), fuse_type,
MountOptions::kMountFlags | MS_DIRSYNC |
(read_only ? MS_RDONLY : 0) |
(nosymfollow_ ? MS_NOSYMFOLLOW : 0),
fuse_mount_options);
if (*error != MOUNT_ERROR_NONE) {
LOG(ERROR) << "Cannot perform unprivileged FUSE mount: " << *error;
return nullptr;
}
pid_t pid =
StartDaemon(fuse_file, source, target_path, std::move(params), error);
if (*error != MOUNT_ERROR_NONE || pid == Process::kInvalidProcessId) {
LOG(ERROR) << "FUSE daemon start failure: " << *error;
LOG(INFO) << "FUSE cleanup on start failure for " << quote(target_path);
MountErrorType unmount_error =
platform_->Unmount(target_path.value(), MNT_FORCE | MNT_DETACH);
LOG_IF(ERROR, unmount_error != MOUNT_ERROR_NONE)
<< "Cannot unmount FUSE mount point " << quote(target_path)
<< " after launch failure: " << unmount_error;
return nullptr;
}
// At this point, the FUSE daemon has successfully started.
std::unique_ptr<FUSEMountPoint> mount_point =
std::make_unique<FUSEMountPoint>(target_path, platform_);
// Add a watcher that cleans up the FUSE mount when the process exits.
// This is defined as in-jail "init" process, denoted by pid(),
// terminates, which happens only when the last process in the jailed PID
// namespace terminates.
process_reaper_->WatchForChild(
FROM_HERE, pid,
base::BindOnce(&FUSEMountPoint::CleanUpCallback, target_path,
mount_point->GetWeakPtr()));
*error = MOUNT_ERROR_NONE;
return std::move(mount_point);
}
pid_t FUSEMounter::StartDaemon(const base::File& fuse_file,
const std::string& source,
const base::FilePath& target_path,
std::vector<std::string> params,
MountErrorType* error) const {
auto mount_process =
PrepareSandbox(source, target_path, std::move(params), error);
if (*error != MOUNT_ERROR_NONE) {
return Process::kInvalidProcessId;
}
mount_process->AddArgument(
base::StringPrintf("/dev/fd/%d", fuse_file.GetPlatformFile()));
std::vector<std::string> output;
const int return_code = mount_process->Run(&output);
*error = InterpretReturnCode(return_code);
if (*error != MOUNT_ERROR_NONE) {
const auto& executable = mount_process->arguments()[0];
if (!output.empty()) {
LOG(ERROR) << "FUSE mount program " << quote(executable) << " outputted "
<< output.size() << " lines:";
for (const std::string& line : output) {
LOG(ERROR) << line;
}
}
LOG(ERROR) << "FUSE mount program " << quote(executable)
<< " returned error code " << return_code;
return Process::kInvalidProcessId;
}
return mount_process->pid();
}
MountErrorType FUSEMounter::InterpretReturnCode(int return_code) const {
if (return_code != 0)
return MOUNT_ERROR_MOUNT_PROGRAM_FAILED;
return MOUNT_ERROR_NONE;
}
FUSEMounterHelper::FUSEMounterHelper(
const Platform* platform,
brillo::ProcessReaper* process_reaper,
std::string filesystem_type,
bool nosymfollow,
const SandboxedProcessFactory* sandbox_factory)
: FUSEMounter(
platform, process_reaper, std::move(filesystem_type), nosymfollow),
sandbox_factory_(sandbox_factory) {}
FUSEMounterHelper::~FUSEMounterHelper() = default;
std::unique_ptr<SandboxedProcess> FUSEMounterHelper::PrepareSandbox(
const std::string& source,
const base::FilePath& target_path,
std::vector<std::string> params,
MountErrorType* error) const {
auto sandbox = sandbox_factory_->CreateSandboxedProcess();
*error =
ConfigureSandbox(source, target_path, std::move(params), sandbox.get());
if (*error != MOUNT_ERROR_NONE) {
return nullptr;
}
return sandbox;
}
FUSEMounterLegacy::FUSEMounterLegacy(Params params)
: FUSEMounter(params.platform,
params.process_reaper,
std::move(params.filesystem_type),
params.nosymfollow),
metrics_(params.metrics),
metrics_name_(std::move(params.metrics_name)),
seccomp_policy_(),
bind_paths_(std::move(params.bind_paths)),
password_needed_codes_(std::move(params.password_needed_codes)),
mount_options_(std::move(params.mount_options)),
sandbox_factory_(
platform(),
{base::FilePath(std::move(params.mount_program)),
UnsetIfEmpty(base::FilePath(std::move(params.seccomp_policy)))},
ResolveUserOrDie(platform(), params.mount_user, params.mount_group),
params.network_access,
std::move(params.supplementary_groups),
UnsetIfEmpty(base::FilePath(std::move(params.mount_namespace)))) {}
void FUSEMounterLegacy::CopyPassword(const std::vector<std::string>& options,
Process* const process) const {
DCHECK(process);
// Is the process "password-aware"?
if (password_needed_codes_.empty())
return;
// Is there a password available in options?
const base::StringPiece prefix = "password=";
const auto it = std::find_if(
options.cbegin(), options.cend(),
[prefix](const base::StringPiece s) { return s.starts_with(prefix); });
if (it == options.cend())
return;
// Pass the password via stdin.
process->SetStdIn(it->substr(prefix.size()));
}
std::unique_ptr<SandboxedProcess> FUSEMounterLegacy::PrepareSandbox(
const std::string& source,
const base::FilePath& target_path,
std::vector<std::string> params,
MountErrorType* error) const {
std::unique_ptr<SandboxedProcess> mount_process = CreateSandboxedProcess();
// TODO(crbug.com/1053778) Only create the necessary tmpfs filesystems.
for (const char* const dir : {"/home", "/media"}) {
if (!mount_process->Mount("tmpfs", dir, "tmpfs", "mode=0755,size=10M")) {
LOG(ERROR) << "Cannot mount " << quote(dir);
*error = MOUNT_ERROR_INTERNAL;
return nullptr;
}
}
// Data dirs if any are mounted inside /run/fuse.
if (!mount_process->BindMount("/run/fuse", "/run/fuse", false, false)) {
LOG(ERROR) << "Can't bind /run/fuse";
return nullptr;
}
// If a block device is being mounted, bind mount it into the sandbox.
if (base::StartsWith(source, "/dev/", base::CompareCase::SENSITIVE)) {
// Re-own source.
if (!platform()->SetOwnership(source, getuid(),
sandbox_factory_.run_as().gid) ||
!platform()->SetPermissions(source, kSourcePathPermissions)) {
LOG(ERROR) << "Can't set up permissions on " << quote(source);
*error = MOUNT_ERROR_INSUFFICIENT_PERMISSIONS;
return nullptr;
}
if (!mount_process->BindMount(source, source, true, false)) {
LOG(ERROR) << "Cannot bind mount device " << quote(source);
*error = MOUNT_ERROR_INVALID_ARGUMENT;
return nullptr;
}
}
// This is for additional data dirs.
for (const BindPath& bind_path : bind_paths_) {
if (!mount_process->BindMount(bind_path.path, bind_path.path,
bind_path.writable, bind_path.recursive)) {
LOG(ERROR) << "Cannot bind-mount " << quote(bind_path.path);
*error = MOUNT_ERROR_INVALID_ARGUMENT;
return nullptr;
}
}
{
// TODO(dats): This it leaking legacy options implementation. Remove it.
std::string options_string = mount_options_.ToFuseMounterOptions();
DCHECK(!options_string.empty());
mount_process->AddArgument("-o");
mount_process->AddArgument(std::move(options_string));
}
if (!source.empty()) {
mount_process->AddArgument(source);
}
CopyPassword(params, mount_process.get());
return mount_process;
}
MountErrorType FUSEMounterLegacy::InterpretReturnCode(int return_code) const {
if (metrics_ && !metrics_name_.empty())
metrics_->RecordFuseMounterErrorCode(metrics_name_, return_code);
if (base::Contains(password_needed_codes_, return_code))
return MOUNT_ERROR_NEED_PASSWORD;
return FUSEMounter::InterpretReturnCode(return_code);
}
bool FUSEMounterLegacy::CanMount(const std::string& source,
const std::vector<std::string>& params,
base::FilePath* suggested_name) const {
NOTREACHED();
return true;
}
std::unique_ptr<SandboxedProcess> FUSEMounterLegacy::CreateSandboxedProcess()
const {
return sandbox_factory_.CreateSandboxedProcess();
}
} // namespace cros_disks