blob: 355c33d7a9d9ee99fe8f1d174824d70da66d98a5 [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"
#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 <memory>
#include <string>
#include <utility>
#include <base/macros.h>
#include <base/bind.h>
#include <base/callback_helpers.h>
#include <base/files/file.h>
#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <base/strings/string_util.h>
#include <brillo/process_reaper.h>
#include "cros-disks/error_logger.h"
#include "cros-disks/platform.h"
#include "cros-disks/quote.h"
#include "cros-disks/sandboxed_process.h"
namespace cros_disks {
namespace {
const mode_t kSourcePathPermissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
const char kFuseDeviceFile[] = "/dev/fuse";
const MountOptions::Flags kRequiredFuseMountFlags =
MS_NODEV | MS_NOEXEC | MS_NOSUID;
void CleanUpCallback(base::OnceClosure cleanup,
const base::FilePath& mount_path,
const siginfo_t& info) {
CHECK_EQ(SIGCHLD, info.si_signo);
if (info.si_code != CLD_EXITED || info.si_status != 0) {
LOG(WARNING) << "FUSE daemon for " << quote(mount_path)
<< " exited with code " << info.si_code << " and status "
<< info.si_status;
} else {
LOG(INFO) << "FUSE daemon for " << quote(mount_path) << " exited normally";
}
std::move(cleanup).Run();
}
MountErrorType ConfigureCommonSandbox(SandboxedProcess* sandbox,
const Platform* platform,
bool network_ns,
const base::FilePath& seccomp) {
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();
// TODO(crbug.com/707327): Remove this when we get rid of AVFS.
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();
if (network_ns) {
sandbox->NewNetworkNamespace();
}
if (!seccomp.empty()) {
if (!platform->PathExists(seccomp.value())) {
LOG(ERROR) << "Seccomp policy " << quote(seccomp) << " is missing";
return MOUNT_ERROR_INTERNAL;
}
sandbox->LoadSeccompFilterPolicy(seccomp.value());
}
// Prepare mounts for pivot_root.
if (!sandbox->SetUpMinimalMounts()) {
LOG(ERROR) << "Can't set up minijail mounts";
return MOUNT_ERROR_INTERNAL;
}
// Data dirs if any are mounted inside /run/fuse.
if (!sandbox->Mount("tmpfs", "/run", "tmpfs", "mode=0755,size=10M")) {
LOG(ERROR) << "Can't mount /run";
return MOUNT_ERROR_INTERNAL;
}
if (!sandbox->BindMount("/run/fuse", "/run/fuse", false, false)) {
LOG(ERROR) << "Can't bind /run/fuse";
return MOUNT_ERROR_INTERNAL;
}
if (!network_ns) {
// Network DNS configs are in /run/shill.
if (!sandbox->BindMount("/run/shill", "/run/shill", false, false)) {
LOG(ERROR) << "Can't bind /run/shill";
return MOUNT_ERROR_INTERNAL;
}
// 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) << "Can't bind /etc/hosts.d";
return MOUNT_ERROR_INTERNAL;
}
}
if (!sandbox->EnterPivotRoot()) {
LOG(ERROR) << "Can't pivot root";
return MOUNT_ERROR_INTERNAL;
}
return MOUNT_ERROR_NONE;
}
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;
}
MountErrorType MountFuseDevice(const Platform* platform,
const std::string& source,
const std::string& filesystem_type,
const base::FilePath& target,
const base::File& fuse_file,
uid_t mount_user_id,
gid_t mount_group_id,
const MountOptions& options) {
// 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(), mount_user_id, mount_group_id, S_IFDIR);
// "nosymfollow" is a special mount option that's passed to the Chromium LSM
// and not forwarded to the FUSE driver. If it's set, add it as a mount
// option.
if (options.HasOption(MountOptions::kOptionNoSymFollow)) {
fuse_mount_options.append(",");
fuse_mount_options.append(MountOptions::kOptionNoSymFollow);
}
std::string fuse_type = "fuse";
struct stat statbuf = {0};
if (stat(source.c_str(), &statbuf) == 0 && 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";
}
if (!filesystem_type.empty()) {
fuse_type += ".";
fuse_type += filesystem_type;
}
auto flags = options.ToMountFlagsAndData().first;
return platform->Mount(source.empty() ? filesystem_type : source,
target.value(), fuse_type,
flags | kRequiredFuseMountFlags, fuse_mount_options);
}
} // namespace
FUSEMounter::FUSEMounter(const std::string& source_path,
const std::string& target_path,
const std::string& filesystem_type,
const MountOptions& mount_options,
const Platform* platform,
brillo::ProcessReaper* process_reaper,
const std::string& mount_program_path,
const std::string& mount_user,
const std::string& seccomp_policy,
const std::vector<BindPath>& accessible_paths,
bool permit_network_access,
const std::string& mount_group)
: MounterCompat(filesystem_type,
source_path,
base::FilePath(target_path),
mount_options),
platform_(platform),
process_reaper_(process_reaper),
mount_program_path_(mount_program_path),
mount_user_(mount_user),
mount_group_(mount_group),
seccomp_policy_(seccomp_policy),
accessible_paths_(accessible_paths),
permit_network_access_(permit_network_access) {}
MountErrorType FUSEMounter::MountImpl() const {
auto mount_process = CreateSandboxedProcess();
MountErrorType error = ConfigureCommonSandbox(
mount_process.get(), platform_, !permit_network_access_,
base::FilePath(seccomp_policy_));
if (error != MOUNT_ERROR_NONE) {
return error;
}
uid_t mount_user_id;
gid_t mount_group_id;
if (!platform_->GetUserAndGroupId(mount_user_, &mount_user_id,
&mount_group_id)) {
LOG(ERROR) << "Cannot resolve user " << quote(mount_user_);
return MOUNT_ERROR_INTERNAL;
}
if (!mount_group_.empty() &&
!platform_->GetGroupId(mount_group_, &mount_group_id)) {
return MOUNT_ERROR_INTERNAL;
}
mount_process->SetUserId(mount_user_id);
mount_process->SetGroupId(mount_group_id);
if (!platform_->PathExists(mount_program_path_)) {
LOG(ERROR) << "Cannot find mount program " << quote(mount_program_path_);
return MOUNT_ERROR_MOUNT_PROGRAM_NOT_FOUND;
}
mount_process->AddArgument(mount_program_path_);
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());
return MOUNT_ERROR_INTERNAL;
}
error = MountFuseDevice(platform_, source(), filesystem_type(),
base::FilePath(target_path()), fuse_file,
mount_user_id, mount_group_id, mount_options());
if (error != MOUNT_ERROR_NONE) {
LOG(ERROR) << "Can't perform unprivileged FUSE mount: " << error;
return error;
}
// The |fuse_failure_unmounter| closure runner is used to unmount the FUSE
// filesystem if any part of starting the FUSE helper process fails.
// TODO(crbug.com/993857): Use base::BindOnce().
base::ScopedClosureRunner fuse_cleanup_runner(base::Bind(
[](const Platform* platform, const std::string& target_path) {
if (!platform->DirectoryExists(target_path)) {
// If the mount directory doesn't exist, don't try to unmount and
// remove it. This will happen when the browser requests to unmount
// the filesystem. In this case, both the following operations are
// unnecessary and will fail, resulting in log spam.
return;
}
MountErrorType unmount_error = platform->Unmount(target_path, 0);
LOG_IF(ERROR, unmount_error != MOUNT_ERROR_NONE)
<< "Cannot unmount FUSE mount point " << quote(target_path) << ": "
<< unmount_error;
if (!platform->RemoveEmptyDirectory(target_path)) {
PLOG(ERROR) << "Cannot remove FUSE mount point "
<< quote(target_path);
}
},
platform_, target_path().value()));
// Source might be an URI. Only try to re-own source if it looks like
// an existing path.
if (!source().empty() && platform_->PathExists(source())) {
if (!platform_->SetOwnership(source(), getuid(), mount_group_id) ||
!platform_->SetPermissions(source(), kSourcePathPermissions)) {
LOG(ERROR) << "Can't set up permissions on the source";
return MOUNT_ERROR_INSUFFICIENT_PERMISSIONS;
}
}
// If a block device is being mounted, bind mount it into the sandbox.
if (base::StartsWith(source(), "/dev/", base::CompareCase::SENSITIVE)) {
if (!mount_process->BindMount(source(), source(), true, false)) {
LOG(ERROR) << "Cannot bind mount device " << quote(source());
return MOUNT_ERROR_INVALID_ARGUMENT;
}
}
// TODO(crbug.com/933018): Remove when DriveFS helper is refactored.
if (!mount_process->Mount("tmpfs", "/home", "tmpfs", "mode=0755,size=10M")) {
LOG(ERROR) << "Can't mount /home";
return MOUNT_ERROR_INTERNAL;
}
// This is for additional data dirs.
for (const auto& path : accessible_paths_) {
if (!mount_process->BindMount(path.path, path.path, path.writable,
path.recursive)) {
LOG(ERROR) << "Can't bind " << path.path;
return MOUNT_ERROR_INVALID_ARGUMENT;
}
}
std::string options_string = mount_options().ToString();
if (!options_string.empty()) {
mount_process->AddArgument("-o");
mount_process->AddArgument(options_string);
}
if (!source().empty()) {
mount_process->AddArgument(source());
}
mount_process->AddArgument(
base::StringPrintf("/dev/fd/%d", fuse_file.GetPlatformFile()));
std::vector<std::string> output;
int return_code = mount_process->Run(&output);
if (return_code != 0) {
LOG(ERROR) << "FUSE mount program failed with return code " << return_code;
if (!output.empty()) {
LOG(ERROR) << "FUSE mount program outputted " << output.size()
<< " lines:";
for (const std::string& line : output) {
LOG(ERROR) << line;
}
}
return MOUNT_ERROR_MOUNT_PROGRAM_FAILED;
}
// At this point, the FUSE daemon has successfully started, so repurpose
// the FUSE cleanup closure runner to run on daemon quitting.
// 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, mount_process->pid(),
base::BindOnce(CleanUpCallback, fuse_cleanup_runner.Release(),
target_path()));
return MOUNT_ERROR_NONE;
}
std::unique_ptr<SandboxedProcess> FUSEMounter::CreateSandboxedProcess() const {
return std::make_unique<SandboxedProcess>();
}
} // namespace cros_disks