blob: 75d2c4a308d2fbdce5b39d9ff0fd5512c5cc3b55 [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 <utility>
#include <vector>
#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/strings/stringprintf.h>
#include <base/time/time.h>
#include "vm_tools/concierge/tap_device_builder.h"
using std::string;
namespace vm_tools {
namespace concierge {
namespace {
// Path to the crosvm binary.
constexpr char kCrosvmBin[] = "/usr/bin/crosvm";
// Name of the plugin dispatcher runtime directory.
constexpr char kDispatcherRuntimeDir[] = "/run/pvm";
// Name of the plugin dispatcher socket.
constexpr char kDispatcherSocket[] = "vmplugin_dispatcher.socket";
// Path to the plugin binaries and other assets.
constexpr char kPluginBinDir[] = "/opt/pita/";
// Name of the plugin VM binary.
constexpr char kPluginBinName[] = "pvm";
constexpr gid_t kPluginGidMap[] = {
7, // lp
600, // cras
603, // arc-camera
};
// 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";
// How long to wait before timing out on child process exits.
constexpr base::TimeDelta kChildExitTimeout = base::TimeDelta::FromSeconds(10);
// Sets the pgid of the current process to its pid. This is needed because
// crosvm assumes that only it and its children are in the same process group
// and indiscriminately sends a SIGKILL if it needs to shut them down.
bool SetPgid() {
if (setpgid(0, 0) != 0) {
PLOG(ERROR) << "Failed to change process group id";
return false;
}
return true;
}
} // namespace
// static
std::unique_ptr<PluginVm> PluginVm::Create(
uint32_t cpus,
std::vector<string> params,
arc_networkd::MacAddress mac_addr,
std::unique_ptr<arc_networkd::SubnetAddress> ipv4_addr,
uint32_t ipv4_netmask,
uint32_t ipv4_gateway,
base::FilePath stateful_dir,
base::FilePath root_dir,
base::FilePath runtime_dir,
std::unique_ptr<SeneschalServerProxy> seneschal_server_proxy) {
auto vm = base::WrapUnique(
new PluginVm(std::move(mac_addr), std::move(ipv4_addr), ipv4_netmask,
ipv4_gateway, std::move(seneschal_server_proxy),
std::move(root_dir), std::move(runtime_dir)));
if (!vm->Start(cpus, std::move(params), std::move(stateful_dir))) {
vm.reset();
}
return vm;
}
PluginVm::~PluginVm() {
Shutdown();
}
bool PluginVm::Shutdown() {
// Do a sanity check here to make sure the process is still around. It may
// have crashed and we don't want to be waiting around for an RPC response
// that's never going to come. kill with a signal value of 0 is explicitly
// documented as a way to check for the existence of a process.
if (process_.pid() == 0 || (kill(process_.pid(), 0) < 0 && errno == ESRCH)) {
// The process is already gone.
process_.Release();
return true;
}
// Kill the process with SIGTERM.
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;
}
VmInterface::Info PluginVm::GetInfo() {
VmInterface::Info info = {
.ipv4_address = ipv4_addr_->Address(),
.pid = process_.pid(),
.cid = 0,
.seneschal_server_handle = seneschal_server_handle(),
.status = VmInterface::Status::RUNNING,
};
return info;
}
bool PluginVm::AttachUsbDevice(uint8_t bus,
uint8_t addr,
uint16_t vid,
uint16_t pid,
int fd,
UsbControlResponse* response) {
return false;
}
bool PluginVm::DetachUsbDevice(uint8_t port, UsbControlResponse* response) {
return false;
}
bool PluginVm::ListUsbDevice(std::vector<UsbDevice>* device) {
return false;
}
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);
}
PluginVm::PluginVm(arc_networkd::MacAddress mac_addr,
std::unique_ptr<arc_networkd::SubnetAddress> ipv4_addr,
uint32_t ipv4_netmask,
uint32_t ipv4_gateway,
std::unique_ptr<SeneschalServerProxy> seneschal_server_proxy,
base::FilePath root_dir,
base::FilePath runtime_dir)
: mac_addr_(std::move(mac_addr)),
ipv4_addr_(std::move(ipv4_addr)),
netmask_(ipv4_netmask),
gateway_(ipv4_gateway),
seneschal_server_proxy_(std::move(seneschal_server_proxy)) {
CHECK(ipv4_addr_);
CHECK(base::DirectoryExists(root_dir));
CHECK(base::DirectoryExists(runtime_dir));
// Take ownership of the root and runtime directories.
CHECK(root_dir_.Set(root_dir));
CHECK(runtime_dir_.Set(runtime_dir));
}
bool PluginVm::Start(uint32_t cpus,
std::vector<string> params,
base::FilePath stateful_dir) {
// Set up the tap device.
base::ScopedFD tap_fd =
BuildTapDevice(mac_addr_, gateway_, netmask_, false /*vnet_hdr*/);
if (!tap_fd.is_valid()) {
LOG(ERROR) << "Unable to build and configure TAP device";
return false;
}
// Build up the process arguments.
// clang-format off
std::vector<string> args = {
kCrosvmBin, "run",
"--cpus", std::to_string(cpus),
"--tap-fd", std::to_string(tap_fd.get()),
"--plugin", base::FilePath(kPluginBinDir)
.Append(kPluginBinName)
.value(),
};
// clang-format on
std::vector<string> bind_mounts = {
"/dev/log:/dev/log:true",
"/run/camera:/run/camera:true",
// TODO(b:117218264) replace with CUPS proxy socket directory when ready.
"/run/cups:/run/cups:true",
// TODO(b:127478233) replace with CRAS proxy socket directory when ready.
"/run/cras:/run/cras:true",
base::StringPrintf("%s:%s:false", kPluginBinDir, kPluginBinDir),
// This is directory where the VM image resides.
base::StringPrintf("%s:%s:true", stateful_dir.value().c_str(),
kStatefulDir),
// 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"),
// This is the directory where the cicerone host socket lives. The plugin
// VM also creates the guest socket for cicerone in this same directory
// using the following <token>.sock as the name. The token resides in
// the VM runtime directory with name cicerone.token.
base::StringPrintf("/run/vm_cicerone/client:%s:true",
base::FilePath(kRuntimeDir)
.Append("cicerone_socket")
.value()
.c_str()),
};
// When testing dispatcher socket might be missing, let's warn and continue.
if (!PathExists(
base::FilePath(kDispatcherRuntimeDir).Append(kDispatcherSocket))) {
LOG(WARNING) << "Plugin dispatcher socket is missing";
} else {
bind_mounts.emplace_back(base::StringPrintf(
"%s:%s:true",
base::FilePath(kDispatcherRuntimeDir)
.Append(kDispatcherSocket)
.value()
.c_str(),
base::FilePath(kRuntimeDir).Append(kDispatcherSocket).value().c_str()));
}
// Put everything into the brillo::ProcessImpl.
for (auto& arg : args) {
process_.AddArg(std::move(arg));
}
for (auto gid : kPluginGidMap) {
process_.AddArg("--plugin-gid-map");
process_.AddArg(base::StringPrintf("%u:%u:1", gid, gid));
}
for (auto& mount : bind_mounts) {
process_.AddArg("--plugin-mount");
process_.AddArg(std::move(mount));
}
for (auto& param : params) {
process_.AddArg("--params");
process_.AddArg(std::move(param));
}
// Change the process group before exec so that crosvm sending SIGKILL to the
// whole process group doesn't kill us as well.
process_.SetPreExecCallback(base::Bind(&SetPgid));
if (!process_.Start()) {
LOG(ERROR) << "Failed to start VM process";
return false;
}
return true;
}
} // namespace concierge
} // namespace vm_tools