blob: b823cc6071049a1b5d152aba7b581d22ee62723f [file] [log] [blame]
// Copyright 2018 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 "vm_tools/seneschal/service.h"
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <mntent.h>
#include <signal.h>
#include <stdint.h>
#include <sys/mount.h>
#include <sys/resource.h>
#include <sys/signalfd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/vm_sockets.h> // needs to come after sys/socket.h
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/bind_helpers.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/location.h>
#include <base/logging.h>
#include <base/stl_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_piece.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/threading/thread_task_runner_handle.h>
#include <base/time/time.h>
#include <brillo/file_utils.h>
#include <chromeos/dbus/service_constants.h>
#include <chromeos/libminijail.h>
#include <chromeos/scoped_minijail.h>
#include <seneschal/proto_bindings/seneschal_service.pb.h>
using std::string;
namespace vm_tools {
namespace seneschal {
namespace {
// Path to the runtime directory where we will create server jails.
constexpr char kRuntimeDir[] = "/run/seneschal";
// The chronos uid and gid. These are used for file system access.
constexpr uid_t kChronosUid = 1000;
constexpr gid_t kChronosGid = 1000;
// Access to android files requires android-everybody gid.
constexpr gid_t kAndroidEverybodyGid = 665357;
constexpr gid_t kSupplementaryGroups[] = {kAndroidEverybodyGid};
// The gid of the chronos-access group.
constexpr gid_t kChronosAccessGid = 1001;
// The uid used for authenticating with DBus.
constexpr uid_t kDbusAuthUid = 20115;
// How long we should wait for a server process to exit.
constexpr base::TimeDelta kServerExitTimeout = base::TimeDelta::FromSeconds(2);
// Path to the 9p server.
constexpr char kServerPath[] = "/usr/bin/9s";
constexpr char kServerRoot[] = "/fsroot";
constexpr char kSeccompPolicyPath[] = "/usr/share/policy/9s-seccomp.policy";
// Static prefix of SmbFs mount names.
constexpr char kSmbFsMountNamePrefix[] = "smbfs-";
// Max number of open files allowed per server.
constexpr rlim_t kMaxOpenFiles = 64 * 1024;
// `mkdir -p`, essentially. Reimplement all of base::CreateDirectory because
// we want mode 0755 instead of mode 0700.
bool MkdirRecursively(const base::FilePath& full_path) {
if (!full_path.IsAbsolute()) {
LOG(INFO) << "Relative paths are not supported: " << full_path.value();
return false;
}
// Collect a list of all parent directories.
std::vector<std::string> components;
full_path.GetComponents(&components);
DCHECK(!components.empty());
base::ScopedFD fd(open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW));
if (!fd.is_valid())
return false;
// Iterate through the parents and create the missing ones. '+ 1' is for
// skipping "/".
for (std::vector<std::string>::const_iterator i = components.begin() + 1;
i != components.end(); ++i) {
// Try to create the directory. Note that Chromium's MkdirRecursively() uses
// 0700, but we use 0755.
if (mkdirat(fd.get(), i->c_str(), 0755) != 0) {
if (errno != EEXIST) {
PLOG(ERROR) << "Failed to mkdirat " << *i
<< ": full_path=" << full_path.value();
return false;
}
// The path already exists. Make sure that the path is a directory.
struct stat st;
if (fstatat(fd.get(), i->c_str(), &st, AT_SYMLINK_NOFOLLOW) != 0) {
PLOG(ERROR) << "Failed to fstatat " << *i
<< ": full_path=" << full_path.value();
return false;
}
if (!S_ISDIR(st.st_mode)) {
LOG(ERROR) << *i << " is not a directory: st_mode=" << st.st_mode
<< ", full_path=" << full_path.value();
return false;
}
}
// Updates the FD so it refers to the new directory created or checked
// above.
const int new_fd =
openat(fd.get(), i->c_str(), O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);
if (new_fd < 0) {
PLOG(ERROR) << "Failed to openat " << *i
<< ": full_path=" << full_path.value();
return false;
}
fd.reset(new_fd);
continue;
}
return true;
}
// Passes |method_call| to |handler| and passes the response to
// |response_sender|. If |handler| returns NULL, an empty response is created
// and sent.
void HandleSynchronousDBusMethodCall(
base::Callback<std::unique_ptr<dbus::Response>(dbus::MethodCall*)> handler,
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
std::unique_ptr<dbus::Response> response = handler.Run(method_call);
if (!response)
response = dbus::Response::FromMethodCall(method_call);
std::move(response_sender).Run(std::move(response));
}
} // namespace
Service::ServerInfo::ServerInfo(pid_t pid, base::FilePath root_dir)
: pid_(pid) {
CHECK(root_dir_.Set(root_dir));
}
Service::ServerInfo::ServerInfo(Service::ServerInfo&& other) noexcept
: pid_(other.pid_) {
CHECK(root_dir_.Set(other.root_dir_.Take()));
}
Service::ServerInfo& Service::ServerInfo::operator=(
Service::ServerInfo&& other) noexcept {
// Self assignment check is required.
if (this != &other) {
pid_ = other.pid_;
CHECK(root_dir_.Set(other.root_dir_.Take()));
}
return *this;
}
Service::ServerInfo::~ServerInfo() {
if (!root_dir_.IsValid()) {
// Nothing to see here.
return;
}
// Clean up the mounts so that we can delete the temporary directory. An
// error in any of these operations means that we cannot safely delete the
// directory. Instead the directory will get cleaned up when seneschal exits
// as this will delete the mount namespace and all the mounts in it.
string contents;
if (!base::ReadFileToString(base::FilePath("/proc/self/mounts"), &contents)) {
PLOG(ERROR) << "Unable to read contents of /proc/self/mounts; not deleting "
<< "runtime directory";
root_dir_.Take();
return;
}
std::vector<string> mounts;
for (base::StringPiece line : base::SplitStringPiece(
contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
std::vector<base::StringPiece> mount_data = base::SplitStringPiece(
line, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (mount_data.size() < 6) {
LOG(ERROR) << "Invalid mount data: " << line;
root_dir_.Take();
return;
}
// The mount point is the second column.
if (root_dir_.GetPath().IsParent(base::FilePath(mount_data[1]))) {
mounts.emplace_back(mount_data[1].as_string());
}
}
// Now unmount everything in reverse order.
for (auto iter = mounts.rbegin(), end = mounts.rend(); iter != end; ++iter) {
if (umount(iter->c_str()) != 0) {
PLOG(ERROR) << "Unable to unmount path; not deleting runtime directory";
root_dir_.Take();
return;
}
}
}
// static
std::unique_ptr<Service> Service::Create(base::Closure quit_closure) {
std::unique_ptr<Service> service(new Service(std::move(quit_closure)));
if (!service->Init()) {
service.reset();
}
return service;
}
Service::Service(base::Closure quit_closure)
: next_server_handle_(1),
quit_closure_(std::move(quit_closure)),
weak_factory_(this) {}
bool Service::Init() {
// Set up the dbus service.
dbus::Bus::Options opts;
opts.bus_type = dbus::Bus::SYSTEM;
bus_ = new dbus::Bus(std::move(opts));
// When authenticating with DBus a client process that wants to connect to
// the system dbus daemon sends an authentication request with its current
// effective uid. The dbus daemon then uses SO_PEERCRED to verify that the
// uid of the client process matches what it claims to be. Normally this is
// fine but when the client process runs inside a user namespace it thinks it
// has uid 0 inside the namespace while the dbus daemon, which runs outside
// the namespace, thinks it has some other uid. To deal with this we
// temprarily change our effective uid to match the effective uid outside the
// user namespace and then change it back once we have authenticated with the
// dbus daemon.
if (seteuid(kDbusAuthUid) != 0) {
PLOG(ERROR) << "Unable to change effective uid to " << kDbusAuthUid;
return false;
}
if (!bus_->Connect()) {
LOG(ERROR) << "Failed to connect to system bus";
return false;
}
if (seteuid(0) != 0) {
PLOG(ERROR) << "Unable to change effective uid back to 0";
return false;
}
// Add chronos-access to our list of supplementary groups. This is needed so
// that we can access the user's files in the /home directory.
gid_t list[NGROUPS_MAX] = {};
int count = getgroups(NGROUPS_MAX, list);
if (count < 0) {
PLOG(ERROR) << "Failed to get supplementary groups";
return false;
}
CHECK_LT(count, NGROUPS_MAX);
list[count++] = kChronosAccessGid;
if (setgroups(count, list) != 0) {
PLOG(ERROR) << "Failed to add chronos-access to supplementary groups";
return false;
}
exported_object_ =
bus_->GetExportedObject(dbus::ObjectPath(kSeneschalServicePath));
if (!exported_object_) {
LOG(ERROR) << "Failed to export " << kSeneschalServicePath << " object";
return false;
}
using ServiceMethod =
std::unique_ptr<dbus::Response> (Service::*)(dbus::MethodCall*);
const std::map<const char*, ServiceMethod> kServiceMethods = {
{kStartServerMethod, &Service::StartServer},
{kStopServerMethod, &Service::StopServer},
{kSharePathMethod, &Service::SharePath},
{kUnsharePathMethod, &Service::UnsharePath},
};
for (const auto& iter : kServiceMethods) {
bool ret = exported_object_->ExportMethodAndBlock(
kSeneschalInterface, iter.first,
base::Bind(&HandleSynchronousDBusMethodCall,
base::Bind(iter.second, base::Unretained(this))));
if (!ret) {
LOG(ERROR) << "Failed to export method " << iter.first;
return false;
}
}
if (!bus_->RequestOwnershipAndBlock(kSeneschalServiceName,
dbus::Bus::REQUIRE_PRIMARY)) {
LOG(ERROR) << "Failed to take ownership of " << kSeneschalServiceName;
return false;
}
// Set up the signalfd for receiving SIGCHLD and SIGTERM.
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGTERM);
signal_fd_.reset(signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC));
if (!signal_fd_.is_valid()) {
PLOG(ERROR) << "Failed to create signalfd";
return false;
}
watcher_ = base::FileDescriptorWatcher::WatchReadable(
signal_fd_.get(),
base::BindRepeating(&Service::OnSignalReadable, base::Unretained(this)));
if (!watcher_) {
LOG(ERROR) << "Failed to watch signalfd";
return false;
}
// Now block signals from the normal signal handling path so that we will get
// them via the signalfd.
if (sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) {
PLOG(ERROR) << "Failed to block signals via sigprocmask";
return false;
}
return true;
}
void Service::OnSignalReadable() {
struct signalfd_siginfo siginfo;
if (read(signal_fd_.get(), &siginfo, sizeof(siginfo)) != sizeof(siginfo)) {
PLOG(ERROR) << "Failed to read from signalfd";
return;
}
if (siginfo.ssi_signo == SIGCHLD) {
HandleChildExit();
} else if (siginfo.ssi_signo == SIGTERM) {
HandleSigterm();
} else {
LOG(ERROR) << "Received unknown signal from signal fd: "
<< strsignal(siginfo.ssi_signo);
}
}
void Service::HandleChildExit() {
// We can't just rely on the information in the siginfo structure because
// more than one child may have exited but only one SIGCHLD will be
// generated.
while (true) {
int status;
pid_t pid = waitpid(-1, &status, WNOHANG);
if (pid <= 0) {
if (pid == -1 && errno != ECHILD) {
PLOG(ERROR) << "Unable to reap child processes";
}
break;
}
if (WIFEXITED(status)) {
LOG(INFO) << "Process " << pid << " exited with status "
<< WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
LOG(INFO) << "Process " << pid << " killed by signal " << WTERMSIG(status)
<< (WCOREDUMP(status) ? " (core dumped)" : "");
} else {
LOG(WARNING) << "Unknown exit status " << status << " for process "
<< pid;
}
// See if this is a process we launched.
for (const auto& pair : servers_) {
if (pid == pair.second.pid()) {
servers_.erase(pair.first);
break;
}
}
}
}
void Service::HandleSigterm() {
LOG(INFO) << "Shutting down due to SIGTERM";
// Close our connection to the bus.
bus_->ShutdownAndBlock();
// Stop the message loop.
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, quit_closure_);
}
// Handles a request to start a new 9p server.
std::unique_ptr<dbus::Response> Service::StartServer(
dbus::MethodCall* method_call) {
LOG(INFO) << "Received request to start new 9p server";
std::unique_ptr<dbus::Response> dbus_response(
dbus::Response::FromMethodCall(method_call));
dbus::MessageReader reader(method_call);
dbus::MessageWriter writer(dbus_response.get());
StartServerRequest request;
StartServerResponse response;
if (!reader.PopArrayOfBytesAsProto(&request)) {
LOG(ERROR) << "Unable to parse StartServerRequest from message";
response.set_failure_reason("Unable to parse protobuf");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
base::ScopedTempDir root_dir;
if (!root_dir.CreateUniqueTempDirUnderPath(base::FilePath(kRuntimeDir))) {
LOG(ERROR) << "Unable to create working dir for server";
response.set_failure_reason("Unable to create working dir for server");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Make sure the child process has permission to read the contents.
if (chmod(root_dir.GetPath().value().c_str(), 0755) != 0) {
PLOG(ERROR) << "Failed to change permissions for "
<< root_dir.GetPath().value();
response.set_failure_reason(
"Failed to change permissions for server's working dir");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Create the directory that the server will serve to clients. Offset the
// root path by 1 because Append wants relative paths.
base::FilePath client_root = root_dir.GetPath().Append(&kServerRoot[1]);
if (mkdir(client_root.value().c_str(), 0755) != 0) {
PLOG(ERROR) << "Unable to create server root dir";
response.set_failure_reason("Unable to create server root dir");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Get the listening address and any extra command line options.
std::vector<string> args = {kServerPath, "-r", kServerRoot};
for (const auto& idmap : request.uid_maps()) {
args.emplace_back("--uid_map");
args.emplace_back(
base::StringPrintf("%u:%u", idmap.server(), idmap.client()));
}
for (const auto& idmap : request.gid_maps()) {
args.emplace_back("--gid_map");
args.emplace_back(
base::StringPrintf("%u:%u", idmap.server(), idmap.client()));
}
base::ScopedFD listen_fd;
bool valid_address = false;
switch (request.listen_address_case()) {
case StartServerRequest::kVsock: {
const VsockAddress& addr = request.vsock();
if (addr.accept_cid() < 3) {
LOG(ERROR) << "Missing or invalid accept_cid field in vsock address: "
<< addr.accept_cid();
break;
}
args.emplace_back("--accept_cid");
args.emplace_back(std::to_string(addr.accept_cid()));
args.emplace_back(string("vsock:") + std::to_string(addr.port()));
valid_address = true;
break;
}
case StartServerRequest::kFd: {
if (!reader.PopFileDescriptor(&listen_fd)) {
LOG(ERROR) << "No fd found in incoming message";
break;
}
// Clear close-on-exec as this FD needs to be passed to 9s.
int flags = fcntl(listen_fd.get(), F_GETFD);
if (flags == -1) {
PLOG(ERROR) << "Failed to get flags for passed fd";
break;
}
if (fcntl(listen_fd.get(), F_SETFD, flags & ~FD_CLOEXEC) == -1) {
PLOG(ERROR) << "Failed to clear close-on-exec flag for fd";
break;
}
args.emplace_back(base::StringPrintf("unix-fd:%d", listen_fd.get()));
valid_address = true;
break;
}
case StartServerRequest::kUnixAddr:
case StartServerRequest::kNet:
LOG(ERROR) << "Listen address not implemented: "
<< request.listen_address_case();
break;
case StartServerRequest::LISTEN_ADDRESS_NOT_SET:
LOG(ERROR) << "Listen address not set";
break;
default:
LOG(ERROR) << "Unknown listen address: " << request.listen_address_case();
break;
}
if (!valid_address) {
LOG(ERROR) << "Unable to create listening address";
response.set_failure_reason("Unable to create listening address");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
std::vector<const char*> argv(args.size());
std::transform(args.begin(), args.end(), argv.begin(),
[](const string& arg) -> const char* { return arg.c_str(); });
argv.emplace_back(nullptr);
ScopedMinijail jail(minijail_new());
if (!jail) {
LOG(ERROR) << "Unable to create minijail";
response.set_failure_reason("Unable to create minijail");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Set up a new mount namespace but allow bind mounts from the parent
// namespace to propagate into the server's namespace.
minijail_namespace_vfs(jail.get());
minijail_remount_mode(jail.get(), MS_SLAVE);
// Since we are going to be in a user namespace all bind mounts have to use
// MS_REC.
constexpr struct {
const char* src;
bool writable;
} bind_mounts[] = {
{
.src = "/proc",
.writable = false,
},
{
.src = "/dev/null",
.writable = true,
},
{
.src = "/dev/log",
.writable = true,
},
};
for (const auto& bind_mount : bind_mounts) {
int flags = MS_BIND | MS_REC;
if (!bind_mount.writable) {
flags |= MS_RDONLY;
}
int ret = minijail_mount(jail.get(), bind_mount.src, bind_mount.src, "bind",
flags);
if (ret < 0) {
LOG(ERROR) << "Failed to bind mount " << bind_mount.src << ": "
<< strerror(-ret);
response.set_failure_reason("Unable to set up server jail");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
}
// Add android-everybody for access to android files.
minijail_set_supplementary_gids(jail.get(), base::size(kSupplementaryGroups),
kSupplementaryGroups);
minijail_change_uid(jail.get(), kChronosUid);
minijail_change_gid(jail.get(), kChronosGid);
// The process can only see what is in its root directory.
int ret =
minijail_enter_pivot_root(jail.get(), root_dir.GetPath().value().c_str());
if (ret < 0) {
LOG(ERROR) << "Unable to configure pivot_root: " << strerror(-ret);
response.set_failure_reason("Unable to configure pivot_root");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// We will manage this process's lifetime.
minijail_run_as_init(jail.get());
// It doesn't need any caps or any new privileges.
minijail_use_caps(jail.get(), 0);
minijail_no_new_privs(jail.get());
// Use a seccomp filter.
minijail_log_seccomp_filter_failures(jail.get());
minijail_parse_seccomp_filters(jail.get(), kSeccompPolicyPath);
minijail_use_seccomp_filter(jail.get());
// The server tends to open more fds than a regular program.
ret =
minijail_rlimit(jail.get(), RLIMIT_NOFILE, kMaxOpenFiles, kMaxOpenFiles);
if (ret < 0) {
LOG(ERROR) << "Unable to configure rlimit: " << strerror(-ret);
response.set_failure_reason("Unable to configure minijail");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Reset the signal mask since we block SIGCHLD and SIGTERM in this process
// for signalfd.
minijail_reset_signal_mask(jail.get());
minijail_reset_signal_handlers(jail.get());
// Launch the server.
pid_t child_pid = 0;
ret = minijail_run_pid(jail.get(), kServerPath,
const_cast<char* const*>(argv.data()), &child_pid);
if (ret < 0) {
LOG(ERROR) << "Unable to spawn server process: " << strerror(-ret);
response.set_failure_reason("Unable to spawn server");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// We're done.
LOG(INFO) << "Started server on " << root_dir.GetPath().value();
uint32_t handle = next_server_handle_++;
servers_.emplace(handle, ServerInfo(child_pid, root_dir.Take()));
response.set_success(true);
response.set_handle(handle);
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Handles a request to stop a running 9p server.
std::unique_ptr<dbus::Response> Service::StopServer(
dbus::MethodCall* method_call) {
LOG(INFO) << "Received request to stop server";
std::unique_ptr<dbus::Response> dbus_response(
dbus::Response::FromMethodCall(method_call));
dbus::MessageReader reader(method_call);
dbus::MessageWriter writer(dbus_response.get());
StopServerRequest request;
StopServerResponse response;
if (!reader.PopArrayOfBytesAsProto(&request)) {
LOG(ERROR) << "Unable to parse StopServerRequest from message";
response.set_failure_reason("Unable to parse protobuf");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
const auto& iter = servers_.find(request.handle());
if (iter == servers_.end()) {
// The server is gone. Nothing left to do here.
response.set_success(true);
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Otherwise we send the process a SIGTERM and report success while lazily
// ensuring the server will exit. This works because we don't reuse handles
// (unless we somehow spawn ~4 billion servers in ~2 seconds).
if (kill(iter->second.pid(), SIGTERM) != 0 && errno != ESRCH) {
PLOG(ERROR) << "Unable to send SIGTERM to child process";
response.set_failure_reason("Unable to send signal to child process");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::Bind(&Service::KillServer, weak_factory_.GetWeakPtr(),
request.handle()),
kServerExitTimeout);
response.set_success(true);
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Handles a request to share a path with a running server.
std::unique_ptr<dbus::Response> Service::SharePath(
dbus::MethodCall* method_call) {
LOG(INFO) << "Received request to share path with server";
std::unique_ptr<dbus::Response> dbus_response(
dbus::Response::FromMethodCall(method_call));
dbus::MessageReader reader(method_call);
dbus::MessageWriter writer(dbus_response.get());
SharePathRequest request;
SharePathResponse response;
if (!reader.PopArrayOfBytesAsProto(&request)) {
LOG(ERROR) << "Unable to parse SharePathRequest from message";
response.set_failure_reason("Unable to parse protobuf");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
const auto& iter = servers_.find(request.handle());
if (iter == servers_.end()) {
LOG(ERROR) << "Requested server does not exist";
response.set_failure_reason("Requested server does not exist");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Validate path.
base::FilePath path(request.shared_path().path());
if (path.IsAbsolute() || path.ReferencesParent() ||
path.BaseName().value() == ".") {
LOG(ERROR) << "Requested path references parent, is absolute, or ends "
<< "with ./";
response.set_failure_reason(
"Path must be relative and cannot reference parent components nor end "
"with \".\"");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Validate owner_id.
base::FilePath owner_id(request.owner_id());
bool owner_id_required =
request.storage_location() == SharePathRequest::DOWNLOADS ||
request.storage_location() == SharePathRequest::MY_FILES ||
request.storage_location() == SharePathRequest::LINUX_FILES;
if (owner_id.ReferencesParent() || owner_id.BaseName() != owner_id ||
(owner_id_required && owner_id.value().size() == 0)) {
LOG(ERROR) << "owner_id references parent, or is "
"more than 1 component, or is required and not populated";
response.set_failure_reason("owner_id must be a single valid component");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Validate drivefs_mount_name.
base::FilePath drivefs_mount_name(request.drivefs_mount_name());
bool drivefs_mount_name_required =
request.storage_location() == SharePathRequest::DRIVEFS_MY_DRIVE ||
request.storage_location() == SharePathRequest::DRIVEFS_TEAM_DRIVES ||
request.storage_location() == SharePathRequest::DRIVEFS_COMPUTERS;
if (drivefs_mount_name.ReferencesParent() ||
drivefs_mount_name.BaseName() != drivefs_mount_name ||
(drivefs_mount_name_required &&
!base::StartsWith(drivefs_mount_name.value(), "drivefs-",
base::CompareCase::SENSITIVE))) {
LOG(ERROR) << "drivefs_mount_name references parent, or is "
"more than 1 component, or is required and not populated";
response.set_failure_reason(
"drivefs_mount_name must be a single valid component");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Validate smbfs_mount_name and set smbfs_dst_prefix.
base::FilePath smbfs_mount_name(request.smbfs_mount_name());
std::string smbfs_dst_prefix;
if (request.storage_location() == SharePathRequest::SMBFS) {
if (smbfs_mount_name.ReferencesParent() ||
smbfs_mount_name.BaseName() != smbfs_mount_name ||
!base::StartsWith(smbfs_mount_name.value(), kSmbFsMountNamePrefix,
base::CompareCase::SENSITIVE)) {
LOG(ERROR) << "smbfs_mount_name references parent, or is more than 1 "
"component, or is not populated";
response.set_failure_reason(
"smbfs_mount_name must be a single valid component");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Paths within SMB shares are all mounted within a parent directory
// that is named based on the share ID itself.
smbfs_dst_prefix = smbfs_mount_name.value().substr(
std::string(kSmbFsMountNamePrefix).size());
}
// Build the source and destination directories.
base::FilePath src;
base::FilePath dst =
iter->second.root_dir().GetPath().Append(&kServerRoot[1]);
// Used later to strip out the prefix from the destination so that we return
// the relative path to the shared target.
const size_t prefix_len = dst.value().size() + 1;
switch (request.storage_location()) {
case SharePathRequest::DOWNLOADS:
src = base::FilePath("/home/user/").Append(owner_id).Append("Downloads");
dst = dst.Append("MyFiles").Append("Downloads");
break;
case SharePathRequest::DRIVEFS_MY_DRIVE:
src = base::FilePath("/media/fuse/")
.Append(drivefs_mount_name)
.Append("root");
dst = dst.Append("GoogleDrive").Append("MyDrive");
break;
case SharePathRequest::DRIVEFS_TEAM_DRIVES:
src = base::FilePath("/media/fuse/")
.Append(drivefs_mount_name)
.Append("team_drives");
dst = dst.Append("GoogleDrive").Append("SharedDrives");
break;
case SharePathRequest::DRIVEFS_COMPUTERS:
src = base::FilePath("/media/fuse/")
.Append(drivefs_mount_name)
.Append("Computers");
dst = dst.Append("GoogleDrive").Append("Computers");
break;
// Note: DriveFs .Trash directory must not ever be shared since it would
// allow linux apps to make permanent deletes to Drive.
case SharePathRequest::REMOVABLE:
src = base::FilePath("/media/removable");
dst = dst.Append("removable");
break;
case SharePathRequest::MY_FILES:
src = base::FilePath("/home/user/").Append(owner_id).Append("MyFiles");
dst = dst.Append("MyFiles");
break;
case SharePathRequest::PLAY_FILES:
src = base::FilePath("/run/arc/sdcard/write/emulated/0");
dst = dst.Append("PlayFiles");
break;
case SharePathRequest::LINUX_FILES:
src = base::FilePath("/media/fuse/")
.Append(base::JoinString(
{"crostini", owner_id.value(), "termina", "penguin"}, "_"));
dst = dst.Append("LinuxFiles");
break;
case SharePathRequest::FONTS:
src = base::FilePath("/usr/share/fonts");
dst = dst.Append("fonts");
break;
case SharePathRequest::ARCHIVE:
src = base::FilePath("/media/archive");
dst = dst.Append("archive");
break;
case SharePathRequest::SMBFS:
src = base::FilePath("/media/fuse").Append(smbfs_mount_name);
dst = dst.Append("SMB").Append(smbfs_dst_prefix);
break;
default:
LOG(ERROR) << "Unknown storage location: " << request.storage_location();
response.set_failure_reason("Unknown storage location");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Get the remaining path.
src = src.Append(path);
if (!base::PathExists(src)) {
LOG(ERROR) << "Requested path does not exist";
response.set_failure_reason("Requested path does not exist");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
base::ScopedFD src_fd(brillo::OpenSafely(src, O_RDONLY | O_CLOEXEC, 0600));
if (!src_fd.is_valid()) {
LOG(ERROR) << "Requested path may contain symlinks or point to a "
<< "non-regular file or directory";
response.set_failure_reason(
"Requested path may contain symlinks or point to a non-regular "
"file/directory");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
dst = dst.Append(path);
// The destination directory may already exist either because one of its
// children was shared and it was automatically created or one of its parents
// was shared and it's already visible.
if (!base::PathExists(dst)) {
// First create everything up to the basename.
if (!MkdirRecursively(dst.DirName())) {
response.set_failure_reason(
"Failed to create parent directory for destination");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Then create a file or directory, as necessary.
struct stat info;
if (fstat(src_fd.get(), &info) != 0) {
PLOG(ERROR) << "Unable to stat source path";
response.set_failure_reason("Unable to stat source path");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
if (S_ISDIR(info.st_mode)) {
if (mkdir(dst.value().c_str(), 0700) != 0 && errno != EEXIST) {
PLOG(ERROR) << "Unable to create destination directory";
response.set_failure_reason("Unable to create destination directory");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
} else {
base::ScopedFD file(open(dst.value().c_str(),
O_WRONLY | O_CREAT | O_CLOEXEC | O_NONBLOCK,
0600));
if (!file.is_valid()) {
PLOG(ERROR) << "Unable to create destination file";
response.set_failure_reason("Unable to create destination file");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
}
}
// Do the mount.
unsigned long flags = MS_BIND | MS_REC; // NOLINT(runtime/int)
string proc_path = base::StringPrintf("/proc/self/fd/%d", src_fd.get());
const char* source = proc_path.c_str();
const char* target = dst.value().c_str();
if (mount(source, target, "none", flags, nullptr) != 0) {
PLOG(ERROR) << "Unable to create bind mount";
response.set_failure_reason("Unable to create bind mount");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Left out because we do not currently have permissions to change the flags
// of a mount, even if it reduces privilege. Thanks, Torvalds.
// We cannot specify `MS_BIND` and `MS_RDONLY` in the same mount call so
// we have remount the path to make it read-only.
// if (!request.shared_path().writable()) {
// flags |= MS_REMOUNT | MS_RDONLY;
// if (mount(source, target, "none", flags, nullptr) != 0) {
// PLOG(ERROR) << "Unable to remount read-only";
// // Unmount the target so that we don't leak it in a writable state.
// // There's not a lot we can do in case of failure here.
// umount2(target, MNT_DETACH);
// // TODO: also delete the path
// response.set_failure_reason("Unable to remount read-only");
// writer.AppendProtoAsArrayOfBytes(response);
// return dbus_response;
// }
// }
response.set_success(true);
response.set_path(dst.value().substr(prefix_len));
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Handles a request to unshare a path with a running server.
std::unique_ptr<dbus::Response> Service::UnsharePath(
dbus::MethodCall* method_call) {
LOG(INFO) << "Received request to unshare path with server";
std::unique_ptr<dbus::Response> dbus_response(
dbus::Response::FromMethodCall(method_call));
dbus::MessageReader reader(method_call);
dbus::MessageWriter writer(dbus_response.get());
UnsharePathRequest request;
UnsharePathResponse response;
if (!reader.PopArrayOfBytesAsProto(&request)) {
LOG(ERROR) << "Unable to parse UnsharePathRequest from message";
response.set_failure_reason("Unable to parse protobuf");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
const auto& iter = servers_.find(request.handle());
if (iter == servers_.end()) {
LOG(ERROR) << "Requested server does not exist";
response.set_failure_reason("Requested server does not exist");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Validate path.
base::FilePath path(request.path());
if (path.empty() || path.IsAbsolute() || path.ReferencesParent() ||
path.BaseName().value() == ".") {
LOG(ERROR) << "Requested path is empty, references parent, is absolute, or "
"ends with ./";
response.set_failure_reason(
"Path must be non-empty, relative, cannot reference parent components, "
"nor end with \".\"");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
base::FilePath server_root =
iter->second.root_dir().GetPath().Append(&kServerRoot[1]);
base::FilePath dst = server_root.Append(path);
base::FilePath my_files = server_root.Append("MyFiles");
base::FilePath my_files_downloads = my_files.Append("Downloads");
// There is a race when unmounting a volume with shares (crbug.com/1132707)
// and |dst| may not exist. It is also expected (crbug.com/1133621) that |dst|
// will not exist when removing a share from settings when the volume is not
// mounted. We will log such cases, but continue and remove any mounts and
// clean up empty mount points.
if (!base::PathExists(dst)) {
LOG(WARNING) << "Unshare path does not exist";
}
// After unmounting, clean up empty directories. Assume at first that we can
// delete the topmost directory under server_root, but validate / modify this
// path to ensure it does not contain any other mount points. E.g. if
// dst=<server_root>/MyFiles/a/b1/c/d, then assume we can delete
// <server_root>/MyFiles, but if another mount exists at or under
// <server_root>/MyFiles/a/b2, then we only delete from
// <server_root>/MyFiles/a/b1.
base::FilePath path_to_delete = server_root;
std::vector<std::string> server_root_components;
server_root.GetComponents(&server_root_components);
size_t path_to_delete_depth = server_root_components.size();
std::vector<std::string> dst_components;
dst.GetComponents(&dst_components);
// Ensure path is listed in /proc/self/mounts and has no parents within
// server_root.
bool path_is_mount = false;
bool path_has_parent_mount = false;
base::ScopedFILE mountinfo(fopen("/proc/self/mounts", "r"));
if (!mountinfo) {
LOG(ERROR) << "Failed to open /proc/self/mounts";
response.set_failure_reason("Failed to open /proc/self/mounts");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// List of paths to be unmounted includes path and any children.
std::vector<base::FilePath> mount_points;
char buf[1024 + 4];
struct mntent entry;
while (getmntent_r(mountinfo.get(), &entry, buf, sizeof(buf)) != nullptr) {
base::FilePath mount_point(entry.mnt_dir);
if (mount_point == dst) {
// Mount is dst. This is expected/required that one entry will match.
path_is_mount = true;
mount_points.emplace_back(mount_point);
} else if (dst.IsParent(mount_point)) {
// Mount is a child of dst. This is OK, we will unmount it before
// unmounting dst.
mount_points.emplace_back(mount_point);
} else if (server_root.IsParent(mount_point) && mount_point.IsParent(dst)) {
// Mount is a parent of dst. This is an error condition and we will soon
// fail.
path_has_parent_mount = true;
} else {
// Modify path_to_delete if required so it does not contain mount_point.
std::vector<std::string> mount_point_components;
mount_point.GetComponents(&mount_point_components);
for (size_t i = 0;
i < dst_components.size() - 1 && i < mount_point_components.size() &&
dst_components[i] == mount_point_components[i];
++i) {
if (i == path_to_delete_depth) {
path_to_delete =
path_to_delete.Append(dst_components[path_to_delete_depth++]);
}
}
}
}
// Set path_to_delete to have 1 more component past server_root or any path
// common with another mount.
path_to_delete =
path_to_delete.Append(dst_components[path_to_delete_depth++]);
if (!path_is_mount) {
LOG(ERROR) << "Path is not a mount point";
response.set_failure_reason("Path is not a mount point");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
if (path_has_parent_mount) {
LOG(ERROR) << "Path has a parent mount point";
response.set_failure_reason("Path has a parent mount point");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// In reverse order, unmount paths.
for (auto iter = mount_points.rbegin(), end = mount_points.rend();
iter != end; ++iter) {
if (umount(iter->value().c_str()) != 0) {
// When MyFiles is shared, its MyFiles/Downloads mount propagates. It
// seems that the kernel does not allow us to unmount MyFiles/Downloads
// with EINVAL, and then also fails to unmount MyFiles with EBUSY even
// when no files are open.
if (errno == EINVAL && dst == my_files &&
iter->value() == my_files_downloads.value()) {
// Ignore EINVAL when unsharing MyFiles and MyFiles/Downloads fails.
PLOG(WARNING)
<< "Unmount MyFiles/Downloads failed with EINVAL, ignoring";
continue;
} else if (errno == EBUSY && iter->value() == my_files.value()) {
// If/when unmount MyFiles fails with EBUSY, we retry with MNT_DETACH.
PLOG(WARNING)
<< "Unmount MyFiles failed with EBUSY, attempting MNT_DETACH";
if (umount2(iter->value().c_str(), MNT_DETACH) == 0) {
continue;
}
}
PLOG(ERROR) << "Failed to unmount";
response.set_failure_reason("Failed to unmount");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
}
// Remove path_to_delete. Recursive is required to delete any children mount
// dirs that were created prior to this path being mounted, and any empty
// directories that were created for this mount. Recursive delete is safe
// since no mounts exist under this directory.
if (!base::DeletePathRecursively(path_to_delete)) {
LOG(ERROR) << "Delete path failed";
response.set_failure_reason("Delete path failed");
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
response.set_success(true);
writer.AppendProtoAsArrayOfBytes(response);
return dbus_response;
}
// Forcibly kills a server if it hasn't already exited.
void Service::KillServer(uint32_t handle) {
const auto& iter = servers_.find(handle);
if (iter != servers_.end()) {
// Kill it with fire.
if (kill(iter->second.pid(), SIGKILL) != 0) {
PLOG(ERROR) << "Unable to send SIGKILL to child process";
}
}
// We reap the child process through the normal sigchld handling mechanism.
}
} // namespace seneschal
} // namespace vm_tools