| // 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 |