blob: 00325339d03f0b5669a5f271116c6515d7ee1dec [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/concierge/plugin_vm.h"
#include <signal.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <algorithm>
#include <sstream>
#include <utility>
#include <vector>
#include <base/callback_helpers.h>
#include <base/check.h>
#include <base/files/file.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/logging.h>
#include <base/memory/ptr_util.h>
#include <base/notreached.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include "vm_tools/concierge/plugin_vm_helper.h"
#include "vm_tools/concierge/tap_device_builder.h"
#include "vm_tools/concierge/vm_builder.h"
#include "vm_tools/concierge/vm_permission_interface.h"
#include "vm_tools/concierge/vm_util.h"
#include "vm_tools/concierge/vmplugin_dispatcher_interface.h"
using std::string;
namespace vm_tools {
namespace concierge {
namespace {
// Path to the plugin binaries and other assets.
constexpr char kPluginDir[] = "/opt/pita";
constexpr char kPluginDlcDir[] = "/run/imageloader/pita/package/root/opt/pita";
// Name of the plugin VM binary.
constexpr char kPluginBin[] = "prl_vm_app";
// Name of the sub-directory containing plugin's seccomp policy.
constexpr char kPluginPolicyDir[] = "policy";
// Name of the runtime directory inside the jail.
constexpr char kRuntimeDir[] = "/run/pvm";
// Name of the stateful directory inside the jail.
constexpr char kStatefulDir[] = "/pvm";
// Name of the directory holding ISOs inside the jail.
constexpr char kIsoDir[] = "/iso";
// How long we give VM to suspend.
constexpr base::TimeDelta kVmSuspendTimeout = base::TimeDelta::FromSeconds(25);
// How long to wait before timing out on child process exits.
constexpr base::TimeDelta kChildExitTimeout = base::TimeDelta::FromSeconds(10);
// The CPU cgroup where all the PluginVm crosvm processes should belong to.
constexpr char kPluginVmCpuCgroup[] = "/sys/fs/cgroup/cpu/vms/plugin";
// Offset in a subnet of the client/guest.
constexpr size_t kGuestAddressOffset = 1;
std::unique_ptr<patchpanel::Subnet> MakeSubnet(
const patchpanel::IPv4Subnet& subnet) {
return std::make_unique<patchpanel::Subnet>(
subnet.base_addr(), subnet.prefix_len(), base::DoNothing());
}
void TrySuspendVm(scoped_refptr<dbus::Bus> bus,
dbus::ObjectProxy* vmplugin_service_proxy,
const VmId& id) {
bool dispatcher_shutting_down = false;
base::TimeTicks suspend_start_time(base::TimeTicks::Now());
do {
pvm::dispatcher::VmOpResult result =
pvm::dispatcher::SuspendVm(bus, vmplugin_service_proxy, id);
switch (result) {
case pvm::dispatcher::VmOpResult::SUCCESS:
return;
case pvm::dispatcher::VmOpResult::DISPATCHER_SHUTTING_DOWN:
// The dispatcher is in process of shutting down, and is supposed
// to suspend all running VMs. Wait a second, and try again, until we
// get "dispatcher not available" response.
if (!dispatcher_shutting_down) {
LOG(INFO) << "Dispatcher is shutting down, will retry suspend";
dispatcher_shutting_down = true;
}
base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
break;
case pvm::dispatcher::VmOpResult::DISPATCHER_NOT_AVAILABLE:
if (!dispatcher_shutting_down)
LOG(ERROR) << "Failed to suspend VM: dispatcher is not available";
return;
default:
// TODO(dtor): handle cases when suspend fails because VM is already
// transitioning into some state, such as suspending or shutting down,
// in which case dispatcher will reject our request.
LOG(ERROR) << "Failed to suspend VM: " << static_cast<int>(result);
return;
}
} while (base::TimeTicks::Now() - suspend_start_time < kVmSuspendTimeout);
}
} // namespace
// static
std::unique_ptr<PluginVm> PluginVm::Create(
VmId id,
base::FilePath stateful_dir,
base::FilePath iso_dir,
base::FilePath root_dir,
base::FilePath runtime_dir,
std::unique_ptr<patchpanel::Client> network_client,
int subnet_index,
bool enable_vnet_hdr,
scoped_refptr<dbus::Bus> bus,
std::unique_ptr<SeneschalServerProxy> seneschal_server_proxy,
dbus::ObjectProxy* vm_permission_service_proxy,
dbus::ObjectProxy* vmplugin_service_proxy,
VmBuilder vm_builder) {
auto vm = std::unique_ptr<PluginVm>(new PluginVm(
std::move(id), std::move(network_client), std::move(bus),
std::move(seneschal_server_proxy), vm_permission_service_proxy,
vmplugin_service_proxy, std::move(iso_dir), std::move(root_dir),
std::move(runtime_dir)));
if (!vm->CreateUsbListeningSocket() ||
!vm->Start(std::move(stateful_dir), subnet_index, enable_vnet_hdr,
std::move(vm_builder))) {
vm.reset();
}
return vm;
}
PluginVm::~PluginVm() {
StopVm();
}
bool PluginVm::StopVm() {
// Notify arc-patchpanel that the VM is down.
// This should run before the process existence check below since we still
// want to release the network resources on crash.
// Note the client will only be null during testing.
if (network_client_ && !network_client_->NotifyPluginVmShutdown(id_hash_)) {
LOG(WARNING) << "Unable to notify networking services";
}
// Notify permission service of VM destruction.
if (!permission_token_.empty()) {
vm_permission::UnregisterVm(bus_, vm_permission_service_proxy_, id_);
}
// Do a check here to make sure the process is still around.
if (!CheckProcessExists(process_.pid())) {
// The process is already gone.
process_.Release();
return true;
}
TrySuspendVm(bus_, vmplugin_service_proxy_, id_);
if (!CheckProcessExists(process_.pid())) {
process_.Release();
return true;
}
// Kill the process with SIGTERM. For Plugin VMs this will cause plugin to
// try to suspend the VM.
if (process_.Kill(SIGTERM, kChildExitTimeout.InSeconds())) {
return true;
}
LOG(WARNING) << "Failed to kill plugin VM with SIGTERM";
// Kill it with fire.
if (process_.Kill(SIGKILL, kChildExitTimeout.InSeconds())) {
return true;
}
LOG(ERROR) << "Failed to kill plugin VM with SIGKILL";
return false;
}
bool PluginVm::Shutdown() {
// Notify arc-patchpanel that the VM is down.
// This should run before the process existence check below since we still
// want to release the network resources on crash.
// Note the client will only be null during testing.
if (network_client_ && !network_client_->NotifyPluginVmShutdown(id_hash_)) {
LOG(WARNING) << "Unable to notify networking services";
}
return !CheckProcessExists(process_.pid()) ||
pvm::dispatcher::ShutdownVm(bus_, vmplugin_service_proxy_, id_) ==
pvm::dispatcher::VmOpResult::SUCCESS;
}
VmInterface::Info PluginVm::GetInfo() {
VmInterface::Info info = {
.ipv4_address = subnet_->AddressAtOffset(kGuestAddressOffset),
.pid = process_.pid(),
.cid = 0,
.seneschal_server_handle = seneschal_server_handle(),
.permission_token = permission_token_,
.status = VmInterface::Status::RUNNING,
.type = VmInfo::PLUGIN_VM,
};
return info;
}
bool PluginVm::GetVmEnterpriseReportingInfo(
GetVmEnterpriseReportingInfoResponse* response) {
response->set_success(false);
response->set_failure_reason("Not implemented");
return false;
}
vm_tools::concierge::DiskImageStatus PluginVm::ResizeDisk(
uint64_t new_size, std::string* failure_reason) {
*failure_reason = "Not implemented";
return DiskImageStatus::DISK_STATUS_FAILED;
}
vm_tools::concierge::DiskImageStatus PluginVm::GetDiskResizeStatus(
std::string* failure_reason) {
*failure_reason = "Not implemented";
return DiskImageStatus::DISK_STATUS_FAILED;
}
// static
base::ScopedFD PluginVm::CreateUnixSocket(const base::FilePath& path,
int type) {
base::ScopedFD fd(socket(AF_UNIX, type, 0));
if (!fd.is_valid()) {
PLOG(ERROR) << "Failed to create AF_UNIX socket";
return base::ScopedFD();
}
struct sockaddr_un sa;
if (path.value().length() >= sizeof(sa.sun_path)) {
LOG(ERROR) << "Path is too long for a UNIX socket: " << path.value();
return base::ScopedFD();
}
memset(&sa, 0, sizeof(sa));
sa.sun_family = AF_UNIX;
// The memset above took care of NUL terminating, and we already verified
// the length before that.
memcpy(sa.sun_path, path.value().data(), path.value().length());
// Delete any old socket instances.
if (PathExists(path) && !DeleteFile(path)) {
PLOG(ERROR) << "failed to delete " << path.value();
return base::ScopedFD();
}
// Bind the socket.
if (bind(fd.get(), reinterpret_cast<const sockaddr*>(&sa), sizeof(sa)) < 0) {
PLOG(ERROR) << "failed to bind " << path.value();
return base::ScopedFD();
}
return fd;
}
// static
bool PluginVm::SetVmCpuRestriction(CpuRestrictionState cpu_restriction_state) {
return VmBaseImpl::SetVmCpuRestriction(cpu_restriction_state,
kPluginVmCpuCgroup);
}
bool PluginVm::CreateUsbListeningSocket() {
usb_listen_fd_ = CreateUnixSocket(runtime_dir_.GetPath().Append("usb.sock"),
SOCK_SEQPACKET);
if (!usb_listen_fd_.is_valid()) {
return false;
}
// Start listening for connections. We only expect one client at a time.
if (listen(usb_listen_fd_.get(), 1) < 0) {
PLOG(ERROR) << "Unable to listen for connections on USB socket";
return false;
}
usb_listen_watcher_ = base::FileDescriptorWatcher::WatchReadable(
usb_listen_fd_.get(),
base::BindRepeating(&PluginVm::OnListenFileCanReadWithoutBlocking,
base::Unretained(this)));
if (!usb_listen_watcher_) {
LOG(ERROR) << "Failed to watch USB listening socket";
return false;
}
return true;
}
bool PluginVm::AttachUsbDevice(uint8_t bus,
uint8_t addr,
uint16_t vid,
uint16_t pid,
int fd,
UsbControlResponse* response) {
base::ScopedFD dup_fd(dup(fd));
if (!dup_fd.is_valid()) {
PLOG(ERROR) << "Unable to duplicate incoming file descriptor";
return false;
}
if (usb_vm_fd_.is_valid() && usb_req_waiting_xmit_.empty()) {
usb_listen_watcher_.reset();
usb_vm_read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanReadWithoutBlocking,
base::Unretained(this)));
usb_vm_write_watcher_ = base::FileDescriptorWatcher::WatchWritable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanWriteWithoutBlocking,
base::Unretained(this)));
if (!usb_vm_read_watcher_ || !usb_vm_write_watcher_) {
LOG(ERROR) << "Failed to start watching USB VM socket";
usb_vm_read_watcher_.reset();
usb_vm_write_watcher_.reset();
return false;
}
}
UsbCtrlRequest req{};
req.type = ATTACH_DEVICE;
req.handle = ++usb_last_handle_;
req.DevInfo.bus = bus;
req.DevInfo.addr = addr;
req.DevInfo.vid = vid;
req.DevInfo.pid = pid;
usb_req_waiting_xmit_.emplace_back(std::move(req), std::move(dup_fd));
// TODO(dtor): report status only when plugin responds; requires changes to
// the VM interface API.
response->type = UsbControlResponseType::OK;
response->port = usb_last_handle_;
return true;
}
bool PluginVm::DetachUsbDevice(uint8_t port, UsbControlResponse* response) {
auto iter = std::find_if(
usb_devices_.begin(), usb_devices_.end(),
[port](const auto& info) { return std::get<2>(info) == port; });
if (iter == usb_devices_.end()) {
response->type = UsbControlResponseType::NO_SUCH_PORT;
return true;
}
if (usb_vm_fd_.is_valid() && usb_req_waiting_xmit_.empty()) {
usb_listen_watcher_.reset();
usb_vm_read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanReadWithoutBlocking,
base::Unretained(this)));
usb_vm_write_watcher_ = base::FileDescriptorWatcher::WatchWritable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanWriteWithoutBlocking,
base::Unretained(this)));
if (!usb_vm_read_watcher_ || !usb_vm_write_watcher_) {
LOG(ERROR) << "Failed to start watching USB VM socket";
usb_vm_read_watcher_.reset();
usb_vm_write_watcher_.reset();
return false;
}
}
UsbCtrlRequest req{};
req.type = DETACH_DEVICE;
req.handle = port;
usb_req_waiting_xmit_.emplace_back(req, base::ScopedFD());
// TODO(dtor): report status only when plugin responds; requires changes to
// the VM interface API.
response->type = UsbControlResponseType::OK;
response->port = port;
return true;
}
bool PluginVm::ListUsbDevice(std::vector<UsbDevice>* device) {
device->resize(usb_devices_.size());
std::transform(usb_devices_.begin(), usb_devices_.end(), device->begin(),
[](const auto& info) {
UsbDevice d{};
std::tie(d.vid, d.pid, d.port) = info;
return d;
});
return true;
}
void PluginVm::HandleUsbControlResponse() {
UsbCtrlResponse resp;
int ret = HANDLE_EINTR(read(usb_vm_fd_.get(), &resp, sizeof(resp)));
if (ret <= 0) {
// If we get 0 bytes from read() that means that the other side closed
// the connection. Disconnect the socket and wait for new connection.
// Do the same if we get any error besides EAGAIN.
if (ret == 0 || errno != EAGAIN) {
usb_vm_read_watcher_.reset();
usb_vm_write_watcher_.reset();
usb_vm_fd_.reset();
usb_listen_watcher_ = base::FileDescriptorWatcher::WatchReadable(
usb_listen_fd_.get(),
base::BindRepeating(&PluginVm::OnListenFileCanReadWithoutBlocking,
base::Unretained(this)));
if (!usb_listen_watcher_) {
LOG(ERROR) << "Failed to restart watching USB listening socket";
}
}
} else if (ret != sizeof(resp)) {
LOG(ERROR) << "Partial read of " << ret
<< " from USB VM socket, discarding";
} else {
auto req_iter = std::find_if(
usb_req_waiting_response_.begin(), usb_req_waiting_response_.end(),
[&resp](const auto& req) {
return resp.type == req.type && resp.handle == req.handle;
});
if (req_iter == usb_req_waiting_response_.end()) {
LOG(ERROR) << "Unexpected response (type " << resp.type << ", handle "
<< resp.handle << ")";
return;
}
if (resp.status != UsbCtrlResponse::Status::OK) {
LOG(ERROR) << "Request (type " << resp.type << ", handle " << resp.handle
<< ") failed: " << resp.status;
}
switch (req_iter->type) {
case ATTACH_DEVICE:
if (resp.status == UsbCtrlResponse::Status::OK) {
usb_devices_.emplace_back(req_iter->DevInfo.vid,
req_iter->DevInfo.pid, req_iter->handle);
}
break;
case DETACH_DEVICE: {
// We will attempt to clean up even if plugin signalled error as there
// is no way the device will continue be operational.
auto dev_iter = std::find_if(usb_devices_.begin(), usb_devices_.end(),
[&resp](const auto& info) {
return std::get<2>(info) == resp.handle;
});
if (dev_iter == usb_devices_.end()) {
LOG(ERROR) << "Received detach response for unknown handle "
<< resp.handle;
}
} break;
default:
NOTREACHED();
}
usb_req_waiting_response_.erase(req_iter);
}
}
void PluginVm::OnListenFileCanReadWithoutBlocking() {
int ret = HANDLE_EINTR(accept4(usb_listen_fd_.get(), nullptr, nullptr,
SOCK_CLOEXEC | SOCK_NONBLOCK));
if (ret < 0) {
PLOG(ERROR) << "Unable to accept connection on USB listening socket";
return;
}
// Start managing socket connected to the VM.
usb_vm_fd_.reset(ret);
// Switch watcher from listener FD to connected socket FD.
// We monitor both writes and reads to detect disconnects.
usb_listen_watcher_.reset();
usb_vm_read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanReadWithoutBlocking,
base::Unretained(this)));
if (!usb_req_waiting_xmit_.empty()) {
usb_vm_write_watcher_ = base::FileDescriptorWatcher::WatchWritable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanWriteWithoutBlocking,
base::Unretained(this)));
}
if (!usb_vm_read_watcher_ ||
(!usb_req_waiting_xmit_.empty() && !usb_vm_write_watcher_)) {
LOG(ERROR) << "Failed to start watching USB VM socket";
usb_vm_write_watcher_.reset();
usb_vm_read_watcher_.reset();
usb_vm_fd_.reset();
return;
}
}
void PluginVm::OnVmFileCanReadWithoutBlocking() {
PluginVm::HandleUsbControlResponse();
}
void PluginVm::OnVmFileCanWriteWithoutBlocking() {
DCHECK(usb_vm_fd_.is_valid());
if (!usb_req_waiting_xmit_.empty()) {
UsbCtrlRequest req = usb_req_waiting_xmit_.front().first;
struct iovec io {};
io.iov_base = &req;
io.iov_len = sizeof(req);
struct msghdr msg {};
msg.msg_iov = &io;
msg.msg_iovlen = 1;
char buf[CMSG_SPACE(sizeof(int))];
base::ScopedFD fd(std::move(usb_req_waiting_xmit_.front().second));
if (fd.is_valid()) {
msg.msg_control = buf;
msg.msg_controllen = CMSG_LEN(sizeof(int));
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
*(reinterpret_cast<int*> CMSG_DATA(cmsg)) = fd.get();
}
int ret = HANDLE_EINTR(sendmsg(usb_vm_fd_.get(), &msg, MSG_EOR));
if (ret == sizeof(req)) {
usb_req_waiting_response_.emplace_back(req);
usb_req_waiting_xmit_.pop_front();
} else if (ret >= 0) {
PLOG(ERROR) << "Partial write of " << ret
<< " while sending USB request; will retry";
} else if (errno != EINTR) {
PLOG(ERROR) << "Failed to send USB request";
}
} else {
usb_listen_watcher_.reset();
usb_vm_write_watcher_.reset();
usb_vm_read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
usb_vm_fd_.get(),
base::BindRepeating(&PluginVm::OnVmFileCanReadWithoutBlocking,
base::Unretained(this)));
if (!usb_vm_read_watcher_) {
LOG(ERROR) << "Failed to switch to watching USB VM socket for reads";
}
}
}
// static
bool PluginVm::WriteResolvConf(const base::FilePath& parent_dir,
const std::vector<string>& nameservers,
const std::vector<string>& search_domains) {
// Create temporary directory on the same file system so that we
// can atomically replace old resolv.conf with new one.
base::ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDirUnderPath(parent_dir)) {
LOG(ERROR) << "Failed to create temporary directory under "
<< parent_dir.value();
return false;
}
base::FilePath path = temp_dir.GetPath().Append("resolv.conf");
base::File file(path, base::File::FLAG_CREATE | base::File::FLAG_WRITE);
if (!file.IsValid()) {
LOG(ERROR) << "Failed to create temporary file " << path.value();
return false;
}
for (auto& ns : nameservers) {
string nameserver_line = base::StringPrintf("nameserver %s\n", ns.c_str());
if (!file.WriteAtCurrentPos(nameserver_line.c_str(),
nameserver_line.length())) {
LOG(ERROR) << "Failed to write nameserver to temporary file";
return false;
}
}
if (!search_domains.empty()) {
string search_domains_line = base::StringPrintf(
"search %s\n", base::JoinString(search_domains, " ").c_str());
if (!file.WriteAtCurrentPos(search_domains_line.c_str(),
search_domains_line.length())) {
LOG(ERROR) << "Failed to write search domains to temporary file";
return false;
}
}
constexpr char kResolvConfOptions[] =
"options single-request timeout:1 attempts:5\n";
if (!file.WriteAtCurrentPos(kResolvConfOptions, strlen(kResolvConfOptions))) {
LOG(ERROR) << "Failed to write search resolver options to temporary file";
return false;
}
// This should flush the buffers.
file.Close();
base::File::Error err;
if (!ReplaceFile(path, parent_dir.Append("resolv.conf"), &err)) {
LOG(ERROR) << "Failed to replace resolv.conf with new instance: "
<< base::File::ErrorToString(err);
return false;
}
return true;
}
bool PluginVm::SetResolvConfig(const std::vector<string>& nameservers,
const std::vector<string>& search_domains) {
return WriteResolvConf(root_dir_.GetPath().Append("etc"), nameservers,
search_domains);
}
void PluginVm::VmToolsStateChanged(bool running) {
LOG(INFO) << "Tools are " << (running ? "" : "not ")
<< "running in plugin VM";
if (running)
pvm::helper::CleanUpAfterInstall(id_, iso_dir_);
}
PluginVm::PluginVm(VmId id,
std::unique_ptr<patchpanel::Client> network_client,
scoped_refptr<dbus::Bus> bus,
std::unique_ptr<SeneschalServerProxy> seneschal_server_proxy,
dbus::ObjectProxy* vm_permission_service_proxy,
dbus::ObjectProxy* vmplugin_service_proxy,
base::FilePath iso_dir,
base::FilePath root_dir,
base::FilePath runtime_dir)
: VmBaseImpl(std::move(network_client),
std::move(seneschal_server_proxy),
std::move(runtime_dir)),
id_(std::move(id)),
iso_dir_(std::move(iso_dir)),
bus_(std::move(bus)),
vm_permission_service_proxy_(vm_permission_service_proxy),
vmplugin_service_proxy_(vmplugin_service_proxy),
usb_last_handle_(0) {
CHECK(vm_permission_service_proxy_);
CHECK(vmplugin_service_proxy_);
CHECK(base::DirectoryExists(iso_dir_));
CHECK(base::DirectoryExists(root_dir));
// Take ownership of the root and runtime directories.
CHECK(root_dir_.Set(root_dir));
std::ostringstream oss;
oss << id_;
id_hash_ = std::hash<std::string>{}(oss.str());
}
bool PluginVm::Start(base::FilePath stateful_dir,
int subnet_index,
bool enable_vnet_hdr,
VmBuilder vm_builder) {
if (!pvm::helper::IsDlcVm()) {
LOG(ERROR) << "PluginVM DLC is not installed.";
return false;
}
// Register the VM with permission service and obtain permission
// token.
if (!vm_permission::RegisterVm(bus_, vm_permission_service_proxy_, id_,
vm_permission::VmType::PLUGIN_VM,
&permission_token_)) {
LOG(ERROR) << "Failed to register with permission service";
return false;
}
// Get the network interface.
patchpanel::NetworkDevice network_device;
if (!network_client_->NotifyPluginVmStartup(id_hash_, subnet_index,
&network_device)) {
LOG(ERROR) << "No network devices available";
return false;
}
subnet_ = MakeSubnet(network_device.ipv4_subnet());
// Open the tap device.
base::ScopedFD tap_fd = OpenTapDevice(
network_device.ifname(), enable_vnet_hdr, nullptr /*ifname_out*/);
if (!tap_fd.is_valid()) {
LOG(ERROR) << "Unable to open and configure TAP device "
<< network_device.ifname();
return false;
}
auto plugin_bin_path = base::FilePath(kPluginDlcDir).Append(kPluginBin);
auto plugin_policy_dir =
base::FilePath(kPluginDlcDir).Append(kPluginPolicyDir);
vm_builder.AppendTapFd(std::move(tap_fd))
.AppendCustomParam("--plugin", plugin_bin_path.value())
.AppendCustomParam("--seccomp-policy-dir", plugin_policy_dir.value())
.AppendCustomParam("--plugin-gid-map-file",
plugin_bin_path.AddExtension("gid_maps").value());
// These are bind mounts with parts may change (i.e. they are either VM
// or config specific).
std::vector<string> bind_mounts = {
base::StringPrintf("%s:%s:false", kPluginDlcDir, kPluginDir),
// This is directory where the VM image resides.
base::StringPrintf("%s:%s:true", stateful_dir.value().c_str(),
kStatefulDir),
// This is directory where ISO images for the VM reside.
base::StringPrintf("%s:%s:false", iso_dir_.value().c_str(), kIsoDir),
// This is directory where control socket, 9p socket, and other axillary
// runtime data lives.
base::StringPrintf("%s:%s:true", runtime_dir_.GetPath().value().c_str(),
kRuntimeDir),
// Plugin '/etc' directory.
base::StringPrintf("%s:%s:true",
root_dir_.GetPath().Append("etc").value().c_str(),
"/etc"),
};
if (vm_permission::IsCameraEnabled(bus_, vm_permission_service_proxy_,
permission_token_)) {
LOG(INFO) << "VM " << id_ << ": camera access enabled";
bind_mounts.push_back("/run/camera:/run/camera:true");
} else {
LOG(INFO) << "VM " << id_ << ": camera access disabled";
}
if (vm_permission::IsMicrophoneEnabled(bus_, vm_permission_service_proxy_,
permission_token_)) {
LOG(INFO) << "VM " << id_ << ": microphone access enabled";
bind_mounts.push_back("/run/cras/plugin/unified:/run/cras:true");
} else {
LOG(INFO) << "VM " << id_ << ": microphone access disabled";
bind_mounts.push_back("/run/cras/plugin/playback:/run/cras:true");
}
// TODO(kimjae): This is a temporary hack to have relative files to be found
// even when started from DLC paths. Clean this up once a cleaner solution
// can be leveraged.
bind_mounts.push_back(
base::StringPrintf("%s:%s:false", kPluginDlcDir, kPluginDlcDir));
for (auto& mount : bind_mounts)
vm_builder.AppendCustomParam("--plugin-mount", std::move(mount));
// Because some of the static paths are mounted in /run/pvm... in the
// plugin jail, they have to come after the dynamic paths above.
vm_builder.AppendCustomParam(
"--plugin-mount-file",
plugin_bin_path.AddExtension("bind_mounts").value());
// Change the process group before exec so that crosvm sending SIGKILL to
// the whole process group doesn't kill us as well. The function also
// changes the cpu cgroup for PluginVm crosvm processes.
process_.SetPreExecCallback(base::BindOnce(
&SetUpCrosvmProcess, base::FilePath(kPluginVmCpuCgroup).Append("tasks")));
if (!StartProcess(vm_builder.BuildVmArgs())) {
LOG(ERROR) << "Failed to start VM process";
return false;
}
return true;
}
} // namespace concierge
} // namespace vm_tools