blob: 0564b66e33055bc465a442a5af56181e98ba1e2a [file] [log] [blame] [edit]
// Copyright 2017 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "vm_tools/concierge/service.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <google/protobuf/repeated_field.h>
#include <linux/capability.h>
#include <net/route.h>
#include <signal.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <linux/vm_sockets.h> // Needs to come after sys/socket.h
#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <ranges>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
#include <base/base64url.h>
#include <base/check.h>
#include <base/check_op.h>
#include <base/containers/span.h>
#include <base/files/file.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_path_watcher.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/format_macros.h>
#include <base/functional/bind.h>
#include <base/functional/bind_internal.h>
#include <base/functional/callback.h>
#include <base/functional/callback_forward.h>
#include <base/functional/callback_helpers.h>
#include <base/hash/md5.h>
#include <base/location.h>
#include <base/logging.h>
#include <base/memory/ptr_util.h>
#include <base/memory/ref_counted.h>
#include <base/notreached.h>
#include <base/posix/eintr_wrapper.h>
#include <base/process/launch.h>
#include <base/run_loop.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/synchronization/waitable_event.h>
#include <base/system/sys_info.h>
#include <base/task/bind_post_task.h>
#include <base/task/single_thread_task_runner.h>
#include <base/task/thread_pool.h>
#include <base/time/time.h>
#include <base/uuid.h>
#include <base/version.h>
#include <blkid/blkid.h>
#include <brillo/dbus/dbus_method_response.h>
#include <brillo/files/safe_fd.h>
#include <brillo/osrelease_reader.h>
#include <brillo/process/process.h>
#include <chromeos/constants/vm_tools.h>
#include <chromeos/dbus/service_constants.h>
#include <chromeos/patchpanel/dbus/client.h>
#include <dbus/error.h>
#include <dbus/object_proxy.h>
#include <dbus/shadercached/dbus-constants.h>
#include <dbus/vm_concierge/dbus-constants.h>
#include <metrics/metrics_library.h>
#include <metrics/metrics_writer.h>
#include <spaced/dbus-proxies.h>
#include <spaced/disk_usage_proxy.h>
#include <vm_applications/apps.pb.h>
#include <vm_cicerone/cicerone_service.pb.h>
#include <vm_concierge/concierge_service.pb.h>
#include <vm_protos/proto_bindings/vm_guest.pb.h>
#include "vm_tools/common/naming.h"
#include "vm_tools/common/vm_id.h"
#include "vm_tools/concierge/arc_vm.h"
#include "vm_tools/concierge/byte_unit.h"
#include "vm_tools/concierge/dbus_adaptor.h"
#include "vm_tools/concierge/dbus_proxy_util.h"
#include "vm_tools/concierge/dlc_helper.h"
#include "vm_tools/concierge/feature_util.h"
#include "vm_tools/concierge/metrics/duration_recorder.h"
#include "vm_tools/concierge/mm/resize_priority.h"
#include "vm_tools/concierge/network/borealis_network.h"
#include "vm_tools/concierge/network/bruschetta_network.h"
#include "vm_tools/concierge/network/guest_os_network.h"
#include "vm_tools/concierge/network/termina_network.h"
#include "vm_tools/concierge/plugin_vm.h"
#include "vm_tools/concierge/plugin_vm_helper.h"
#include "vm_tools/concierge/seneschal_server_proxy.h"
#include "vm_tools/concierge/service_common.h"
#include "vm_tools/concierge/service_start_vm_helper.h"
#include "vm_tools/concierge/shadercached_helper.h"
#include "vm_tools/concierge/ssh_keys.h"
#include "vm_tools/concierge/termina_vm.h"
#include "vm_tools/concierge/thread_utils.h"
#include "vm_tools/concierge/tracing.h"
#include "vm_tools/concierge/untrusted_vm_utils.h"
#include "vm_tools/concierge/vm_base_impl.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/vm_wl_interface.h"
#include "vm_tools/concierge/vmplugin_dispatcher_interface.h"
namespace vm_tools::concierge {
namespace {
// How long we should wait for a VM to start up.
// While this timeout might be high, it's meant to be a final failure point, not
// the lower bound of how long it takes. On a loaded system (like extracting
// large compressed files), it could take 10 seconds to boot.
constexpr base::TimeDelta kVmStartupDefaultTimeout = base::Seconds(60);
// Borealis has a longer default timeout, as it can take a long time to create
// its swap file on eMMC devices.
constexpr base::TimeDelta kBorealisVmStartupDefaultTimeout = base::Seconds(180);
// crosvm log directory name.
constexpr char kCrosvmLogDir[] = "log";
// Extension for crosvm log files
constexpr char kCrosvmLogFileExt[] = "log";
// Extension for vmlog_forwarder listener sockets.
constexpr char kCrosvmLogSocketExt[] = "lsock";
// crosvm gpu cache directory name.
constexpr char kCrosvmGpuCacheDir[] = "gpucache";
// Path to system boot_id file.
constexpr char kBootIdFile[] = "/proc/sys/kernel/random/boot_id";
// Extended attribute indicating that user has picked a size for a non-sparse
// disk image and it should not be resized.
constexpr char kDiskImagePreallocatedWithUserChosenSizeXattr[] =
"user.crostini.user_chosen_size";
// File extension for raw disk types
constexpr char kRawImageExtension[] = ".img";
// File extension for qcow2 disk types
constexpr char kQcowImageExtension[] = ".qcow2";
// File extension for Plugin VMs disk types
constexpr char kPluginVmImageExtension[] = ".pvm";
// Valid file extensions for disk images
constexpr const char* kDiskImageExtensions[] = {kRawImageExtension,
kQcowImageExtension, nullptr};
// Valid file extensions for Plugin VM images
constexpr const char* kPluginVmImageExtensions[] = {kPluginVmImageExtension,
nullptr};
constexpr uint64_t kMinimumDiskSize = GiB(1);
constexpr uint64_t kDiskSizeMask = ~4095ll; // Round to disk block size.
// vmlog_forwarder relies on creating a socket for crosvm to receive log
// messages. Socket paths may only be 108 character long. Further, while Linux
// actually allows for 108 non-null bytes to be used, the rust interface to bind
// only allows for 107, with the last byte always being null.
//
// We can abbreviate the directories in the path by opening the target directory
// and using /proc/self/fd/ to access it, but this still uses up
// 21 + (fd digits) characters on the prefix and file extension. This leaves us
// with 86 - (fd digits) characters for the base64 encoding of the VM
// name. Base64 always produces encoding that are a multiple of 4 digits long,
// so we can either allow for 63/84 characters before/after encoding, or
// 60/80. The first will break if our file descriptor numbers ever go above 99,
// which seems unlikely but not impossible. We can definitely be sure they won't
// go above 99,999, however.
constexpr int kMaxVmNameLength = 60;
constexpr uint64_t kDefaultIoLimit = MiB(1);
// How often we should broadcast state of a disk operation (import or export).
constexpr base::TimeDelta kDiskOpReportInterval = base::Seconds(15);
// Path to cpu information directories
constexpr char kCpuInfosPath[] = "/sys/devices/system/cpu/";
// Path of system timezone file.
constexpr char kLocaltimePath[] = "/etc/localtime";
// Path to zone info directory in host.
constexpr char kZoneInfoPath[] = "/usr/share/zoneinfo";
// Feature name of per-boot-vm-shader-cache
constexpr char kPerBootVmShaderCacheFeatureName[] =
"CrOSLateBootVmPerBootShaderCache";
// Needs to be const as libfeatures does pointers checking.
const VariationsFeature kPerBootVmShaderCacheFeature{
kPerBootVmShaderCacheFeatureName, FEATURE_DISABLED_BY_DEFAULT};
// Feature name of borealis-vcpu-tweaks
constexpr char kBorealisVcpuTweaksFeatureName[] =
"CrOSLateBootBorealisVcpuTweaks";
// Feature name of borealis-provision.
constexpr char kBorealisProvisionFeature[] = "BorealisProvision";
// A feature name for throttling ARCVM's crosvm with cpu.cfs_quota_us.
constexpr char kArcVmInitialThrottleFeatureName[] =
"CrOSLateBootArcVmInitialThrottle";
// A parameter name for |kArcVmInitialThrottleFeatureName|. Can be 1 to 100,
// or -1 (disabled).
constexpr char kArcVmInitialThrottleFeatureQuotaParam[] = "quota";
// Needs to be const as libfeatures does pointers checking.
const VariationsFeature kArcVmInitialThrottleFeature{
kArcVmInitialThrottleFeatureName, FEATURE_DISABLED_BY_DEFAULT};
const VariationsFeature kBorealisVcpuTweaksFeature{
kBorealisVcpuTweaksFeatureName, FEATURE_DISABLED_BY_DEFAULT};
// Rational for setting bytes-per-inode to 32KiB (rather than the default 16
// KiB) in go/borealis-inode.
const uint64_t kExt4BytesPerInode = 32768;
// Opts to be used when making an ext4 image. Note: these were specifically
// selected for Borealis, please take care when using outside of Borealis
// (especially the casefold feature).
const std::vector<std::string> kExtMkfsOpts = {
"-Elazy_itable_init=0,lazy_journal_init=0,discard", "-Ocasefold",
"-i" + std::to_string(kExt4BytesPerInode)};
// A TBW limit that is unlikely to impact disk health over the lifetime of a
// given 32GB device.
constexpr int64_t kTbwTargetForVmmSwapPerDay = 550ull * 1000 * 1000;
// The reference disk size used to determine the base TBW target.
constexpr int64_t kTbwTargetForVmmSwapReferenceDiskSize =
32ull * 1000 * 1000 * 1000;
// Maximum daily TBW budget for vmm-swap - if we're writing more than this,
// then the user is using ARCVM enough that we don't want to activate vmm-swap.
constexpr int64_t kTbwMaxForVmmSwapPerDay = 2ull * 1000 * 1000 * 1000;
// The path to the history file for VmmSwapTbwPolicy.
constexpr char kVmmSwapTbwHistoryFilePath[] =
"/var/lib/vm_concierge/vmm_swap_policy/tbw_history2";
// Maximum size of logs to send through D-Bus. Must be less than the maximum
// D-Bus array length (64 MiB) and the configured maximum message size for the
// system bus (usually 32 MiB).
constexpr int64_t kMaxGetVmLogsSize = MiB(30);
std::string ConvertToFdBasedPaths(brillo::SafeFD& root_fd,
bool is_rootfs_writable,
VMImageSpec& image_spec,
std::vector<brillo::SafeFD>& owned_fds) {
std::string failure_reason;
if (image_spec.kernel.empty() && image_spec.bios.empty()) {
LOG(ERROR) << "neither a kernel nor a BIOS were provided";
failure_reason = "neither a kernel nor a BIOS were provided";
return failure_reason;
}
if (!image_spec.kernel.empty()) {
failure_reason =
ConvertToFdBasedPath(root_fd, &image_spec.kernel, O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Missing VM kernel path: " << image_spec.kernel.value();
failure_reason = "Kernel path does not exist";
return failure_reason;
}
}
if (!image_spec.bios.empty()) {
failure_reason =
ConvertToFdBasedPath(root_fd, &image_spec.bios, O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Missing VM BIOS path: " << image_spec.bios.value();
failure_reason = "BIOS path does not exist";
return failure_reason;
}
}
if (!image_spec.pflash.empty()) {
failure_reason =
ConvertToFdBasedPath(root_fd, &image_spec.pflash, O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Missing VM pflash path: " << image_spec.pflash.value();
failure_reason = "pflash path does not exist";
return failure_reason;
}
}
if (!image_spec.initrd.empty()) {
failure_reason =
ConvertToFdBasedPath(root_fd, &image_spec.initrd, O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Missing VM initrd path: " << image_spec.initrd.value();
failure_reason = "Initrd path does not exist";
return failure_reason;
}
}
if (!image_spec.rootfs.empty()) {
failure_reason =
ConvertToFdBasedPath(root_fd, &image_spec.rootfs,
is_rootfs_writable ? O_RDWR : O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Missing VM rootfs path: " << image_spec.rootfs.value();
failure_reason = "Rootfs path does not exist";
return failure_reason;
}
}
return failure_reason;
}
// Posted to a grpc thread to startup a listener service. Puts a copy of
// the pointer to the grpc server in |server_copy| and then signals |event|.
// It will listen on the address specified in |listener_address|.
void RunListenerService(grpc::Service* listener,
const std::string& listener_address,
base::WaitableEvent* event,
std::shared_ptr<grpc::Server>* server_copy) {
// Build the grpc server.
grpc::ServerBuilder builder;
builder.AddListeningPort(listener_address, grpc::InsecureServerCredentials());
builder.RegisterService(listener);
std::shared_ptr<grpc::Server> server(builder.BuildAndStart().release());
*server_copy = server;
event->Signal();
if (server) {
server->Wait();
}
}
// Sets up a gRPC listener service by starting the |grpc_thread| and posting
// the main task to run for the thread. |listener_address| should be the
// address the gRPC server is listening on. A copy of the pointer to the
// server is put in |server_copy|. Returns true if setup & started
// successfully, false otherwise.
bool SetupListenerService(grpc::Service* listener_impl,
const std::string& listener_address,
std::shared_ptr<grpc::Server>* server_copy) {
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED);
bool ret = base::ThreadPool::PostTask(
FROM_HERE, base::BindOnce(&RunListenerService, listener_impl,
listener_address, &event, server_copy));
if (!ret) {
LOG(ERROR) << "Failed to post server startup task to grpc thread";
return false;
}
// Wait for the VM grpc server to start.
event.Wait();
if (!server_copy) {
LOG(ERROR) << "grpc server failed to start";
return false;
}
return true;
}
// Gets the path to a VM disk given the name, user id, and location.
bool GetDiskPathFromName(
const VmId& vm_id,
StorageLocation storage_location,
base::FilePath* path_out,
enum DiskImageType preferred_image_type = DiskImageType::DISK_IMAGE_AUTO) {
switch (storage_location) {
case STORAGE_CRYPTOHOME_ROOT: {
const auto qcow2_path =
GetFilePathFromName(vm_id, storage_location, kQcowImageExtension);
if (!qcow2_path) {
return false;
}
const auto raw_path =
GetFilePathFromName(vm_id, storage_location, kRawImageExtension);
if (!raw_path) {
return false;
}
const bool qcow2_exists = base::PathExists(*qcow2_path);
const bool raw_exists = base::PathExists(*raw_path);
// This scenario (both <name>.img and <name>.qcow2 exist) should never
// happen. It is prevented by the later checks in this function.
// However, in case it does happen somehow (e.g. user manually created
// files in dev mode), bail out, since we can't tell which one the user
// wants.
if (qcow2_exists && raw_exists) {
LOG(ERROR) << "Both qcow2 and raw variants of " << vm_id.name()
<< " already exist.";
return false;
}
// Return the path to an existing image of any type, if one exists.
// If not, generate a path based on the preferred image type.
if (qcow2_exists) {
*path_out = *qcow2_path;
} else if (raw_exists) {
*path_out = *raw_path;
} else if (preferred_image_type == DISK_IMAGE_QCOW2) {
*path_out = *qcow2_path;
} else if (preferred_image_type == DISK_IMAGE_RAW ||
preferred_image_type == DISK_IMAGE_AUTO) {
*path_out = *raw_path;
} else {
LOG(ERROR) << "Unknown image type " << preferred_image_type;
return false;
}
return true;
}
case STORAGE_CRYPTOHOME_PLUGINVM: {
const auto plugin_path =
GetFilePathFromName(vm_id, storage_location, kPluginVmImageExtension);
if (!plugin_path) {
return false;
}
*path_out = *plugin_path;
return true;
}
default:
LOG(ERROR) << "Unknown storage location type";
return false;
}
}
// Given a VM's stateful disk, stored at |disk_location|, returns the filesystem
// which that stateful disk is formatted with. Returns "" if:
// - The disk hasn't been formatted (yet)
// - Some error occurs while checking
std::string GetFilesystem(const base::FilePath& disk_location) {
std::string output;
blkid_cache blkid_cache;
// No cache file is used as it should always query information from
// the device, i.e. setting cache file to /dev/null.
if (blkid_get_cache(&blkid_cache, "/dev/null") != 0) {
LOG(ERROR) << "Failed to initialize blkid cache handler";
return output;
}
blkid_dev dev = blkid_get_dev(blkid_cache, disk_location.value().c_str(),
BLKID_DEV_NORMAL);
if (!dev) {
LOG(ERROR) << "Failed to get device for '" << disk_location.value().c_str()
<< "'";
blkid_put_cache(blkid_cache);
return output;
}
char* filesystem_type =
blkid_get_tag_value(blkid_cache, "TYPE", disk_location.value().c_str());
if (filesystem_type) {
output = filesystem_type;
}
blkid_put_cache(blkid_cache);
return output;
}
bool CheckVmExists(const VmId& vm_id,
base::FilePath* out_path = nullptr,
StorageLocation* storage_location = nullptr) {
for (int l = StorageLocation_MIN; l <= StorageLocation_MAX; l++) {
StorageLocation location = static_cast<StorageLocation>(l);
base::FilePath disk_path;
if (GetDiskPathFromName(vm_id, location, &disk_path) &&
base::PathExists(disk_path)) {
if (out_path) {
*out_path = disk_path;
}
if (storage_location) {
*storage_location = location;
}
return true;
}
}
return false;
}
// Returns the desired size of VM disks, which is 90% of the available space
// (excluding the space already taken up by the disk). If storage ballooning
// is being used, we instead return 95% of the total disk space.
uint64_t CalculateDesiredDiskSize(base::FilePath disk_location,
uint64_t current_usage,
bool storage_ballooning = false) {
if (storage_ballooning) {
auto total_space =
base::SysInfo::AmountOfTotalDiskSpace(disk_location.DirName());
return ((total_space * 95) / 100) & kDiskSizeMask;
}
uint64_t free_space =
base::SysInfo::AmountOfFreeDiskSpace(disk_location.DirName());
free_space += current_usage;
uint64_t disk_size = ((free_space * 9) / 10) & kDiskSizeMask;
return std::max(disk_size, kMinimumDiskSize);
}
// Returns true if the disk should not be automatically resized because it is
// not sparse and its size was specified by the user.
bool IsDiskPreallocatedWithUserChosenSize(const std::string& disk_path) {
return getxattr(disk_path.c_str(),
kDiskImagePreallocatedWithUserChosenSizeXattr, nullptr,
0) >= 0;
}
// Mark a non-sparse disk with an xattr indicating its size has been chosen by
// the user.
bool SetPreallocatedWithUserChosenSizeAttr(const base::ScopedFD& fd) {
// The xattr value doesn't matter, only its existence.
// Store something human-readable for debugging.
static constexpr char val[] = "1";
return fsetxattr(fd.get(), kDiskImagePreallocatedWithUserChosenSizeXattr, val,
sizeof(val), 0) == 0;
}
void FormatDiskImageStatus(const DiskImageOperation* op,
DiskImageStatusResponse* status) {
status->set_status(op->status());
status->set_command_uuid(op->uuid());
status->set_failure_reason(op->failure_reason());
status->set_progress(op->GetProgress());
}
uint64_t GetFileUsage(const base::FilePath& path) {
struct stat st;
if (stat(path.value().c_str(), &st) == 0) {
// Use the st_blocks value to get the space usage (as in 'du') of the
// file. st_blocks is always in units of 512 bytes, regardless of the
// underlying filesystem and block device block size.
return st.st_blocks * 512;
}
return 0;
}
// vm_id.name should always be less than kMaxVmNameLength characters long.
base::FilePath GetVmLogPath(const VmId& vm_id, const std::string& extension) {
std::string encoded_vm_name = GetEncodedName(vm_id.name());
base::FilePath path = base::FilePath(kCryptohomeRoot)
.Append(kCrosvmDir)
.Append(vm_id.owner_id())
.Append(kCrosvmLogDir)
.Append(encoded_vm_name)
.AddExtension(extension);
return path;
}
// Returns a hash string that is safe to use as a filename.
std::string GetMd5HashForFilename(const std::string& str) {
std::string result;
base::MD5Digest digest;
base::MD5Sum(base::as_byte_span(str), &digest);
std::string_view hash_piece(reinterpret_cast<char*>(&digest.a[0]),
sizeof(digest.a));
// Note, we can not have '=' symbols in this path or it will break crosvm's
// commandline argument parsing, so we use OMIT_PADDING.
base::Base64UrlEncode(hash_piece, base::Base64UrlEncodePolicy::OMIT_PADDING,
&result);
return result;
}
// Reclaims memory of the crosvm process with |pid| by writing "shmem" to
// /proc/<pid>/reclaim. Since this function may block 10 seconds or more, do
// not call on the main thread.
ReclaimVmMemoryResponse ReclaimVmMemoryInternal(pid_t pid, int32_t page_limit) {
ReclaimVmMemoryResponse response;
if (page_limit < 0) {
LOG(ERROR) << "Invalid negative page_limit " << page_limit;
response.set_failure_reason("Negative page_limit");
return response;
}
const std::string path = base::StringPrintf("/proc/%d/reclaim", pid);
base::ScopedFD fd(
HANDLE_EINTR(open(path.c_str(), O_WRONLY | O_CLOEXEC | O_NOFOLLOW)));
if (!fd.is_valid()) {
LOG(ERROR) << "Failed to open " << path;
response.set_failure_reason("Failed to open /proc filesystem");
return response;
}
const std::string reclaim = "shmem";
std::list commands = {reclaim};
if (page_limit != 0) {
LOG(INFO) << "per-process reclaim active: [" << page_limit << "] pages";
commands.push_front(reclaim + " " + base::NumberToString(page_limit));
}
ssize_t bytes_written = 0;
int attempts = 0;
bool write_ok = false;
for (const auto& v : commands) {
++attempts;
// We want to open the file only once, and write two times to it,
// different values. WriteFile() and its variants would
// open/close/write, which would cause an unnecessary open/close
// cycle, so we use write() directly.
bytes_written = HANDLE_EINTR(write(fd.get(), v.c_str(), v.size()));
write_ok = (bytes_written == v.size());
if (write_ok || (errno != EINVAL)) {
break;
}
}
if (!write_ok) {
PLOG(ERROR) << "Failed to write to " << path
<< " bytes_written: " << bytes_written
<< " attempts: " << attempts;
response.set_failure_reason("Failed to write to /proc filesystem");
return response;
}
LOG(INFO) << "Successfully reclaimed VM memory. PID=" << pid;
response.set_success(true);
return response;
}
} // namespace
namespace internal {
std::optional<internal::VmStartImageFds> GetVmStartImageFds(
const google::protobuf::RepeatedField<int>& fds,
const std::vector<base::ScopedFD>& file_handles) {
if (file_handles.size() != fds.size()) {
return std::nullopt;
}
struct internal::VmStartImageFds result;
size_t count = 0;
for (const auto& fdType : fds) {
std::optional<base::ScopedFD> fd(dup(file_handles[count++].get()));
if (!fd) {
LOG(ERROR) << "Failed to get VM start image file descriptor";
return std::nullopt;
}
switch (fdType) {
case StartVmRequest_FdType_KERNEL:
result.kernel_fd = std::move(*fd);
break;
case StartVmRequest_FdType_ROOTFS:
result.rootfs_fd = std::move(*fd);
break;
case StartVmRequest_FdType_INITRD:
result.initrd_fd = std::move(*fd);
break;
case StartVmRequest_FdType_STORAGE:
result.storage_fd = std::move(*fd);
break;
case StartVmRequest_FdType_BIOS:
result.bios_fd = std::move(*fd);
break;
case StartVmRequest_FdType_PFLASH:
result.pflash_fd = std::move(*fd);
break;
default:
LOG(WARNING) << "received request with unknown FD type " << fdType
<< ". Ignoring.";
}
}
return result;
}
} // namespace internal
base::FilePath Service::GetVmGpuCachePathInternal(const VmId& vm_id) {
std::string vm_dir;
// Note, we can not have '=' symbols in this path or it will break crosvm's
// commandline argument parsing, so we use OMIT_PADDING.
base::Base64UrlEncode(vm_id.name(), base::Base64UrlEncodePolicy::OMIT_PADDING,
&vm_dir);
std::string cache_id;
std::string error;
bool per_boot_cache = feature::PlatformFeatures::Get()->IsEnabledBlocking(
kPerBootVmShaderCacheFeature);
// if per-boot cache feature is enabled or we failed to read BUILD_ID from
// /etc/os-release, set |cache_id| as boot-id.
brillo::OsReleaseReader reader;
reader.Load();
if (per_boot_cache || !reader.GetString("BUILD_ID", &cache_id)) {
CHECK(base::ReadFileToString(base::FilePath(kBootIdFile), &cache_id));
}
return base::FilePath(kCryptohomeRoot)
.Append(kCrosvmDir)
.Append(vm_id.owner_id())
.Append(kCrosvmGpuCacheDir)
.Append(GetMd5HashForFilename(cache_id))
.Append(vm_dir);
}
std::optional<int64_t> Service::GetAvailableMemory() {
dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
resource_manager::kGetAvailableMemoryKBMethod);
auto dbus_response =
CallDBusMethod(bus_, resource_manager_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response) {
LOG(ERROR) << "Failed to get available memory size from resourced";
return std::nullopt;
}
dbus::MessageReader reader(dbus_response.get());
uint64_t available_kb;
if (!reader.PopUint64(&available_kb)) {
LOG(ERROR)
<< "Failed to read available memory size from the D-Bus response";
return std::nullopt;
}
return KiB(available_kb);
}
std::optional<int64_t> Service::GetForegroundAvailableMemory() {
dbus::MethodCall method_call(
resource_manager::kResourceManagerInterface,
resource_manager::kGetForegroundAvailableMemoryKBMethod);
auto dbus_response =
CallDBusMethod(bus_, resource_manager_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response) {
LOG(ERROR)
<< "Failed to get foreground available memory size from resourced";
return std::nullopt;
}
dbus::MessageReader reader(dbus_response.get());
uint64_t available_kb;
if (!reader.PopUint64(&available_kb)) {
LOG(ERROR) << "Failed to read foreground available memory size from the "
"D-Bus response";
return std::nullopt;
}
return KiB(available_kb);
}
std::optional<MemoryMargins> Service::GetMemoryMargins() {
dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
resource_manager::kGetMemoryMarginsKBMethod);
auto dbus_response =
CallDBusMethod(bus_, resource_manager_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response) {
LOG(ERROR) << "Failed to get critical margin size from resourced";
return std::nullopt;
}
dbus::MessageReader reader(dbus_response.get());
MemoryMargins margins;
if (!reader.PopUint64(&margins.critical)) {
LOG(ERROR)
<< "Failed to read available critical margin from the D-Bus response";
return std::nullopt;
}
if (!reader.PopUint64(&margins.moderate)) {
LOG(ERROR)
<< "Failed to read available moderate margin from the D-Bus response";
return std::nullopt;
}
margins.critical *= KiB(1);
margins.moderate *= KiB(1);
return margins;
}
std::optional<ComponentMemoryMargins> Service::GetComponentMemoryMargins() {
static constexpr char kChromeCriticalKey[] = "ChromeCritical";
static constexpr char kChromeModerateKey[] = "ChromeModerate";
static constexpr char kArcvmForegroundKey[] = "ArcvmForeground";
static constexpr char kArcvmPerceptibleKey[] = "ArcvmPerceptible";
static constexpr char kArcvmCachedKey[] = "ArcvmCached";
static constexpr char kArcContainerForeground[] = "ArcContainerForeground";
static constexpr char kArcContainerPerceptible[] = "ArcContainerPerceptible";
static constexpr char kArcContainerCached[] = "ArcContainerCached";
dbus::MethodCall method_call(
resource_manager::kResourceManagerInterface,
resource_manager::kGetComponentMemoryMarginsKBMethod);
auto dbus_response =
CallDBusMethod(bus_, resource_manager_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response) {
LOG(ERROR) << "Failed to get component margin sizes from resourced.";
return std::nullopt;
}
dbus::MessageReader reader(dbus_response.get());
dbus::MessageReader array_reader(nullptr);
if (!reader.PopArray(&array_reader)) {
LOG(ERROR) << "Failed parsing component memory margins";
return std::nullopt;
}
ComponentMemoryMargins margins;
while (array_reader.HasMoreData()) {
dbus::MessageReader dict_entry_reader(nullptr);
if (array_reader.PopDictEntry(&dict_entry_reader)) {
std::string key;
uint64_t value;
if (!dict_entry_reader.PopString(&key) ||
!dict_entry_reader.PopUint64(&value)) {
LOG(ERROR) << "Error popping dictionary entry from D-Bus message";
return std::nullopt;
}
value *= KiB(1);
if (key == kChromeCriticalKey) {
margins.chrome_critical = value;
} else if (key == kChromeModerateKey) {
margins.chrome_moderate = value;
} else if (key == kArcvmForegroundKey) {
margins.arcvm_foreground = value;
} else if (key == kArcvmPerceptibleKey) {
margins.arcvm_perceptible = value;
} else if (key == kArcvmCachedKey) {
margins.arcvm_cached = value;
} else if (key == kArcContainerForeground) {
margins.arc_container_foreground = value;
} else if (key == kArcContainerPerceptible) {
margins.arc_container_perceptible = value;
} else if (key == kArcContainerCached) {
margins.arc_container_cached = value;
} else {
LOG(ERROR) << "Unrecognized dict entry '" << key
<< "' for component memory margins";
}
}
}
return margins;
}
std::optional<resource_manager::GameMode> Service::GetGameMode() {
dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
resource_manager::kGetGameModeMethod);
auto dbus_response =
CallDBusMethod(bus_, resource_manager_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response) {
LOG(ERROR) << "Failed to get geme mode from resourced";
return std::nullopt;
}
dbus::MessageReader reader(dbus_response.get());
uint8_t game_mode;
if (!reader.PopByte(&game_mode)) {
LOG(ERROR) << "Failed to read game mode from the D-Bus response";
return std::nullopt;
}
return static_cast<resource_manager::GameMode>(game_mode);
}
static std::optional<std::string> GameModeToForegroundVmName(
resource_manager::GameMode game_mode) {
using resource_manager::GameMode;
if (USE_BOREALIS_HOST && game_mode == GameMode::BOREALIS) {
return "borealis";
}
if (game_mode == GameMode::OFF) {
return std::nullopt;
}
LOG(ERROR) << "Unexpected game mode value " << static_cast<int>(game_mode);
return std::nullopt;
}
// Runs balloon policy against each VM to balance memory.
// This will be called periodically by balloon_resizing_timer_.
void Service::RunBalloonPolicy() {
VMT_TRACE(kCategory, "Service::RunBalloonPolicy");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(b/191946183): Design and migrate to a new D-Bus API
// that is less chatty for implementing balloon logic.
std::optional<MemoryMargins> memory_margins_opt = GetMemoryMargins();
if (!memory_margins_opt) {
LOG(ERROR) << "Failed to get ChromeOS memory margins";
return;
}
MemoryMargins memory_margins = *memory_margins_opt;
const auto available_memory = GetAvailableMemory();
if (!available_memory.has_value()) {
return;
}
const auto game_mode = GetGameMode();
if (!game_mode.has_value()) {
return;
}
std::optional<int64_t> foreground_available_memory;
if (*game_mode != resource_manager::GameMode::OFF) {
// foreground_available_memory is only used when the game mode is enabled.
foreground_available_memory = GetForegroundAvailableMemory();
if (!foreground_available_memory.has_value()) {
return;
}
}
std::optional<ComponentMemoryMargins> component_margins =
GetComponentMemoryMargins();
if (!component_margins) {
LOG(ERROR) << "Failed to get component memory margins";
return;
}
const auto foreground_vm_name = GameModeToForegroundVmName(*game_mode);
for (auto& vm_entry : vms_) {
auto& vm = vm_entry.second;
if (vm->IsSuspended()) {
// Skip suspended VMs since there is no effect.
continue;
}
const std::unique_ptr<BalloonPolicyInterface>& policy =
vm->GetBalloonPolicy(memory_margins, vm_entry.first.name());
if (!policy) {
// Skip VMs that don't have a memory policy. It may just not be ready
// yet.
continue;
}
auto stats_opt = vm->GetBalloonStats(base::Milliseconds(100));
if (!stats_opt) {
// Stats not available. Skip running policies.
continue;
}
BalloonStats stats = *stats_opt;
// Switch available memory for this VM based on the current game mode.
bool is_in_game_mode = foreground_vm_name.has_value() &&
vm_entry.first.name() == foreground_vm_name;
const int64_t available_memory_for_vm =
is_in_game_mode ? *foreground_available_memory : *available_memory;
int64_t delta = policy->ComputeBalloonDelta(
stats, available_memory_for_vm, is_in_game_mode, vm_entry.first.name(),
*available_memory, *component_margins);
uint64_t target =
std::max(static_cast<int64_t>(0),
static_cast<int64_t>(stats.balloon_actual) + delta);
if (target != stats.balloon_actual) {
vm->SetBalloonSize(target);
}
}
}
std::optional<bool> Service::IsFeatureEnabled(const std::string& feature_name,
std::string* error_out) {
dbus::MethodCall method_call(
chromeos::kChromeFeaturesServiceInterface,
chromeos::kChromeFeaturesServiceIsFeatureEnabledMethod);
dbus::MessageWriter writer(&method_call);
writer.AppendString(feature_name);
dbus::Error error;
auto dbus_response = CallDBusMethodWithErrorResponse(
bus_, chrome_features_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, &error);
if (error.IsValid()) {
*error_out = error.message();
return std::nullopt;
}
dbus::MessageReader reader(dbus_response.get());
bool result;
if (!reader.PopBool(&result)) {
*error_out = "Failed to read bool from D-Bus response";
return std::nullopt;
}
*error_out = "";
return result;
}
bool Service::ListVmDisksInLocation(const std::string& cryptohome_id,
StorageLocation location,
const std::string& lookup_name,
ListVmDisksResponse* response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::FilePath image_dir;
base::FileEnumerator::FileType file_type = base::FileEnumerator::FILES;
const char* const* allowed_ext = kDiskImageExtensions;
switch (location) {
case STORAGE_CRYPTOHOME_ROOT:
image_dir = base::FilePath(kCryptohomeRoot)
.Append(kCrosvmDir)
.Append(cryptohome_id);
break;
case STORAGE_CRYPTOHOME_PLUGINVM:
image_dir = base::FilePath(kCryptohomeRoot)
.Append(kPluginVmDir)
.Append(cryptohome_id);
file_type = base::FileEnumerator::DIRECTORIES;
allowed_ext = kPluginVmImageExtensions;
break;
default:
response->set_failure_reason("Unsupported storage location for images");
return false;
}
if (!base::DirectoryExists(image_dir)) {
// No directory means no VMs, return the empty response.
return true;
}
uint64_t total_size = 0;
base::FileEnumerator dir_enum(image_dir, false, file_type);
for (base::FilePath path = dir_enum.Next(); !path.empty();
path = dir_enum.Next()) {
std::string extension = path.BaseName().Extension();
bool allowed = false;
for (auto p = allowed_ext; *p; p++) {
if (extension == *p) {
allowed = true;
break;
}
}
if (!allowed) {
continue;
}
base::FilePath bare_name = path.BaseName().RemoveExtension();
if (bare_name.empty()) {
continue;
}
std::string image_name = GetDecodedName(bare_name.value());
if (image_name.empty()) {
continue;
}
if (!lookup_name.empty() && lookup_name != image_name) {
continue;
}
uint64_t size = dir_enum.GetInfo().IsDirectory()
? ComputeDirectorySize(path)
: GetFileUsage(path);
total_size += size;
uint64_t min_size;
uint64_t available_space;
VmId vm_id(cryptohome_id, image_name);
auto iter = FindVm(vm_id);
// VM may not be running - in this case, we can't determine min_size or
// available_space, so report 0 for unknown.
min_size = 0;
available_space = 0;
if (iter != vms_.end()) {
// GetMinDiskSize relies on btrfs specific functions.
if (GetFilesystem(path) == "btrfs") {
min_size = iter->second->GetMinDiskSize();
}
available_space = iter->second->GetAvailableDiskSpace();
}
enum DiskImageType image_type = DiskImageType::DISK_IMAGE_AUTO;
if (extension == kRawImageExtension) {
image_type = DiskImageType::DISK_IMAGE_RAW;
} else if (extension == kQcowImageExtension) {
image_type = DiskImageType::DISK_IMAGE_QCOW2;
} else if (extension == kPluginVmImageExtension) {
image_type = DiskImageType::DISK_IMAGE_PLUGINVM;
}
VmDiskInfo* image = response->add_images();
image->set_name(std::move(image_name));
image->set_storage_location(location);
image->set_size(size);
image->set_min_size(min_size);
image->set_available_space(available_space);
image->set_image_type(image_type);
image->set_user_chosen_size(
IsDiskPreallocatedWithUserChosenSize(path.value()));
image->set_path(path.value());
}
response->set_total_size(response->total_size() + total_size);
return true;
}
// static
void Service::CreateAndHost(
int signal_fd,
base::OnceCallback<void(std::unique_ptr<Service>)> on_hosted) {
dbus::Bus::Options opts;
opts.bus_type = dbus::Bus::SYSTEM;
opts.dbus_task_runner =
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()});
scoped_refptr<dbus::Bus> bus = new dbus::Bus(std::move(opts));
dbus::Bus* bus_ptr = bus.get();
bus->GetDBusTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&dbus::Bus::Connect, base::Unretained(bus_ptr)),
base::BindOnce(
[](scoped_refptr<dbus::Bus> bus, int signal_fd,
base::OnceCallback<void(std::unique_ptr<Service>)> on_hosted,
bool connected) {
if (!connected) {
LOG(ERROR) << "Failed to connect to system bus";
std::move(on_hosted).Run(nullptr);
return;
}
CreateAndHost(std::move(bus), signal_fd, std::move(on_hosted));
},
std::move(bus), signal_fd, std::move(on_hosted)));
}
// static
void Service::CreateAndHost(
scoped_refptr<dbus::Bus> bus,
int signal_fd,
base::OnceCallback<void(std::unique_ptr<Service>)> on_hosted) {
// Bus should be connected when using this API.
CHECK(bus->IsConnected());
auto service = base::WrapUnique(new Service(signal_fd, std::move(bus)));
if (!service->Init()) {
std::move(on_hosted).Run(nullptr);
return;
}
Service* service_ptr = service.get();
DbusAdaptor::Create(
service_ptr->bus_, service_ptr,
base::BindOnce(
[](std::unique_ptr<Service> owned_service,
base::OnceCallback<void(std::unique_ptr<Service>)> on_hosted,
std::unique_ptr<DbusAdaptor> adaptor) {
if (!adaptor) {
std::move(on_hosted).Run(nullptr);
return;
}
owned_service->concierge_adaptor_ = std::move(adaptor);
std::move(on_hosted).Run(std::move(owned_service));
},
std::move(service), std::move(on_hosted)));
}
Service::Service(int signal_fd, scoped_refptr<dbus::Bus> bus)
: signal_fd_(signal_fd),
bus_(std::move(bus)),
next_seneschal_server_port_(kFirstSeneschalServerPort),
weak_ptr_factory_(this) {
// The service should run on the thread that *created* the bus, not the
// thread that de/serializes dbus messages.
bus_->AssertOnOriginThread();
}
Service::~Service() {
if (grpc_server_vm_) {
grpc_server_vm_->Shutdown();
}
}
bool Service::Init() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
VMT_TRACE_BEGIN(kCategory, "Service::Init");
metrics_ = std::make_unique<MetricsLibrary>(
base::MakeRefCounted<AsynchronousMetricsWriter>(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})));
vmm_swap_tbw_policy_ = std::make_unique<VmmSwapTbwPolicy>(
raw_ref<MetricsLibraryInterface>::from_ptr(metrics_.get()),
base::FilePath(kVmmSwapTbwHistoryFilePath));
dlcservice_client_ = std::make_unique<DlcHelper>(bus_);
// Set up the D-Bus client for shill.
shill_client_ = std::make_unique<ShillClient>(bus_);
shill_client_->RegisterResolvConfigChangedHandler(base::BindRepeating(
&Service::OnResolvConfigChanged, weak_ptr_factory_.GetWeakPtr()));
shill_client_->RegisterDefaultServiceChangedHandler(
base::BindRepeating(&Service::OnDefaultNetworkServiceChanged,
weak_ptr_factory_.GetWeakPtr()));
// Set up the D-Bus client for powerd and register suspend/resume handlers.
power_manager_client_ = std::make_unique<PowerManagerClient>(bus_);
power_manager_client_->RegisterSuspendDelay(
base::BindRepeating(&Service::HandleSuspendImminent,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&Service::HandleSuspendDone,
weak_ptr_factory_.GetWeakPtr()));
// Setup D-Bus proxy for spaced.
disk_usage_proxy_ = std::make_unique<spaced::DiskUsageProxy>(
std::make_unique<org::chromium::SpacedProxy>(bus_));
disk_usage_proxy_->AddObserver(this);
disk_usage_proxy_->StartMonitoring();
// Get the D-Bus proxy for communicating with cicerone.
cicerone_service_proxy_ = bus_->GetObjectProxy(
vm_tools::cicerone::kVmCiceroneServiceName,
dbus::ObjectPath(vm_tools::cicerone::kVmCiceroneServicePath));
cicerone_service_proxy_->ConnectToSignal(
vm_tools::cicerone::kVmCiceroneServiceName,
vm_tools::cicerone::kTremplinStartedSignal,
base::BindRepeating(&Service::OnTremplinStartedSignal,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&Service::OnSignalConnected,
weak_ptr_factory_.GetWeakPtr()));
// Get the D-Bus proxy for communicating with seneschal.
seneschal_service_proxy_ = bus_->GetObjectProxy(
vm_tools::seneschal::kSeneschalServiceName,
dbus::ObjectPath(vm_tools::seneschal::kSeneschalServicePath));
// Get the D-Bus proxy for communicating with Plugin VM dispatcher.
vm_permission_service_proxy_ = vm_permission::GetServiceProxy(bus_);
// Get the D-Bus proxy for communicating with Plugin VM dispatcher.
vmplugin_service_proxy_ = pvm::dispatcher::GetServiceProxy(bus_);
pvm::dispatcher::RegisterVmToolsChangedCallbacks(
vmplugin_service_proxy_,
base::BindRepeating(&Service::OnVmToolsStateChangedSignal,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&Service::OnSignalConnected,
weak_ptr_factory_.GetWeakPtr()));
// Get the D-Bus proxy for communicating with resource manager.
resource_manager_service_proxy_ = bus_->GetObjectProxy(
resource_manager::kResourceManagerServiceName,
dbus::ObjectPath(resource_manager::kResourceManagerServicePath));
// Get the D-Bus proxy for communicating with Chrome Features Service.
chrome_features_service_proxy_ = bus_->GetObjectProxy(
chromeos::kChromeFeaturesServiceName,
dbus::ObjectPath(chromeos::kChromeFeaturesServicePath));
shadercached_proxy_ = bus_->GetObjectProxy(
shadercached::kShaderCacheServiceName,
dbus::ObjectPath(shadercached::kShaderCacheServicePath));
CHECK(feature::PlatformFeatures::Initialize(bus_));
VMT_TRACE_END(kCategory);
// Setup & start the gRPC listener services.
if (!SetupListenerService(
&startup_listener_,
base::StringPrintf("vsock:%u:%u", VMADDR_CID_ANY,
vm_tools::kDefaultStartupListenerPort),
&grpc_server_vm_)) {
LOG(ERROR) << "Failed to setup/startup the VM grpc server";
return false;
}
if (!localtime_watcher_.Watch(
base::FilePath(kLocaltimePath),
base::FilePathWatcher::Type::kNonRecursive,
base::BindRepeating(&Service::OnLocaltimeFileChanged,
weak_ptr_factory_.GetWeakPtr()))) {
LOG(WARNING) << "Failed to initialize file watcher for timezone change";
}
int64_t root_device_size = PostTaskAndWaitForResult(
bus_->GetDBusTaskRunner(), base::BindOnce(
[](spaced::DiskUsageProxy* proxy) {
return proxy->GetRootDeviceSize();
},
disk_usage_proxy_.get()));
if (root_device_size < 0) {
LOG(WARNING) << "Failed to determine disk size, defaulting to minimum 16GB";
root_device_size = 16ull * 1000 * 1000 * 1000;
}
double device_size_multiplier = static_cast<double>(root_device_size) /
kTbwTargetForVmmSwapReferenceDiskSize;
int64_t tbw_target = std::min(
static_cast<int64_t>(device_size_multiplier * kTbwTargetForVmmSwapPerDay),
kTbwMaxForVmmSwapPerDay);
vmm_swap_tbw_policy_->SetTargetTbwPerDay(tbw_target);
base::FilePath tbw_history_file_path(kVmmSwapTbwHistoryFilePath);
// VmmSwapTbwPolicy repopulate pessimistic history if it fails to init. This
// is safe to continue using regardless of the result.
vmm_swap_tbw_policy_->Init();
return true;
}
bool Service::InitVmMemoryManagementService() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!vmmms_init_done_) {
if (!DoInitVmMemoryManagementService()) {
return false;
}
vmmms_init_done_ = true;
if (get_vmmms_kills_connection_response_sender_) {
SendGetVmmmsKillConnectionResponse();
}
}
return true;
}
bool Service::DoInitVmMemoryManagementService() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The VM Memory Management Service has a dependency on VSOCK Loopback and
// cannot be enabled on kernels older than 5.4
auto kernel_version = UntrustedVMUtils::GetKernelVersion();
if (kernel_version.first < 5 ||
(kernel_version.first == 5 && kernel_version.second < 4)) {
LOG(INFO) << "VmMemoryManagementService not supported by kernel";
return false;
}
vm_memory_management_service_ = std::make_unique<mm::MmService>(
raw_ref<MetricsLibraryInterface>::from_ptr(metrics_.get()));
if (!vm_memory_management_service_->Start()) {
vm_memory_management_service_.reset();
LOG(ERROR) << "Failed to initialize VmMemoryManagementService.";
return false;
}
LOG(INFO) << "Enabling VmMemoryManagementService";
return true;
}
void Service::ChildExited() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// 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)) {
if (WEXITSTATUS(status) != 0) {
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.
auto iter = std::find_if(vms_.begin(), vms_.end(), [=](auto& pair) {
VmBaseImpl::Info info = pair.second->GetInfo();
return pid == info.pid;
});
if (iter != vms_.end()) {
// Notify that the VM has exited.
NotifyVmStopped(iter->first, iter->second->GetInfo().cid, VM_EXITED);
// Now remove it from the vm list.
vms_.erase(iter);
}
}
// By this point if a VM exited, the VM instance is guaranteed to have been
// removed from vms_. HandleChildExit() is run regardless of the exit type
// (graceful, crash, etc.) so this is the best place to check if the balloon
// policy timer should be stopped.
if (balloon_resizing_timer_.IsRunning() && !BalloonTimerShouldRun()) {
LOG(INFO) << "Balloon timer no longer needed. Stopping the timer.";
balloon_resizing_timer_.Stop();
}
}
void Service::Stop(base::OnceClosure on_stopped) {
LOG(INFO) << "Shutting down due to SIGTERM";
StopAllVmsImpl(SERVICE_SHUTDOWN);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(on_stopped));
}
void Service::StartVm(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<StartVmResponse>>
response_cb,
const StartVmRequest& request,
const std::vector<base::ScopedFD>& file_handles) {
ASYNC_SERVICE_METHOD();
StartVmResponse response;
// We change to a success status later if necessary.
response.set_status(VM_STATUS_FAILURE);
if (!CheckStartVmPreconditions(request, &response)) {
response_cb->Return(response);
return;
}
std::optional<internal::VmStartImageFds> vm_start_image_fds =
internal::GetVmStartImageFds(request.fds(), file_handles);
if (!vm_start_image_fds) {
response.set_failure_reason("failed to get a VmStartImage fd");
response_cb->Return(response);
return;
}
response = StartVmInternal(request, std::move(*vm_start_image_fds));
response_cb->Return(response);
return;
}
StartVmResponse Service::StartVmInternal(
StartVmRequest request, internal::VmStartImageFds vm_start_image_fds) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
StartVmResponse response;
response.set_status(VM_STATUS_FAILURE);
VmId vm_id(request.owner_id(), request.name());
VmBuilder vm_builder;
apps::VmType classification = internal::ClassifyVm(request);
// Log how long it takes to start the VM.
metrics::DurationRecorder duration_recorder(
raw_ref<MetricsLibraryInterface>::from_ptr(metrics_.get()),
classification, metrics::DurationRecorder::Event::kVmStart);
std::string failure_reason;
std::optional<base::FilePath> biosDlcPath, vmDlcPath, toolsDlcPath;
if (!untrusted_vm_utils_.SafeToRunVirtualMachines(&failure_reason)) {
LOG(ERROR) << failure_reason;
response.set_failure_reason(failure_reason);
return response;
}
if (!vm_start_image_fds.bios_fd.has_value() &&
!request.vm().bios_dlc_id().empty() &&
request.vm().bios_dlc_id() == kBruschettaBiosDlcId) {
biosDlcPath = GetVmImagePath(kBruschettaBiosDlcId, failure_reason);
if (!failure_reason.empty() || !biosDlcPath.has_value()) {
response.set_failure_reason(failure_reason);
return response;
}
}
if (!request.vm().dlc_id().empty()) {
vmDlcPath = GetVmImagePath(request.vm().dlc_id(), failure_reason);
if (!failure_reason.empty() || !vmDlcPath.has_value()) {
response.set_failure_reason(failure_reason);
return response;
}
}
if (!request.vm().tools_dlc_id().empty()) {
toolsDlcPath = GetVmImagePath(request.vm().tools_dlc_id(), failure_reason);
if (!failure_reason.empty() || !toolsDlcPath.has_value()) {
response.set_failure_reason(failure_reason);
return response;
}
}
// Make sure we have our signal connected if starting a Termina VM.
if (classification == apps::VmType::TERMINA &&
!is_tremplin_started_signal_connected_) {
LOG(ERROR) << "Can't start Termina VM without TremplinStartedSignal";
response.set_failure_reason("TremplinStartedSignal not connected");
return response;
}
if (request.disks_size() > kMaxExtraDisks) {
LOG(ERROR) << "Rejecting request with " << request.disks_size()
<< " extra disks";
response.set_failure_reason("Too many extra disks");
return response;
}
// Exists just to keep FDs around for crosvm to inherit
std::vector<brillo::SafeFD> owned_fds;
auto root_fd_result = brillo::SafeFD::Root();
if (brillo::SafeFD::IsError(root_fd_result.second)) {
LOG(ERROR) << "Could not open root directory: "
<< static_cast<int>(root_fd_result.second);
response.set_failure_reason("Could not open root directory");
return response;
}
auto root_fd = std::move(root_fd_result.first);
VMImageSpec image_spec = internal::GetImageSpec(
vm_start_image_fds.kernel_fd, vm_start_image_fds.rootfs_fd,
vm_start_image_fds.initrd_fd, vm_start_image_fds.bios_fd,
vm_start_image_fds.pflash_fd, biosDlcPath, vmDlcPath, toolsDlcPath,
failure_reason);
if (!failure_reason.empty()) {
LOG(ERROR) << "Failed to get image paths: " << failure_reason;
response.set_failure_reason("Failed to get image paths: " + failure_reason);
return response;
}
std::string convert_fd_based_path_result = ConvertToFdBasedPaths(
root_fd, request.writable_rootfs(), image_spec, owned_fds);
if (!convert_fd_based_path_result.empty()) {
response.set_failure_reason(convert_fd_based_path_result);
return response;
}
std::optional<base::FilePath> pflash_result =
GetInstalledOrRequestPflashPath(vm_id, image_spec.pflash);
if (!pflash_result) {
LOG(ERROR) << "Failed to get pflash path";
response.set_failure_reason("Failed to get pflash path");
return response;
}
// The path can be empty if no pflash file is installed or nothing sent by the
// user.
base::FilePath pflash = pflash_result.value();
// Track the next available virtio-blk device name.
// Assume that the rootfs filesystem was assigned /dev/pmem0 if
// pmem is used, /dev/vda otherwise.
// Assume every subsequent image was assigned a letter in alphabetical order
// starting from 'b'.
bool use_pmem = USE_PMEM_DEVICE_FOR_ROOTFS;
std::string rootfs_device = use_pmem ? "/dev/pmem0" : "/dev/vda";
unsigned char disk_letter = use_pmem ? 'a' : 'b';
// In newer components, the /opt/google/cros-containers directory
// is split into its own disk image(vm_tools.img). Detect whether it exists
// to keep compatibility with older components with only vm_rootfs.img.
std::string tools_device;
if (base::PathExists(image_spec.tools_disk)) {
failure_reason = ConvertToFdBasedPath(root_fd, &image_spec.tools_disk,
O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Could not open tools_disk file";
response.set_failure_reason(failure_reason);
return response;
}
vm_builder.AppendDisk(VmBuilder::Disk{
.path = std::move(image_spec.tools_disk), .writable = false});
tools_device = base::StringPrintf("/dev/vd%c", disk_letter++);
}
if (request.disks().size() == 0) {
LOG(ERROR) << "Missing required stateful disk";
response.set_failure_reason("Missing required stateful disk");
return response;
}
// Assume the stateful device is the first disk in the request.
std::string stateful_device = base::StringPrintf("/dev/vd%c", disk_letter);
auto stateful_path = base::FilePath(request.disks()[0].path());
int64_t stateful_size = -1;
if (!base::GetFileSize(stateful_path, &stateful_size)) {
LOG(ERROR) << "Could not determine stateful disk size";
response.set_failure_reason(
"Internal error: unable to determine stateful disk size");
return response;
}
bool storage_ballooning = false;
// Storage ballooning enabled for Borealis (for ext4 setups in order
// to not interfere with the storage management solutions of legacy
// setups) and Bruschetta VMs.
if (USE_BOREALIS_HOST && classification == apps::VmType::BOREALIS &&
GetFilesystem(stateful_path) == "ext4") {
storage_ballooning = request.storage_ballooning();
} else if (classification == apps::VmType::BRUSCHETTA) {
storage_ballooning = true;
}
// TODO(b/288998343): remove when bug is fixed and interrupted discards are
// not lost.
if (storage_ballooning) {
TrimUserFilesystem();
}
for (const auto& d : request.disks()) {
VmBuilder::Disk disk{
.path = base::FilePath(d.path()),
.writable = d.writable(),
.sparse = !IsDiskPreallocatedWithUserChosenSize(d.path())};
failure_reason = ConvertToFdBasedPath(
root_fd, &disk.path, disk.writable ? O_RDWR : O_RDONLY, owned_fds);
if (!failure_reason.empty()) {
LOG(ERROR) << "Could not open disk file";
response.set_failure_reason(failure_reason);
return response;
}
vm_builder.AppendDisk(disk);
}
// Check if an opened storage image was passed over D-BUS.
if (vm_start_image_fds.storage_fd.has_value()) {
int raw_fd = vm_start_image_fds.storage_fd.value().get();
std::string failure_reason = internal::RemoveCloseOnExec(raw_fd);
if (!failure_reason.empty()) {
LOG(ERROR) << "failed to remove close-on-exec flag: " << failure_reason;
response.set_failure_reason(
"failed to get a path for extra storage disk: " + failure_reason);
return response;
}
bool writable = false;
int mode = fcntl(raw_fd, F_GETFL);
if (mode & O_RDWR || mode & O_WRONLY) {
writable = true;
}
vm_builder.AppendDisk(
VmBuilder::Disk{.path = base::FilePath(kProcFileDescriptorsPath)
.Append(base::NumberToString(raw_fd)),
.writable = writable,
.block_id = "cr-extra-disk"});
}
// Create the runtime directory.
base::FilePath runtime_dir;
if (!base::CreateTemporaryDirInDir(base::FilePath(kRuntimeDir), "vm.",
&runtime_dir)) {
PLOG(ERROR) << "Unable to create runtime directory for VM";
response.set_failure_reason(
"Internal error: unable to create runtime directory");
return response;
}
if (request.name().size() > kMaxVmNameLength) {
LOG(ERROR) << "VM name is too long";
response.set_failure_reason("VM name is too long");
return response;
}
base::FilePath log_path = GetVmLogPath(vm_id, kCrosvmLogSocketExt);
base::FilePath log_dir = log_path.DirName();
base::File::Error dir_error;
if (!base::CreateDirectoryAndGetError(log_dir, &dir_error)) {
LOG(ERROR) << "Failed to create crosvm log directory " << log_dir << ": "
<< base::File::ErrorToString(dir_error);
response.set_failure_reason("Failed to create crosvm log directory");
return response;
}
if (request.enable_big_gl() && !request.enable_gpu()) {
LOG(ERROR) << "Big GL enabled without GPU";
response.set_failure_reason("Big GL enabled without GPU");
return response;
}
if (request.enable_virtgpu_native_context() && !request.enable_gpu()) {
LOG(ERROR) << "Virtgpu native context enabled without GPU";
response.set_failure_reason("Virtgpu native context enabled without GPU");
return response;
}
// Enable the render server for Vulkan.
const bool enable_render_server = request.enable_gpu() && USE_CROSVM_VULKAN;
// Enable foz db list (dynamic un/loading for RO mesa shader cache) only for
// Borealis, for now.
const bool enable_foz_db_list =
USE_BOREALIS_HOST && classification == apps::VmType::BOREALIS;
VMGpuCacheSpec gpu_cache_spec;
if (request.enable_gpu()) {
gpu_cache_spec =
PrepareVmGpuCachePaths(vm_id, enable_render_server, enable_foz_db_list);
}
// Allocate resources for the VM.
uint32_t vsock_cid = vsock_cid_pool_.Allocate();
std::unique_ptr<GuestOsNetwork> network;
if (classification == apps::BRUSCHETTA) {
network = BruschettaNetwork::Create(bus_, vsock_cid);
} else if (USE_BOREALIS_HOST && classification == apps::BOREALIS) {
network = BorealisNetwork::Create(bus_, vsock_cid);
} else if (classification == apps::BAGUETTE) {
// Steal borealis network for Baguette
// TODO(b/339679659): Use separate network for Baguette after patchpanel change
LOG(INFO) << "Steal borealis network for Baguette";
network = BorealisNetwork::Create(bus_, vsock_cid);
} else {
network = TerminaNetwork::Create(bus_, vsock_cid);
}
if (!network) {
LOG(ERROR) << "Unable to get network resources";
response.set_failure_reason("Unable to get network resources");
return response;
}
uint32_t seneschal_server_port = next_seneschal_server_port_++;
std::unique_ptr<SeneschalServerProxy> server_proxy =
SeneschalServerProxy::CreateVsockProxy(bus_, seneschal_service_proxy_,
seneschal_server_port, vsock_cid,
{}, {});
if (!server_proxy) {
LOG(ERROR) << "Unable to start shared directory server";
response.set_failure_reason("Unable to start shared directory server");
return response;
}
// Set up a "checker" that will wait until the VM is ready or a signal is
// received while waiting for the VM to start or we timeout.
std::unique_ptr<VmStartChecker> vm_start_checker =
VmStartChecker::Create(signal_fd_);
if (!vm_start_checker) {
LOG(ERROR) << "Failed to create VM start checker";
response.set_failure_reason("Failed to create VM start checker");
return response;
}
// This will signal the event fd passed in when the VM is ready.
startup_listener_.AddPendingVm(vsock_cid, vm_start_checker->GetEventFd());
// Start the VM and build the response.
VmFeatures features{
.gpu = request.enable_gpu(),
.dgpu_passthrough = request.enable_dgpu_passthrough(),
.big_gl = request.enable_big_gl(),
.virtgpu_native_context = request.enable_virtgpu_native_context(),
.render_server = enable_render_server,
.vtpm_proxy = request.vtpm_proxy(),
.audio_capture = request.enable_audio_capture(),
};
std::vector<std::string> params(
std::make_move_iterator(request.mutable_kernel_params()->begin()),
std::make_move_iterator(request.mutable_kernel_params()->end()));
features.kernel_params = std::move(params);
std::vector<std::string> oem_strings(
std::make_move_iterator(request.mutable_oem_strings()->begin()),
std::make_move_iterator(request.mutable_oem_strings()->end()));
features.oem_strings = std::move(oem_strings);
// We use _SC_NPROCESSORS_ONLN here rather than
// base::SysInfo::NumberOfProcessors() so that offline CPUs are not counted.
// Also, |untrusted_vm_utils_| may disable SMT leading to cores being
// disabled. Hence, only allocate the lower of (available cores, cpus
// allocated by the user).
const int32_t cpus =
request.cpus() == 0
? sysconf(_SC_NPROCESSORS_ONLN)
: std::min(static_cast<int32_t>(sysconf(_SC_NPROCESSORS_ONLN)),
static_cast<int32_t>(request.cpus()));
// Notify VmLogForwarder that a vm is starting up.
SendVmStartingUpSignal(vm_id, classification, vsock_cid);
vm_builder.SetKernel(std::move(image_spec.kernel))
.SetBios(std::move(image_spec.bios))
.SetPflash(std::move(pflash))
.SetInitrd(std::move(image_spec.initrd))
.SetCpus(cpus)
.AppendSharedDir(CreateFontsSharedDataParam())
.EnableSmt(false /* enable */)
.SetGpuCachePath(std::move(gpu_cache_spec.device))
.AppendCustomParam("--vcpu-cgroup-path",
base::FilePath(kTerminaVcpuCpuCgroup).value())
.SetRenderServerCachePath(std::move(gpu_cache_spec.render_server));
if (enable_foz_db_list) {
auto prepare_result = PrepareShaderCache(vm_id, bus_, shadercached_proxy_);
if (prepare_result.has_value()) {
auto precompiled_cache_path =
base::FilePath(prepare_result.value().precompiled_cache_path());
vm_builder.SetFozDbListPath(std::move(gpu_cache_spec.foz_db_list))
.SetPrecompiledCachePath(precompiled_cache_path)
.AppendSharedDir(CreateShaderSharedDataParam(precompiled_cache_path));
} else {
LOG(ERROR) << "Unable to initialize shader cache: "
<< prepare_result.error();
}
}
if (!image_spec.rootfs.empty()) {
vm_builder.SetRootfs({.device = std::move(rootfs_device),
.path = std::move(image_spec.rootfs),
.writable = request.writable_rootfs()});
}
// Spoof baguette vm as termina to wayland server
VmWlInterface::Result wl_result = VmWlInterface::CreateWaylandServer(
bus_, vm_id,
classification == apps::BAGUETTE ? apps::TERMINA : classification);
if (!wl_result.has_value()) {
response.set_failure_reason("Unable to start a wayland server: " +
wl_result.error());
LOG(ERROR) << response.failure_reason();
return response;
}
std::unique_ptr<ScopedWlSocket> socket = std::move(wl_result).value();
vm_builder.SetWaylandSocket(socket->GetPath().value());
// Group the CPUs by their physical package ID to determine CPU cluster
// layout.
VmBuilder::VmCpuArgs vm_cpu_args =
internal::GetVmCpuArgs(cpus, base::FilePath(kCpuInfosPath));
vm_builder.SetVmCpuArgs(vm_cpu_args);
/* Enable hugepages on devices with > 7 GB memory */
if (base::SysInfo::AmountOfPhysicalMemoryMB() >= 7 * 1024) {
vm_builder.AppendCustomParam("--hugepages", "");
}
if (USE_BOREALIS_HOST && classification == apps::BOREALIS) {
bool vcpu_tweaks = feature::PlatformFeatures::Get()->IsEnabledBlocking(
kBorealisVcpuTweaksFeature);
if (vcpu_tweaks) {
// Enable the vCPU tweaks here
vm_builder.SetCpus(GetBorealisCpuCountOverride(cpus));
}
}
// TODO(b/288361720): This is temporary while we test the 'provision'
// mount option. Once we're satisfied things are stable, we'll make this
// the default and remove this feature check.
if (USE_BOREALIS_HOST && classification == apps::BOREALIS) {
std::string error;
std::optional<bool> provision =
IsFeatureEnabled(kBorealisProvisionFeature, &error);
if (!provision.has_value()) {
LOG(WARNING) << "Failed to check borealis provision feature: " << error;
} else if (provision.value()) {
vm_builder.AppendKernelParam("maitred.provision_stateful");
}
}
auto vm = TerminaVm::Create(TerminaVm::Config{
.vsock_cid = vsock_cid,
.network = std::move(network),
.seneschal_server_proxy = std::move(server_proxy),
.runtime_dir = std::move(runtime_dir),
.log_path = std::move(log_path),
.stateful_device = std::move(stateful_device),
.stateful_size = static_cast<uint64_t>(std::move(stateful_size)),
.features = features,
.vm_permission_service_proxy = vm_permission_service_proxy_,
.bus = bus_,
.id = vm_id,
.classification = classification,
.storage_ballooning = storage_ballooning,
.vm_builder = std::move(vm_builder),
.socket = std::move(socket)});
if (!vm) {
LOG(ERROR) << "Unable to start VM";
startup_listener_.RemovePendingVm(vsock_cid);
response.set_failure_reason("Unable to start VM");
return response;
}
// Wait for the VM to finish starting up and for maitre'd to signal that it's
// ready.
// TODO(b/338085116) Remove Borealis special case when we fix swap creation.
base::TimeDelta timeout = (classification == apps::VmType::BOREALIS)
? kBorealisVmStartupDefaultTimeout
: kVmStartupDefaultTimeout;
if (request.timeout() != 0) {
timeout = base::Seconds(request.timeout());
}
VmStartChecker::Status vm_start_checker_status =
vm_start_checker->Wait(timeout);
if (vm_start_checker_status != VmStartChecker::Status::READY) {
LOG(ERROR) << "Error starting VM. VmStartCheckerStatus="
<< vm_start_checker_status;
response.set_failure_reason("Error starting VM. VmStartCheckerStatus=" +
std::to_string(vm_start_checker_status));
return response;
}
// maitre'd is ready. Finish setting up the VM.
if (!vm->ConfigureNetwork(nameservers_, search_domains_)) {
LOG(ERROR) << "Failed to configure VM network";
response.set_failure_reason("Failed to configure VM network");
return response;
}
// Attempt to set the timezone of the VM correctly. Incorrect timezone does
// not introduce issues to turnup process. Timezone can also be set during
// runtime upon host's update.
std::string error;
if (!vm->SetTimezone(GetHostTimeZone(), &error)) {
LOG(WARNING) << "Failed to set VM timezone: " << error;
}
// Do all the mounts.
for (const auto& disk : request.disks()) {
std::string src = base::StringPrintf("/dev/vd%c", disk_letter++);
if (!disk.do_mount())
continue;
uint64_t flags = disk.flags();
if (!disk.writable()) {
flags |= MS_RDONLY;
}
if (!vm->Mount(std::move(src), disk.mount_point(), disk.fstype(), flags,
disk.data())) {
LOG(ERROR) << "Failed to mount " << disk.path() << " -> "
<< disk.mount_point();
response.set_failure_reason("Failed to mount extra disk");
return response;
}
}
// Mount the 9p server.
if (!vm->Mount9P(seneschal_server_port, "/mnt/shared")) {
LOG(ERROR) << "Failed to mount shared directory";
response.set_failure_reason("Failed to mount shared directory");
return response;
}
// Determine the VM token. Termina doesnt use a VM token because it has
// per-container tokens.
std::string vm_token = "";
if (!request.start_termina())
vm_token = base::Uuid::GenerateRandomV4().AsLowercaseString();
// Notify cicerone that we have started a VM.
// We must notify cicerone now before calling StartTermina, but we will only
// send the VmStartedSignal on success.
NotifyCiceroneOfVmStarted(vm_id, vm->cid(), vm->pid(), vm_token,
classification);
vm_tools::StartTerminaResponse::MountResult mount_result =
vm_tools::StartTerminaResponse::UNKNOWN;
int64_t free_bytes = -1;
if (request.start_termina() &&
!StartTermina(vm.get(), request.features(), &failure_reason,
&mount_result, &free_bytes)) {
response.set_failure_reason(std::move(failure_reason));
response.set_mount_result((StartVmResponse::MountResult)mount_result);
return response;
}
response.set_mount_result((StartVmResponse::MountResult)mount_result);
if (free_bytes >= 0) {
response.set_free_bytes(free_bytes);
response.set_free_bytes_has_value(true);
}
if (!vm_token.empty() &&
!vm->ConfigureContainerGuest(vm_token, request.vm_username(),
&failure_reason)) {
failure_reason =
"Failed to configure the container guest: " + failure_reason;
// TODO(b/162562622): This request is temporarily non-fatal. Once we are
// satisfied that the maitred changes have been completed, we will make this
// failure fatal.
LOG(WARNING) << failure_reason;
}
LOG(INFO) << "Started VM with pid " << vm->pid();
// Mount an extra disk in the VM. We mount them after calling StartTermina
// because /mnt/external is set up there.
if (vm_start_image_fds.storage_fd.has_value()) {
const std::string external_disk_path =
base::StringPrintf("/dev/vd%c", disk_letter++);
// To support multiple extra disks in the future easily, we use integers for
// names of mount points. Since we support only one extra disk for now,
// |target_dir| is always "0".
if (!vm->MountExternalDisk(std::move(external_disk_path),
/* target_dir= */ "0")) {
LOG(ERROR) << "Failed to mount " << external_disk_path;
response.set_failure_reason("Failed to mount extra disk");
return response;
}
}
vms_[vm_id] = std::move(vm);
// While VmStartedSignal is delayed, the return of StartVM does not wait for
// the control socket to avoid a delay in boot time. Ref: b:316491142.
HandleControlSocketReady(vm_id);
response.set_success(true);
response.set_status(request.start_termina() ? VM_STATUS_STARTING
: VM_STATUS_RUNNING);
*response.mutable_vm_info() = ToVmInfo(vms_[vm_id]->GetInfo(), true);
return response;
}
void Service::StopVm(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<StopVmResponse>>
response_cb,
const StopVmRequest& request) {
ASYNC_SERVICE_METHOD();
StopVmResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
if (!StopVmInternal(vm_id, STOP_VM_REQUESTED)) {
LOG(ERROR) << "Unable to shut down VM";
response.set_failure_reason("Unable to shut down VM");
} else {
response.set_success(true);
}
response_cb->Return(response);
return;
}
void Service::StopVmWithoutOwnerId(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<StopVmResponse>>
response_cb,
const StopVmRequest& request) {
ASYNC_SERVICE_METHOD();
StopVmResponse response;
if (request.name().empty()) {
response_cb->Return(response);
return;
}
std::vector<VmId> vms_to_stop;
for (const auto& [vm_id, _] : vms_) {
if (vm_id.name() == request.name()) {
vms_to_stop.push_back(vm_id);
}
}
for (const auto& vm_to_stop : vms_to_stop) {
if (!StopVmInternal(vm_to_stop, STOP_VM_REQUESTED)) {
LOG(ERROR) << "Unable to shut down VM";
response.set_failure_reason("Unable to shut down VM");
response_cb->Return(response);
return;
}
}
response.set_success(true);
response_cb->Return(response);
return;
}
bool Service::StopVmInternal(const VmId& vm_id, VmStopReason reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
// This is not an error to Chrome
return true;
}
std::unique_ptr<VmBaseImpl>& vm = iter->second;
VmBaseImpl::Info info = vm->GetInfo();
// Notify that we are about to stop a VM.
NotifyVmStopping(vm_id, info.cid);
{
metrics::DurationRecorder duration_recorder(
raw_ref<MetricsLibraryInterface>::from_ptr(metrics_.get()), info.type,
metrics::DurationRecorder::Event::kVmStop);
if (!vm->Shutdown()) {
return false;
}
}
// Notify that we have stopped a VM.
NotifyVmStopped(vm_id, info.cid, reason);
vms_.erase(iter);
return true;
}
void Service::StopVmInternalAsTask(VmId vm_id, VmStopReason reason) {
StopVmInternal(vm_id, reason);
}
// Wrapper to destroy VM in another thread
class VMDelegate : public base::PlatformThread::Delegate {
public:
explicit VMDelegate(VmBaseImpl* vm = nullptr) : vm_(vm) {}
~VMDelegate() override = default;
void ThreadMain() override { vm_->Shutdown(); }
private:
VmBaseImpl* vm_;
};
void Service::StopAllVms(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<>> response_cb) {
ASYNC_SERVICE_METHOD();
StopAllVmsImpl(STOP_ALL_VMS_REQUESTED);
response_cb->Return();
}
void Service::StopAllVmsImpl(VmStopReason reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
is_shutting_down_ = true;
struct ThreadContext {
base::PlatformThreadHandle handle;
VMDelegate delegate;
};
std::vector<ThreadContext> ctxs(vms_.size());
// Spawn a thread for each VM to shut it down.
int i = 0;
for (auto& vm : vms_) {
ThreadContext& ctx = ctxs[i++];
const VmId& id = vm.first;
VmBaseImpl* vm_base_impl = vm.second.get();
VmBaseImpl::Info info = vm_base_impl->GetInfo();
// Notify that we are about to stop a VM.
NotifyVmStopping(id, info.cid);
// The VM will be destructred in the new thread, stopping it normally (and
// then forcibly) it if it hasn't stopped yet.
//
// Would you just take a lambda function? Why do we need the Delegate?...
// It's safe to pass a pointer to |metrics_| to another thread because
// |metrics_| uses AsynchronousMetricsWriter, which is thread-safe.
ctx.delegate = VMDelegate(vm_base_impl);
base::PlatformThread::Create(0, &ctx.delegate, &ctx.handle);
}
i = 0;
for (auto& vm : vms_) {
ThreadContext& ctx = ctxs[i++];
base::PlatformThread::Join(ctx.handle);
const VmId& id = vm.first;
VmBaseImpl* vm_base_impl = vm.second.get();
VmBaseImpl::Info info = vm_base_impl->GetInfo();
// Notify that we have stopped a VM.
NotifyVmStopped(id, info.cid, reason);
}
vms_.clear();
if (!ctxs.empty()) {
LOG(INFO) << "Stopped all Vms";
}
}
void Service::SuspendVm(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<SuspendVmResponse>>
response_cb,
const SuspendVmRequest& request) {
ASYNC_SERVICE_METHOD();
SuspendVmResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << request.name() << " does not exist";
// This is not an error to Chrome
response.set_success(true);
response_cb->Return(response);
return;
}
auto& vm = iter->second;
if (!vm->UsesExternalSuspendSignals()) {
LOG(ERROR) << "Received D-Bus suspend request for " << iter->first
<< " but it does not use external suspend signals.";
response.set_failure_reason(
"VM does not support external suspend signals.");
response_cb->Return(response);
return;
}
vm->Suspend();
response.set_success(true);
response_cb->Return(response);
return;
}
void Service::ResumeVm(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<ResumeVmResponse>>
response_cb,
const ResumeVmRequest& request) {
ASYNC_SERVICE_METHOD();
ResumeVmResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
// This is not an error to Chrome
response.set_success(true);
response_cb->Return(response);
return;
}
auto& vm = iter->second;
if (!vm->UsesExternalSuspendSignals()) {
LOG(ERROR) << "Received D-Bus resume request for " << iter->first
<< " but it does not use external suspend signals.";
response.set_failure_reason(
"VM does not support external suspend signals.");
response_cb->Return(response);
return;
}
vm->Resume();
std::string failure_reason;
if (vm->SetTime(&failure_reason)) {
LOG(INFO) << "Successfully set VM clock in " << iter->first << ".";
} else {
LOG(ERROR) << "Failed to set VM clock in " << iter->first << ": "
<< failure_reason;
}
vm->SetResolvConfig(nameservers_, search_domains_);
response.set_success(true);
response_cb->Return(response);
return;
}
void Service::GetVmInfo(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<GetVmInfoResponse>>
response_cb,
const GetVmInfoRequest& request) {
ASYNC_SERVICE_METHOD();
GetVmInfoResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response_cb->Return(response);
return;
}
*response.mutable_vm_info() = ToVmInfo(iter->second->GetInfo(), true);
response.set_success(true);
response_cb->Return(response);
return;
}
void Service::GetVmEnterpriseReportingInfo(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
GetVmEnterpriseReportingInfoResponse>> response_cb,
const GetVmEnterpriseReportingInfoRequest& request) {
ASYNC_SERVICE_METHOD();
GetVmEnterpriseReportingInfoResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_failure_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
// failure_reason and success will be set by GetVmEnterpriseReportingInfo.
if (!iter->second->GetVmEnterpriseReportingInfo(&response)) {
LOG(ERROR) << "Failed to get VM enterprise reporting info";
}
response_cb->Return(response);
return;
}
void Service::SetBalloonTimer(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
SetBalloonTimerResponse>> response_cb,
const SetBalloonTimerRequest& request) {
ASYNC_SERVICE_METHOD();
SetBalloonTimerResponse response;
if (request.timer_interval_millis() == 0) {
LOG(INFO) << "timer_interval_millis is 0. Stop the timer.";
balloon_resizing_timer_.Stop();
} else if (BalloonTimerShouldRun()) {
LOG(INFO) << "Update balloon timer interval as "
<< request.timer_interval_millis() << "ms.";
balloon_resizing_timer_.Start(
FROM_HERE, base::Milliseconds(request.timer_interval_millis()), this,
&Service::RunBalloonPolicy);
} else {
LOG(WARNING) << "SetBalloonTimer request received but the balloon timer "
"should not be "
"running. Defaulting to a disabled balloon timer.";
}
response.set_success(true);
response_cb->Return(response);
return;
}
void Service::AdjustVm(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<AdjustVmResponse>>
response_cb,
const AdjustVmRequest& request) {
ASYNC_SERVICE_METHOD();
AdjustVmResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
StorageLocation location;
if (!CheckVmExists(vm_id, nullptr, &location)) {
response.set_failure_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
std::vector<std::string> params(request.params().begin(),
request.params().end());
std::string failure_reason;
bool success = false;
if (request.operation() == "pvm.shared-profile") {
if (location != STORAGE_CRYPTOHOME_PLUGINVM) {
failure_reason = "Operation is not supported for the VM";
} else {
success = pvm::helper::ToggleSharedProfile(
bus_, vmplugin_service_proxy_,
VmId(request.owner_id(), request.name()), std::move(params),
&failure_reason);
}
} else if (request.operation() == "memsize") {
if (params.size() != 1) {
failure_reason = "Incorrect number of arguments for 'memsize' operation";
} else if (location != STORAGE_CRYPTOHOME_PLUGINVM) {
failure_reason = "Operation is not supported for the VM";
} else {
success =
pvm::helper::SetMemorySize(bus_, vmplugin_service_proxy_,
VmId(request.owner_id(), request.name()),
std::move(params), &failure_reason);
}
} else if (request.operation() == "rename") {
if (params.size() != 1) {
failure_reason = "Incorrect number of arguments for 'rename' operation";
} else if (params[0].empty()) {
failure_reason = "New name can not be empty";
} else {
VmId new_id(request.owner_id(), params[0]);
if (CheckVmExists(new_id)) {
failure_reason = "VM with such name already exists";
} else if (location != STORAGE_CRYPTOHOME_PLUGINVM) {
failure_reason = "Operation is not supported for the VM";
} else {
success = RenamePluginVm(vm_id, new_id, &failure_reason);
}
}
} else {
failure_reason = "Unrecognized operation";
}
response.set_success(success);
response.set_failure_reason(failure_reason);
response_cb->Return(response);
return;
}
void Service::SyncVmTimes(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
vm_tools::concierge::SyncVmTimesResponse>> response_cb) {
ASYNC_SERVICE_METHOD();
SyncVmTimesResponse response;
int failures = 0;
int requests = 0;
for (auto& vm_entry : vms_) {
requests++;
std::string failure_reason;
if (!vm_entry.second->SetTime(&failure_reason)) {
failures++;
response.add_failure_reason(std::move(failure_reason));
}
}
response.set_requests(requests);
response.set_failures(failures);
response_cb->Return(response);
return;
}
bool Service::StartTermina(TerminaVm* vm,
const google::protobuf::RepeatedField<int>& features,
std::string* failure_reason,
vm_tools::StartTerminaResponse::MountResult* result,
int64_t* out_free_bytes) {
LOG(INFO) << "Starting Termina-specific services";
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(result);
std::string error;
vm_tools::StartTerminaResponse response;
if (!vm->StartTermina(vm->ContainerCIDRAddress().ToString(), features, &error,
&response)) {
failure_reason->assign(error);
return false;
}
if (response.mount_result() ==
vm_tools::StartTerminaResponse::PARTIAL_DATA_LOSS) {
LOG(ERROR) << "Possible data loss from filesystem corruption detected";
}
*result = response.mount_result();
if (response.free_bytes_has_value()) {
*out_free_bytes = response.free_bytes();
}
return true;
}
// Executes a command on the specified disk path. Returns false when
// `GetAppOutputWithExitCode()` fails (i.e., the command could not be launched
// or does not exit cleanly). Otherwise returns true and sets |exit_code|.
bool ExecuteCommandOnDisk(const base::FilePath& disk_path,
const std::string& executable_path,
const std::vector<std::string>& opts,
int* exit_code) {
std::vector<std::string> args = {executable_path, disk_path.value()};
args.insert(args.end(), opts.begin(), opts.end());
std::string output;
return base::GetAppOutputWithExitCode(base::CommandLine(args), &output,
exit_code);
}
// Generates a file path that is a distinct sibling of the specified path and
// does not contain the equal sign '='.
base::FilePath GenerateTempFilePathWithNoEqualSign(const base::FilePath& path) {
std::string temp_name;
base::RemoveChars(path.BaseName().value(), "=", &temp_name);
return path.DirName().Append(temp_name + ".tmp");
}
// Creates a filesystem at the specified file/path.
bool CreateFilesystem(const base::FilePath& disk_location,
enum FilesystemType filesystem_type,
const std::vector<std::string>& mkfs_opts,
const std::vector<std::string>& tune2fs_opts) {
std::string filesystem_string;
switch (filesystem_type) {
case FilesystemType::EXT4:
filesystem_string = "ext4";
break;
case FilesystemType::UNSPECIFIED:
default:
LOG(ERROR) << "Filesystem was not specified";
return false;
}
std::string existing_filesystem = GetFilesystem(disk_location);
if (!existing_filesystem.empty() &&
existing_filesystem != filesystem_string) {
LOG(ERROR) << "Filesystem already exists but is the wrong type, expected:"
<< filesystem_string << ", got:" << existing_filesystem;
return false;
}
if (existing_filesystem == filesystem_string) {
return true;
}
LOG(INFO) << "Creating " << filesystem_string << " filesystem at "
<< disk_location;
int exit_code = -1;
ExecuteCommandOnDisk(disk_location, "/sbin/mkfs." + filesystem_string,
mkfs_opts, &exit_code);
if (exit_code != 0) {
LOG(ERROR) << "Can't format '" << disk_location << "' as "
<< filesystem_string << ", exit status: " << exit_code;
return false;
}
if (tune2fs_opts.empty()) {
return true;
}
LOG(INFO) << "Adjusting ext4 filesystem at " << disk_location
<< " with tune2fs";
// Currently, tune2fs cannot handle paths containing '=' (b/267134417).
// To avoid the issue, below we temporarily rename the disk image so that it
// does not contain '=', apply tune2fs to the renamed path, and then rename
// the disk image back to its original name.
// TODO(b/267134417): Remove this workaround once tune2fs is fixed.
const base::FilePath temp_disk_location =
GenerateTempFilePathWithNoEqualSign(disk_location);
if (!base::Move(disk_location, temp_disk_location)) {
LOG(ERROR) << "Failed to move " << disk_location << " to "
<< temp_disk_location;
unlink(temp_disk_location.value().c_str());
return false;
}
exit_code = -1;
ExecuteCommandOnDisk(temp_disk_location, "/sbin/tune2fs", tune2fs_opts,
&exit_code);
// Move the disk image back to the original location before checking the exit
// code. This is to make the behavior on tune2fs failures aligh with that on
// mkfs failures (the disk image exists in the original location).
// Note that the disk image is removed if the move (rename) operation fails,
// but it should be much rarer than mkfs/tune2fs failures.
if (!base::Move(temp_disk_location, disk_location)) {
LOG(ERROR) << "Failed to move " << temp_disk_location << " back to "
<< disk_location;
unlink(temp_disk_location.value().c_str());
return false;
}
if (exit_code != 0) {
LOG(ERROR) << "Can't adjust '" << disk_location
<< "' with tune2fs, exit status: " << exit_code;
return false;
}
return true;
}
void Service::CreateDiskImage(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
CreateDiskImageResponse>> response_cb,
const CreateDiskImageRequest& request,
const std::vector<base::ScopedFD>& file_handles) {
ASYNC_SERVICE_METHOD();
CreateDiskImageResponse response;
base::ScopedFD in_fd;
if (request.storage_location() == STORAGE_CRYPTOHOME_PLUGINVM) {
if (file_handles.size() == 0) {
LOG(ERROR) << "CreateDiskImage: no fd found";
response.set_failure_reason("no source fd found");
response_cb->Return(response);
return;
}
in_fd.reset(dup(file_handles[0].get()));
}
response_cb->Return(CreateDiskImageInternal(request, std::move(in_fd)));
return;
}
CreateDiskImageResponse Service::CreateDiskImageInternal(
CreateDiskImageRequest request, base::ScopedFD in_fd) {
CreateDiskImageResponse response;
VmId vm_id(request.cryptohome_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response.set_status(DISK_STATUS_FAILED);
return response;
}
// Set up the disk image as a sparse file when
// 1) |allocation_type| is DISK_ALLOCATION_TYPE_SPARSE, or
// 2) |allocation_type| is DISK_ALLOCATION_TYPE_AUTO (the default value) and
// |disk_size| is 0.
// The latter case exists to preserve the old behaviors for existing callers.
if (request.allocation_type() ==
DiskImageAllocationType::DISK_ALLOCATION_TYPE_AUTO) {
LOG(WARNING) << "Disk allocation type is unspecified (or specified as "
"auto). Whether to create a sparse disk image will be "
"automatically determined using the requested disk size.";
}
bool is_sparse = request.allocation_type() ==
DiskImageAllocationType::DISK_ALLOCATION_TYPE_SPARSE ||
(request.allocation_type() ==
DiskImageAllocationType::DISK_ALLOCATION_TYPE_AUTO &&
request.disk_size() == 0);
if (!is_sparse && request.disk_size() == 0) {
response.set_failure_reason(
"Request is invalid, disk size must be non-zero for non-sparse disks");
return response;
}
if (!is_sparse && request.storage_ballooning()) {
response.set_failure_reason(
"Request is invalid, storage ballooning is only available for sparse "
"disks");
return response;
}
base::FilePath disk_path;
StorageLocation disk_location;
if (CheckVmExists(vm_id, &disk_path, &disk_location)) {
if (disk_location != request.storage_location()) {
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason(
"VM/disk with same name already exists in another storage location");
return response;
}
if (disk_location == STORAGE_CRYPTOHOME_PLUGINVM) {
// We do not support extending Plugin VM images.
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Plugin VM with such name already exists");
return response;
}
LOG(INFO) << "Found existing disk at " << disk_path.value();
response.set_status(DISK_STATUS_EXISTS);
response.set_disk_path(disk_path.value());
return response;
}
if (!GetDiskPathFromName(vm_id, request.storage_location(), &disk_path,
request.image_type())) {
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Failed to create vm image");
return response;
}
if (request.storage_location() == STORAGE_CRYPTOHOME_PLUGINVM) {
// Make sure we have the FD to fill with disk image data.
if (!in_fd.is_valid()) {
LOG(ERROR) << "CreateDiskImage: fd is not valid";
response.set_failure_reason("fd is not valid");
}
// Get the name of directory for ISO images. Do not create it - it will be
// created by the PluginVmCreateOperation code.
base::FilePath iso_dir;
if (!GetPluginIsoDirectory(vm_id, false /* create */, &iso_dir)) {
LOG(ERROR) << "Unable to determine directory for ISOs";
response.set_failure_reason("Unable to determine ISO directory");
return response;
}
std::vector<std::string> params(
std::make_move_iterator(request.mutable_params()->begin()),
std::make_move_iterator(request.mutable_params()->end()));
std::unique_ptr<PluginVmCreateOperation> op =
PluginVmCreateOperation::Create(
std::move(in_fd), iso_dir, request.source_size(),
VmId(request.cryptohome_id(), request.vm_name()),
std::move(params));
response.set_disk_path(disk_path.value());
response.set_status(op->status());
response.set_command_uuid(op->uuid());
response.set_failure_reason(op->failure_reason());
if (op->status() == DISK_STATUS_IN_PROGRESS) {
std::string uuid = op->uuid();
disk_image_ops_.emplace_back(std::move(op));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&Service::RunDiskImageOperation,
weak_ptr_factory_.GetWeakPtr(), std::move(uuid)));
}
return response;
}
uint64_t disk_size = request.disk_size()
? request.disk_size()
: CalculateDesiredDiskSize(
disk_path, 0, request.storage_ballooning());
if (request.image_type() == DISK_IMAGE_QCOW2) {
LOG(ERROR) << "Creating qcow2 disk images is unsupported";
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Creating qcow2 disk images is unsupported");
return response;
}
if (request.image_type() == DISK_IMAGE_RAW ||
request.image_type() == DISK_IMAGE_AUTO) {
LOG(INFO) << "Creating raw disk at: " << disk_path.value() << " size "
<< disk_size;
base::ScopedFD fd(
open(disk_path.value().c_str(), O_CREAT | O_NONBLOCK | O_WRONLY, 0600));
if (!fd.is_valid()) {
PLOG(ERROR) << "Failed to create raw disk";
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Failed to create raw disk file");
return response;
}
if (!is_sparse) {
LOG(INFO) << "Creating user-chosen-size raw disk image";
if (!SetPreallocatedWithUserChosenSizeAttr(fd)) {
PLOG(ERROR) << "Failed to set user_chosen_size xattr";
unlink(disk_path.value().c_str());
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Failed to set user_chosen_size xattr");
return response;
}
LOG(INFO) << "Preallocating user-chosen-size raw disk image";
if (fallocate(fd.get(), 0, 0, disk_size) != 0) {
PLOG(ERROR) << "Failed to allocate raw disk";
unlink(disk_path.value().c_str());
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Failed to allocate raw disk file");
return response;
}
LOG(INFO) << "Disk image preallocated";
response.set_status(DISK_STATUS_CREATED);
response.set_disk_path(disk_path.value());
} else {
LOG(INFO) << "Creating sparse raw disk image";
int ret = ftruncate(fd.get(), disk_size);
if (ret != 0) {
PLOG(ERROR) << "Failed to truncate raw disk";
unlink(disk_path.value().c_str());
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Failed to truncate raw disk file");
return response;
}
LOG(INFO) << "Sparse raw disk image created";
response.set_status(DISK_STATUS_CREATED);
response.set_disk_path(disk_path.value());
}
if (request.filesystem_type() == FilesystemType::UNSPECIFIED) {
// Skip creating a filesystem when no filesystem type is specified.
return response;
}
// Create a filesystem on the disk to make it usable for the VM.
std::vector<std::string> mkfs_opts(
std::make_move_iterator(request.mutable_mkfs_opts()->begin()),
std::make_move_iterator(request.mutable_mkfs_opts()->end()));
if (mkfs_opts.empty()) {
// Set the default options.
mkfs_opts = kExtMkfsOpts;
}
// -q is added to silence the output.
mkfs_opts.push_back("-q");
const std::vector<std::string> tune2fs_opts(
std::make_move_iterator(request.mutable_tune2fs_opts()->begin()),
std::make_move_iterator(request.mutable_tune2fs_opts()->end()));
if (!CreateFilesystem(disk_path, request.filesystem_type(), mkfs_opts,
tune2fs_opts)) {
PLOG(ERROR) << "Failed to create filesystem";
unlink(disk_path.value().c_str());
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Failed to create filesystem");
}
return response;
}
LOG(ERROR) << "Unknown image_type in CreateDiskImage: "
<< request.image_type();
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Unknown image_type");
return response;
}
void Service::DestroyDiskImage(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
DestroyDiskImageResponse>> response_cb,
const DestroyDiskImageRequest& request) {
ASYNC_SERVICE_METHOD();
DestroyDiskImageResponse response;
VmId vm_id(request.cryptohome_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response.set_status(DISK_STATUS_FAILED);
response_cb->Return(response);
return;
}
// Stop the associated VM if it is still running.
auto iter = FindVm(vm_id);
if (iter != vms_.end()) {
LOG(INFO) << "Shutting down VM " << request.vm_name();
if (!StopVmInternal(vm_id, DESTROY_DISK_IMAGE_REQUESTED)) {
LOG(ERROR) << "Unable to shut down VM";
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Unable to shut down VM");
response_cb->Return(response);
return;
}
}
// Delete shader cache best-effort. Shadercached is only distributed to boards
// if borealis enabled. There is no way to check VM type easily unless we turn
// it up.
// TODO(endlesspring): Deal with errors once we distribute to all boards.
auto _ = PurgeShaderCache(vm_id, bus_, shadercached_proxy_);
base::FilePath disk_path;
StorageLocation location;
if (!CheckVmExists(vm_id, &disk_path, &location)) {
response.set_status(DISK_STATUS_DOES_NOT_EXIST);
response.set_failure_reason("No such image");
response_cb->Return(response);
return;
}
if (!EraseGuestSshKeys(vm_id)) {
// Don't return a failure here, just log an error because this is only a
// side effect and not what the real request is about.
LOG(ERROR) << "Failed removing guest SSH keys for VM " << request.vm_name();
}
if (location == STORAGE_CRYPTOHOME_PLUGINVM) {
// Plugin VMs need to be unregistered before we can delete them.
bool registered;
if (!pvm::dispatcher::IsVmRegistered(bus_, vmplugin_service_proxy_, vm_id,
&registered)) {
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason(
"failed to check Plugin VM registration status");
response_cb->Return(response);
return;
}
if (registered &&
!pvm::dispatcher::UnregisterVm(bus_, vmplugin_service_proxy_, vm_id)) {
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("failed to unregister Plugin VM");
response_cb->Return(response);
return;
}
base::FilePath iso_dir;
if (GetPluginIsoDirectory(vm_id, false /* create */, &iso_dir) &&
base::PathExists(iso_dir) && !base::DeletePathRecursively(iso_dir)) {
LOG(ERROR) << "Unable to remove ISO directory for " << vm_id.name();
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Unable to remove ISO directory");
response_cb->Return(response);
return;
}
// Delete GPU shader disk cache.
base::FilePath gpu_cache_path = GetVmGpuCachePathInternal(vm_id);
if (!base::DeletePathRecursively(gpu_cache_path)) {
LOG(ERROR) << "Failed to remove GPU cache for VM: " << gpu_cache_path;
}
}
bool delete_result = (location == STORAGE_CRYPTOHOME_PLUGINVM)
? base::DeletePathRecursively(disk_path)
: base::DeleteFile(disk_path);
if (!delete_result) {
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Disk removal failed");
response_cb->Return(response);
return;
}
// Pflash may not be present for all VMs. We should only report error if it
// exists and we failed to delete it. The |DeleteFile| API handles the
// non-existing case as a success.
std::optional<PflashMetadata> pflash_metadata = GetPflashMetadata(vm_id);
if (pflash_metadata && pflash_metadata->is_installed) {
if (!base::DeleteFile(pflash_metadata->path)) {
response.set_status(DISK_STATUS_FAILED);
response.set_failure_reason("Pflash removal failed");
response_cb->Return(response);
return;
}
}
response.set_status(DISK_STATUS_DESTROYED);
response_cb->Return(response);
return;
}
void Service::ResizeDiskImage(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
ResizeDiskImageResponse>> response_cb,
const ResizeDiskImageRequest& request) {
ASYNC_SERVICE_METHOD();
ResizeDiskImageResponse response;
VmId vm_id(request.cryptohome_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response.set_status(DISK_STATUS_FAILED);
response_cb->Return(response);
return;
}
base::FilePath disk_path;
StorageLocation location;
if (!CheckVmExists(vm_id, &disk_path, &location)) {
response.set_status(DISK_STATUS_DOES_NOT_EXIST);
response.set_failure_reason("Resize image doesn't exist");
response_cb->Return(response);
return;
}
auto size = request.disk_size() & kDiskSizeMask;
if (size != request.disk_size()) {
LOG(INFO) << "Rounded requested disk size from " << request.disk_size()
<< " to " << size;
}
auto op = VmResizeOperation::Create(
vm_id, location, disk_path, size,
base::BindOnce(&Service::ResizeDisk, weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&Service::ProcessResize,
weak_ptr_factory_.GetWeakPtr()));
response.set_status(op->status());
response.set_command_uuid(op->uuid());
response.set_failure_reason(op->failure_reason());
if (op->status() == DISK_STATUS_IN_PROGRESS) {
std::string uuid = op->uuid();
disk_image_ops_.emplace_back(std::move(op));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&Service::RunDiskImageOperation,
weak_ptr_factory_.GetWeakPtr(), std::move(uuid)));
} else if (op->status() == DISK_STATUS_RESIZED) {
DiskImageStatusEnum status = DISK_STATUS_RESIZED;
std::string failure_reason;
FinishResize(vm_id, location, &status, &failure_reason);
if (status != DISK_STATUS_RESIZED) {
response.set_status(status);
response.set_failure_reason(failure_reason);
}
}
response_cb->Return(response);
return;
}
void Service::ResizeDisk(const VmId& vm_id,
StorageLocation location,
uint64_t new_size,
DiskImageStatusEnum* status,
std::string* failure_reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Unable to find VM " << vm_id.name();
*failure_reason = "No such image";
*status = DISK_STATUS_DOES_NOT_EXIST;
return;
}
*status = iter->second->ResizeDisk(new_size, failure_reason);
}
void Service::ProcessResize(const VmId& vm_id,
StorageLocation location,
uint64_t target_size,
DiskImageStatusEnum* status,
std::string* failure_reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Unable to find VM " << vm_id.name();
*failure_reason = "No such image";
*status = DISK_STATUS_DOES_NOT_EXIST;
return;
}
*status = iter->second->GetDiskResizeStatus(failure_reason);
if (*status == DISK_STATUS_RESIZED) {
FinishResize(vm_id, location, status, failure_reason);
}
}
void Service::FinishResize(const VmId& vm_id,
StorageLocation location,
DiskImageStatusEnum* status,
std::string* failure_reason) {
base::FilePath disk_path;
if (!GetDiskPathFromName(vm_id, location, &disk_path)) {
LOG(ERROR) << "Failed to get disk path after resize";
*failure_reason = "Failed to get disk path after resize";
*status = DISK_STATUS_FAILED;
return;
}
base::ScopedFD fd(
open(disk_path.value().c_str(), O_CREAT | O_NONBLOCK | O_WRONLY, 0600));
if (!fd.is_valid()) {
PLOG(ERROR) << "Failed to open disk image";
*failure_reason = "Failed to open disk image";
*status = DISK_STATUS_FAILED;
return;
}
// This disk now has a user-chosen size by virtue of being resized.
if (!SetPreallocatedWithUserChosenSizeAttr(fd)) {
LOG(ERROR) << "Failed to set user-chosen size xattr";
*failure_reason = "Failed to set user-chosen size xattr";
*status = DISK_STATUS_FAILED;
return;
}
}
void Service::ExportDiskImage(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
ExportDiskImageResponse>> response_cb,
const ExportDiskImageRequest& request,
const std::vector<base::ScopedFD>& fds) {
ASYNC_SERVICE_METHOD();
ExportDiskImageResponse response;
response.set_status(DISK_STATUS_FAILED);
if (fds.size() == 0) {
LOG(ERROR) << "Need 1 or 2 fds";
response.set_failure_reason("Need 1 or 2 fds");
response_cb->Return(response);
return;
}
// Get the FD to fill with disk image data.
base::ScopedFD storage_fd{dup(fds[0].get())};
base::ScopedFD digest_fd;
if (request.generate_sha256_digest()) {
if (fds.size() != 2) {
LOG(ERROR) << "export: no digest fd found";
response.set_failure_reason("export: no digest fd found");
response_cb->Return(response);
return;
}
digest_fd.reset(dup(fds[1].get()));
}
response_cb->Return(ExportDiskImageInternal(
std::move(request), std::move(storage_fd), std::move(digest_fd)));
return;
}
ExportDiskImageResponse Service::ExportDiskImageInternal(
ExportDiskImageRequest request,
base::ScopedFD storage_fd,
base::ScopedFD digest_fd) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ExportDiskImageResponse response;
response.set_status(DISK_STATUS_FAILED);
VmId vm_id(request.cryptohome_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response.set_status(DISK_STATUS_FAILED);
return response;
}
base::FilePath disk_path;
StorageLocation location;
if (!CheckVmExists(vm_id, &disk_path, &location)) {
response.set_status(DISK_STATUS_DOES_NOT_EXIST);
response.set_failure_reason("Export image doesn't exist");
return response;
}
ArchiveFormat fmt;
switch (location) {
case STORAGE_CRYPTOHOME_ROOT:
fmt = ArchiveFormat::TAR_GZ;
break;
case STORAGE_CRYPTOHOME_PLUGINVM:
fmt = ArchiveFormat::ZIP;
break;
default:
LOG(ERROR) << "Unsupported location for source image";
response.set_failure_reason("Unsupported location for image");
return response;
}
if (!request.force()) {
if (FindVm(vm_id) != vms_.end()) {
LOG(ERROR) << "VM " << request.vm_name() << " is currently running";
response.set_failure_reason("VM is currently running");
return response;
}
// For Parallels VMs we want to be sure that the VM is shut down, not
// merely suspended, to have consistent export.
if (location == STORAGE_CRYPTOHOME_PLUGINVM) {
bool is_shut_down;
if (!pvm::dispatcher::IsVmShutDown(bus_, vmplugin_service_proxy_, vm_id,
&is_shut_down)) {
LOG(ERROR) << "Unable to query VM state";
response.set_failure_reason("Unable to query VM state");
return response;
}
if (!is_shut_down) {
LOG(ERROR) << "VM is not shut down";
response.set_failure_reason("VM needs to be shut down for exporting");
return response;
}
}
}
auto op = VmExportOperation::Create(vm_id, disk_path, std::move(storage_fd),
std::move(digest_fd), fmt);
response.set_status(op->status());
response.set_command_uuid(op->uuid());
response.set_failure_reason(op->failure_reason());
if (op->status() == DISK_STATUS_IN_PROGRESS) {
std::string uuid = op->uuid();
disk_image_ops_.emplace_back(std::move(op));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&Service::RunDiskImageOperation,
weak_ptr_factory_.GetWeakPtr(), std::move(uuid)));
}
return response;
}
void Service::ImportDiskImage(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
ImportDiskImageResponse>> response_cb,
const ImportDiskImageRequest& request,
const base::ScopedFD& in_fd) {
ASYNC_SERVICE_METHOD();
ImportDiskImageResponse response;
response.set_status(DISK_STATUS_FAILED);
VmId vm_id(request.cryptohome_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
if (CheckVmExists(vm_id)) {
response.set_status(DISK_STATUS_EXISTS);
response.set_failure_reason("VM/disk with such name already exists");
response_cb->Return(response);
return;
}
if (request.storage_location() != STORAGE_CRYPTOHOME_PLUGINVM) {
LOG(ERROR)
<< "Locations other than STORAGE_CRYPTOHOME_PLUGINVM are not supported";
response.set_failure_reason("Unsupported location for image");
response_cb->Return(response);
return;
}
base::FilePath disk_path;
if (!GetDiskPathFromName(vm_id, request.storage_location(), &disk_path)) {
response.set_failure_reason("Failed to set up vm image name");
response_cb->Return(response);
return;
}
auto op = PluginVmImportOperation::Create(
base::ScopedFD(dup(in_fd.get())), disk_path, request.source_size(), vm_id,
bus_, vmplugin_service_proxy_);
response.set_status(op->status());
response.set_command_uuid(op->uuid());
response.set_failure_reason(op->failure_reason());
if (op->status() == DISK_STATUS_IN_PROGRESS) {
std::string uuid = op->uuid();
disk_image_ops_.emplace_back(std::move(op));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&Service::RunDiskImageOperation,
weak_ptr_factory_.GetWeakPtr(), std::move(uuid)));
}
response_cb->Return(response);
return;
}
void Service::RunDiskImageOperation(std::string uuid) {
auto iter =
std::find_if(disk_image_ops_.begin(), disk_image_ops_.end(),
[&uuid](auto& info) { return info.op->uuid() == uuid; });
if (iter == disk_image_ops_.end()) {
LOG(ERROR) << "RunDiskImageOperation called with unknown uuid";
return;
}
if (iter->canceled) {
// Operation was cancelled. Now that our posted task is running we can
// remove it from the list and not reschedule ourselves.
disk_image_ops_.erase(iter);
return;
}
auto op = iter->op.get();
op->Run(kDefaultIoLimit);
if (base::TimeTicks::Now() - iter->last_report_time > kDiskOpReportInterval ||
op->status() != DISK_STATUS_IN_PROGRESS) {
LOG(INFO) << "Disk Image Operation: UUID=" << uuid
<< " progress: " << op->GetProgress()
<< " status: " << op->status();
// Send the D-Bus signal out updating progress of the operation.
DiskImageStatusResponse status;
FormatDiskImageStatus(op, &status);
concierge_adaptor_->SendDiskImageProgressSignal(status);
// Note the time we sent out the notification.
iter->last_report_time = base::TimeTicks::Now();
}
if (op->status() == DISK_STATUS_IN_PROGRESS) {
// Reschedule ourselves so we can execute next chunk of work.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&Service::RunDiskImageOperation,
weak_ptr_factory_.GetWeakPtr(), std::move(uuid)));
}
}
void Service::DiskImageStatus(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
DiskImageStatusResponse>> response_cb,
const DiskImageStatusRequest& request) {
ASYNC_SERVICE_METHOD();
DiskImageStatusResponse response;
response.set_status(DISK_STATUS_FAILED);
// Locate the pending command in the list.
auto iter = std::find_if(disk_image_ops_.begin(), disk_image_ops_.end(),
[&request](auto& info) {
return info.op->uuid() == request.command_uuid();
});
if (iter == disk_image_ops_.end() || iter->canceled) {
LOG(ERROR) << "Unknown command uuid in DiskImageStatusRequest";
response.set_failure_reason("Unknown command uuid");
response_cb->Return(response);
return;
}
auto op = iter->op.get();
FormatDiskImageStatus(op, &response);
// Erase operation form the list if it is no longer in progress.
if (op->status() != DISK_STATUS_IN_PROGRESS) {
disk_image_ops_.erase(iter);
}
response_cb->Return(response);
return;
}
void Service::CancelDiskImageOperation(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
CancelDiskImageResponse>> response_cb,
const CancelDiskImageRequest& request) {
ASYNC_SERVICE_METHOD();
CancelDiskImageResponse response;
// Locate the pending command in the list.
auto iter = std::find_if(disk_image_ops_.begin(), disk_image_ops_.end(),
[&request](auto& info) {
return info.op->uuid() == request.command_uuid();
});
if (iter == disk_image_ops_.end()) {
LOG(ERROR) << "Unknown command uuid in CancelDiskImageRequest";
response.set_failure_reason("Unknown command uuid");
response_cb->Return(response);
return;
}
auto op = iter->op.get();
if (op->status() != DISK_STATUS_IN_PROGRESS) {
response.set_failure_reason("Command is no longer in progress");
response_cb->Return(response);
return;
}
// Mark the operation as canceled. We can't erase it from the list right
// away as there is a task posted for it. The task will erase this operation
// when it gets to run.
iter->canceled = true;
response.set_success(true);
response_cb->Return(response);
}
void Service::ListVmDisks(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<ListVmDisksResponse>>
response_cb,
const ListVmDisksRequest& request) {
ASYNC_SERVICE_METHOD();
ListVmDisksResponse response;
if (!CheckVmNameAndOwner(request, response, true /* Empty VmName allowed*/)) {
response_cb->Return(response);
return;
}
response.set_success(true);
response.set_total_size(0);
for (int location = StorageLocation_MIN; location <= StorageLocation_MAX;
location++) {
if (request.all_locations() || location == request.storage_location()) {
if (!ListVmDisksInLocation(request.cryptohome_id(),
static_cast<StorageLocation>(location),
request.vm_name(), &response)) {
break;
}
}
}
response_cb->Return(response);
}
void Service::AttachNetDevice(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
AttachNetDeviceResponse>> response_cb,
const AttachNetDeviceRequest& request) {
ASYNC_SERVICE_METHOD();
AttachNetDeviceResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
response.set_failure_reason("Requested VM " + vm_id.name() +
" with owner " + vm_id.owner_id() +
" does not exist");
LOG(ERROR) << response.failure_reason();
response_cb->Return(response);
return;
}
uint8_t out_bus;
if (!iter->second->AttachNetDevice(request.tap_name(), &out_bus)) {
response.set_failure_reason(
"Failed to attach tap device due to crosvm error.");
LOG(ERROR) << response.failure_reason();
response_cb->Return(response);
return;
}
response.set_success(true);
response.set_guest_bus(out_bus);
response_cb->Return(response);
}
void Service::DetachNetDevice(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
DetachNetDeviceResponse>> response_cb,
const DetachNetDeviceRequest& request) {
ASYNC_SERVICE_METHOD();
DetachNetDeviceResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
response.set_failure_reason("Requested VM " + vm_id.name() +
" with owner " + vm_id.owner_id() +
" does not exist");
LOG(ERROR) << response.failure_reason();
response_cb->Return(response);
return;
}
if (request.guest_bus() == 0 || request.guest_bus() > 0xFF) {
response.set_failure_reason("PCI bus number " +
std::to_string(request.guest_bus()) +
" is out of valid range 1-255");
LOG(ERROR) << response.failure_reason();
response_cb->Return(response);
return;
}
if (!iter->second->DetachNetDevice(request.guest_bus())) {
response.set_failure_reason(
"Failed to detach tap device due to crosvm error.");
LOG(ERROR) << response.failure_reason();
response_cb->Return(response);
return;
}
response.set_success(true);
response_cb->Return(response);
}
void Service::AttachUsbDevice(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
AttachUsbDeviceResponse>> response_cb,
const AttachUsbDeviceRequest& request,
const base::ScopedFD& fd) {
ASYNC_SERVICE_METHOD();
AttachUsbDeviceResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
if (request.bus_number() > 0xFF) {
LOG(ERROR) << "Bus number out of valid range " << request.bus_number();
response.set_reason("Invalid bus number");
response_cb->Return(response);
return;
}
if (request.port_number() > 0xFF) {
LOG(ERROR) << "Port number out of valid range " << request.port_number();
response.set_reason("Invalid port number");
response_cb->Return(response);
return;
}
if (request.vendor_id() > 0xFFFF) {
LOG(ERROR) << "Vendor ID out of valid range " << request.vendor_id();
response.set_reason("Invalid vendor ID");
response_cb->Return(response);
return;
}
if (request.product_id() > 0xFFFF) {
LOG(ERROR) << "Product ID out of valid range " << request.product_id();
response.set_reason("Invalid product ID");
response_cb->Return(response);
return;
}
uint8_t guest_port{};
if (!iter->second->AttachUsbDevice(
request.bus_number(), request.port_number(), request.vendor_id(),
request.product_id(), fd.get(), &guest_port)) {
LOG(ERROR) << "Failed to attach USB device.";
response.set_reason("Error from crosvm");
response_cb->Return(response);
return;
}
response.set_success(true);
response.set_guest_port(guest_port);
response_cb->Return(response);
}
void Service::DetachUsbDevice(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
DetachUsbDeviceResponse>> response_cb,
const DetachUsbDeviceRequest& request) {
ASYNC_SERVICE_METHOD();
DetachUsbDeviceResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
if (request.guest_port() > 0xFF) {
LOG(ERROR) << "Guest port number out of valid range "
<< request.guest_port();
response.set_reason("Invalid guest port number");
response_cb->Return(response);
return;
}
if (!iter->second->DetachUsbDevice(request.guest_port())) {
LOG(ERROR) << "Failed to detach USB device";
response_cb->Return(response);
return;
}
response.set_success(true);
response_cb->Return(response);
}
void Service::AttachKey(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<AttachKeyResponse>>
response_cb,
const AttachKeyRequest& request,
const base::ScopedFD& hidraw) {
ASYNC_SERVICE_METHOD();
AttachKeyResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
uint8_t guest_port{};
// TODO(b/333838456): refactor virtualization metrics in a single module
std::string metric_name = base::StrCat(
{"Virtualization.", apps::VmType_Name(iter->second->GetInfo().type), ".",
"SecurityKeyAttach"});
if (!iter->second->AttachKey(hidraw.get(), &guest_port)) {
LOG(ERROR) << "Failed to attach security key.";
response.set_reason("Error from crosvm");
response_cb->Return(response);
metrics_->SendBoolToUMA(metric_name, false);
return;
}
metrics_->SendBoolToUMA(metric_name, true);
response.set_success(true);
response.set_guest_port(guest_port);
response_cb->Return(response);
}
void Service::ListUsbDevices(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
ListUsbDeviceResponse>> response_cb,
const ListUsbDeviceRequest& request) {
ASYNC_SERVICE_METHOD();
ListUsbDeviceResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response_cb->Return(response);
return;
}
std::vector<UsbDeviceEntry> usb_list;
if (!iter->second->ListUsbDevice(&usb_list)) {
LOG(ERROR) << "Failed to list USB devices";
response_cb->Return(response);
return;
}
for (auto usb : usb_list) {
UsbDeviceMessage* usb_proto = response.add_usb_devices();
usb_proto->set_guest_port(usb.port);
usb_proto->set_vendor_id(usb.vendor_id);
usb_proto->set_product_id(usb.product_id);
}
response.set_success(true);
response_cb->Return(response);
}
DnsSettings Service::ComposeDnsResponse() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DnsSettings dns_settings;
for (const auto& server : nameservers_) {
dns_settings.add_nameservers(server);
}
for (const auto& domain : search_domains_) {
dns_settings.add_search_domains(domain);
}
return dns_settings;
}
void Service::GetDnsSettings(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<DnsSettings>>
response_cb) {
ASYNC_SERVICE_METHOD();
response_cb->Return(ComposeDnsResponse());
}
void Service::SetVmCpuRestriction(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
SetVmCpuRestrictionResponse>> response_cb,
const SetVmCpuRestrictionRequest& request) {
ASYNC_SERVICE_METHOD();
// TODO(yusukes,hashimoto): Instead of allowing Chrome to decide when to
// restrict each VM's CPU usage, let Concierge itself do that for potentially
// better security. See crrev.com/c/3564880 for more context.
SetVmCpuRestrictionResponse response;
bool success = false;
const CpuRestrictionState state = request.cpu_restriction_state();
switch (request.cpu_cgroup()) {
case CPU_CGROUP_TERMINA:
success = TerminaVm::SetVmCpuRestriction(state);
break;
case CPU_CGROUP_PLUGINVM:
success = PluginVm::SetVmCpuRestriction(state);
break;
case CPU_CGROUP_ARCVM:
success = ArcVm::SetVmCpuRestriction(state, GetCpuQuota());
break;
default:
LOG(ERROR) << "Unknown cpu_group";
break;
}
response.set_success(success);
response_cb->Return(response);
}
void Service::ListVms(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<ListVmsResponse>>
response_cb,
const ListVmsRequest& request) {
ASYNC_SERVICE_METHOD();
ListVmsResponse response;
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
for (const auto& vm_entry : vms_) {
const auto& id = vm_entry.first;
const auto& vm = vm_entry.second;
if (id.owner_id() != request.owner_id()) {
continue;
}
VmBaseImpl::Info info = vm->GetInfo();
// The vms_ member only contains VMs with running crosvm instances. So the
// STOPPED case below should not be possible.
DCHECK(info.status != VmBaseImpl::Status::STOPPED);
ExtendedVmInfo* proto = response.add_vms();
proto->set_name(id.name());
proto->set_owner_id(id.owner_id());
*proto->mutable_vm_info() = ToVmInfo(info, false);
proto->set_status(ToVmStatus(info.status));
}
response.set_success(true);
response_cb->Return(response);
}
void Service::ReclaimVmMemory(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
ReclaimVmMemoryResponse>> response_cb,
const ReclaimVmMemoryRequest& request) {
ASYNC_SERVICE_METHOD();
ReclaimVmMemoryResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_failure_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
const pid_t pid = iter->second->GetInfo().pid;
const auto page_limit = request.page_limit();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&ReclaimVmMemoryInternal, pid, page_limit),
base::BindOnce(
[](std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
ReclaimVmMemoryResponse>> response_sender,
ReclaimVmMemoryResponse response) {
std::move(response_sender)->Return(response);
},
std::move(response_cb)));
}
using AggressiveBalloonResponder = std::unique_ptr<
brillo::dbus_utils::DBusMethodResponse<AggressiveBalloonResponse>>;
void Service::AggressiveBalloon(AggressiveBalloonResponder response_cb,
const AggressiveBalloonRequest& request) {
ASYNC_SERVICE_METHOD();
AggressiveBalloonResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_failure_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
auto type = iter->second->GetInfo().type;
if (vm_memory_management_service_ &&
mm::MmService::ManagedVms().contains(type)) {
auto cid = iter->second->GetInfo().cid;
if (request.enable()) {
LOG(INFO) << "Starting Aggressive Baloon for CID: " << cid;
auto cb = base::BindPostTaskToCurrentDefault(base::BindOnce(
&Service::OnAggressiveBalloonFinished, weak_ptr_factory_.GetWeakPtr(),
std::move(response_cb), cid));
vm_memory_management_service_->ReclaimUntilBlocked(
cid, mm::ResizePriority::kAggressiveBalloon, std::move(cb));
} else {
LOG(INFO) << "Stopping Aggressive Baloon for CID: " << cid;
vm_memory_management_service_->StopReclaimUntilBlocked(cid);
response.set_success(true);
response_cb->Return(response);
}
} else {
if (request.enable()) {
iter->second->InflateAggressiveBalloon(base::BindOnce(
[](AggressiveBalloonResponder response_sender,
AggressiveBalloonResponse response) {
std::move(response_sender)->Return(response);
},
std::move(response_cb)));
} else {
iter->second->StopAggressiveBalloon(response);
response_cb->Return(response);
}
}
}
void Service::OnAggressiveBalloonFinished(
AggressiveBalloonResponder response_sender,
int cid,
bool success,
const char* err_msg) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LOG(INFO) << "Aggressive Balloon finished for VM: " << cid;
// After aggressive balloon finishes, clear the blockers on the ARCVM balloon
// so cached apps aren't immediately killed when they become cached.
if (vm_memory_management_service_) {
LOG(INFO) << "Clearing balloon blockers for VM: " << cid;
vm_memory_management_service_->ClearBlockersUpToInclusive(
cid, mm::ResizePriority::kAggressiveBalloon);
}
AggressiveBalloonResponse response;
response.set_success(success);
if (!success) {
response.set_failure_reason(err_msg);
}
std::move(response_sender)->Return(response);
}
void Service::GetVmMemoryManagementKillsConnection(
GetVmmmsKillsConnectionResponseSender response_cb,
const GetVmMemoryManagementKillsConnectionRequest& in_request) {
ASYNC_SERVICE_METHOD();
if (get_vmmms_kills_connection_response_sender_) {
GetVmMemoryManagementKillsConnectionResponse response;
std::vector<base::ScopedFD> fds;
static constexpr char error[] =
"Overlapping requests, cancelling earlier request";
LOG(ERROR) << error;
response.set_failure_reason(error);
auto local = std::move(get_vmmms_kills_connection_response_sender_);
local->Return(response, fds);
}
get_vmmms_kills_connection_response_sender_ = std::move(response_cb);
if (vmmms_init_done_) {
SendGetVmmmsKillConnectionResponse();
}
}
void Service::SendGetVmmmsKillConnectionResponse() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
GetVmMemoryManagementKillsConnectionResponse response;
std::vector<base::ScopedFD> fds;
if (vm_memory_management_service_) {
auto fd = vm_memory_management_service_->GetKillsServerConnection();
if (fd.is_valid()) {
fds.push_back(std::move(fd));
} else {
static constexpr char error[] = "Failed to connect.";
LOG(ERROR) << error;
response.set_failure_reason(error);
}
} else {
static constexpr char error[] = "Service is not enabled.";
LOG(ERROR) << error;
response.set_failure_reason(error);
}
response.set_success(!fds.empty());
// The timeout that the host (resourced) should use when waiting on a kill
// decision response from VMMMS.
static constexpr base::TimeDelta kVmMemoryManagementHostKillDecisionTimeout =
base::Milliseconds(300);
response.set_host_kill_request_timeout_ms(
kVmMemoryManagementHostKillDecisionTimeout.InMilliseconds());
auto local = std::move(get_vmmms_kills_connection_response_sender_);
local->Return(response, fds);
}
void Service::OnResolvConfigChanged(std::vector<std::string> nameservers,
std::vector<std::string> search_domains) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (nameservers_ == nameservers && search_domains_ == search_domains) {
return;
}
nameservers_ = std::move(nameservers);
search_domains_ = std::move(search_domains);
for (auto& vm_entry : vms_) {
auto& vm = vm_entry.second;
if (vm->IsSuspended()) {
// The VM is currently suspended and will not respond to RPCs.
// SetResolvConfig() will be called when the VM resumes.
continue;
}
vm->SetResolvConfig(nameservers_, search_domains_);
}
// Broadcast DnsSettingsChanged signal so Plugin VM dispatcher is aware as
// well.
concierge_adaptor_->SendDnsSettingsChangedSignal(ComposeDnsResponse());
}
void Service::OnDefaultNetworkServiceChanged() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (auto& vm_entry : vms_) {
auto& vm = vm_entry.second;
if (vm->IsSuspended()) {
continue;
}
vm->HostNetworkChanged();
}
}
void Service::NotifyCiceroneOfVmStarted(const VmId& vm_id,
uint32_t cid,
pid_t pid,
std::string vm_token,
vm_tools::apps::VmType vm_type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface,
vm_tools::cicerone::kNotifyVmStartedMethod);
dbus::MessageWriter writer(&method_call);
vm_tools::cicerone::NotifyVmStartedRequest request;
request.set_owner_id(vm_id.owner_id());
request.set_vm_name(vm_id.name());
request.set_cid(cid);
request.set_vm_token(std::move(vm_token));
request.set_pid(pid);
// TODO(b/320329723): Add special handling to cicerone if needed
request.set_vm_type(vm_type);
writer.AppendProtoAsArrayOfBytes(request);
if (!CallDBusMethod(bus_, cicerone_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)) {
LOG(ERROR) << "Failed notifying cicerone of VM startup";
}
}
void Service::HandleControlSocketReady(const VmId& vm_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto path = base::FilePath(vms_[vm_id]->GetVmSocketPath());
// Initialize the watcher before we check if the path exists
// to avoid racing with the socket being created.
vm_socket_ready_watchers_.emplace(std::piecewise_construct,
std::make_tuple(vm_id), std::make_tuple());
if (!vm_socket_ready_watchers_[vm_id].Watch(
path, base::FilePathWatcher::Type::kNonRecursive,
base::BindRepeating(&Service::OnControlSocketChange,
weak_ptr_factory_.GetWeakPtr(), vm_id))) {
PLOG(ERROR) << "Failed to initialize file watcher " << vm_id;
vm_socket_ready_watchers_.erase(vm_id);
}
if (base::PathExists(path)) {
OnControlSocketReady(vm_id);
}
}
void Service::OnControlSocketChange(const VmId& vm_id,
const base::FilePath&,
bool error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "VM " << vm_id.name() << " stopped prematurely";
vm_socket_ready_watchers_.erase(vm_id);
return;
}
if (error) {
LOG(ERROR) << "Control socket watcher error " << vm_id;
vm_socket_ready_watchers_.erase(vm_id);
}
if (base::PathExists(base::FilePath(iter->second->GetVmSocketPath()))) {
OnControlSocketReady(vm_id);
}
}
void Service::OnControlSocketReady(const VmId& vm_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
vm_socket_ready_watchers_.erase(vm_id);
std::unique_ptr<VmBaseImpl>& vm = vms_[vm_id];
VmBaseImpl::Info info = vm->GetInfo();
if (vm_memory_management_service_) {
vm_memory_management_service_->NotifyVmStarted(info.type, info.cid,
vm->GetVmSocketPath());
}
if (BalloonTimerShouldRun() && !balloon_resizing_timer_.IsRunning()) {
LOG(INFO) << "New VM. Starting balloon resize timer.";
balloon_resizing_timer_.Start(FROM_HERE, base::Seconds(1), this,
&Service::RunBalloonPolicy);
}
SendVmStartedSignal(vm_id, info);
}
void Service::SendVmStartedSignal(const VmId& vm_id,
const VmBaseImpl::Info& vm_info) {
vm_tools::concierge::VmStartedSignal proto;
proto.set_owner_id(vm_id.owner_id());
proto.set_name(vm_id.name());
*proto.mutable_vm_info() = ToVmInfo(vm_info, false);
proto.set_status(ToVmStatus(vm_info.status));
concierge_adaptor_->SendVmStartedSignalSignal(proto);
}
void Service::SendVmStartingUpSignal(const VmId& vm_id,
apps::VmType vm_type,
uint64_t cid) {
vm_tools::concierge::VmStartingUpSignal proto;
proto.set_owner_id(vm_id.owner_id());
proto.set_name(vm_id.name());
proto.set_vm_type(ToLegacyVmType(vm_type));
proto.set_cid(cid);
concierge_adaptor_->SendVmStartingUpSignalSignal(proto);
}
void Service::SendVmGuestUserlandReadySignal(
const VmId& vm_id, const vm_tools::concierge::GuestUserlandReady ready) {
vm_tools::concierge::VmGuestUserlandReadySignal proto;
proto.set_owner_id(vm_id.owner_id());
proto.set_name(vm_id.name());
proto.set_ready(ready);
concierge_adaptor_->SendVmGuestUserlandReadySignalSignal(proto);
}
void Service::NotifyVmStopping(const VmId& vm_id, int64_t cid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (vm_memory_management_service_) {
vm_memory_management_service_->NotifyVmStopping(cid);
}
// Notify cicerone.
dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface,
vm_tools::cicerone::kNotifyVmStoppingMethod);
dbus::MessageWriter writer(&method_call);
vm_tools::cicerone::NotifyVmStoppingRequest request;
request.set_owner_id(vm_id.owner_id());
request.set_vm_name(vm_id.name());
writer.AppendProtoAsArrayOfBytes(request);
if (!CallDBusMethod(bus_, cicerone_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)) {
LOG(ERROR) << "Failed notifying cicerone of stopping VM";
}
// Send the D-Bus signal out to notify everyone that we are stopping a VM.
vm_tools::concierge::VmStoppingSignal proto;
proto.set_owner_id(vm_id.owner_id());
proto.set_name(vm_id.name());
proto.set_cid(cid);
concierge_adaptor_->SendVmStoppingSignalSignal(proto);
}
void Service::NotifyVmStopped(const VmId& vm_id,
int64_t cid,
VmStopReason reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Note: In the case of a VM crash, NotifyVmStopped is called without a
// proceeding NotifyVmStopping(). In this case, the
// vm_memory_management_service should still be informed that the VM has
// stopped. Multiple NotifyVmStopping calls for the same VM are supported.
if (vm_memory_management_service_) {
vm_memory_management_service_->NotifyVmStopping(cid);
}
// Notify cicerone.
dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface,
vm_tools::cicerone::kNotifyVmStoppedMethod);
dbus::MessageWriter writer(&method_call);
vm_tools::cicerone::NotifyVmStoppedRequest request;
request.set_owner_id(vm_id.owner_id());
request.set_vm_name(vm_id.name());
writer.AppendProtoAsArrayOfBytes(request);
if (!CallDBusMethod(bus_, cicerone_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)) {
LOG(ERROR) << "Failed notifying cicerone of VM stopped";
}
// Send the D-Bus signal out to notify everyone that we have stopped a VM.
vm_tools::concierge::VmStoppedSignal proto;
proto.set_owner_id(vm_id.owner_id());
proto.set_name(vm_id.name());
proto.set_cid(cid);
proto.set_reason(reason);
concierge_adaptor_->SendVmStoppedSignalSignal(proto);
}
std::string Service::GetContainerToken(const VmId& vm_id,
const std::string& container_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface,
vm_tools::cicerone::kGetContainerTokenMethod);
dbus::MessageWriter writer(&method_call);
vm_tools::cicerone::ContainerTokenRequest request;
vm_tools::cicerone::ContainerTokenResponse response;
request.set_owner_id(vm_id.owner_id());
request.set_vm_name(vm_id.name());
request.set_container_name(container_name);
writer.AppendProtoAsArrayOfBytes(request);
std::unique_ptr<dbus::Response> dbus_response =
CallDBusMethod(bus_, cicerone_service_proxy_, &method_call,
dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
if (!dbus_response) {
LOG(ERROR) << "Failed getting container token from cicerone";
return "";
}
dbus::MessageReader reader(dbus_response.get());
if (!reader.PopArrayOfBytesAsProto(&response)) {
LOG(ERROR) << "Failed parsing proto response";
return "";
}
return response.container_token();
}
std::string Service::GetHostTimeZone() {
base::FilePath system_timezone;
// Timezone is set by creating a symlink to an existing file at
// /usr/share/zoneinfo.
if (!base::NormalizeFilePath(base::FilePath(kLocaltimePath),
&system_timezone)) {
LOG(ERROR) << "Failed to get system timezone";
return "";
}
base::FilePath zoneinfo(kZoneInfoPath);
base::FilePath system_timezone_name;
if (!zoneinfo.AppendRelativePath(system_timezone, &system_timezone_name)) {
LOG(ERROR) << "Could not get name of timezone " << system_timezone.value();
return "";
}
return system_timezone_name.value();
}
void Service::OnLocaltimeFileChanged(const base::FilePath& path, bool error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
LOG(WARNING) << "Error while reading system timezone change";
return;
}
LOG(INFO) << "System timezone changed, updating VM timezones";
std::string timezone = GetHostTimeZone();
for (auto& vm_entry : vms_) {
auto& vm = vm_entry.second;
std::string error_msg;
if (!vm->SetTimezone(timezone, &error_msg)) {
LOG(WARNING) << "Failed to set timezone for " << vm_entry.first.name()
<< ": " << error_msg;
}
}
}
void Service::OnTremplinStartedSignal(dbus::Signal* signal) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(signal->GetInterface(), vm_tools::cicerone::kVmCiceroneInterface);
DCHECK_EQ(signal->GetMember(), vm_tools::cicerone::kTremplinStartedSignal);
vm_tools::cicerone::TremplinStartedSignal tremplin_started_signal;
dbus::MessageReader reader(signal);
if (!reader.PopArrayOfBytesAsProto(&tremplin_started_signal)) {
LOG(ERROR) << "Failed to parse TremplinStartedSignal from DBus Signal";
return;
}
VmId vm_id(tremplin_started_signal.owner_id(),
tremplin_started_signal.vm_name());
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Received signal from an unknown VM " << vm_id.name();
return;
}
LOG(INFO) << "Received request: " << __func__ << " for " << iter->first;
iter->second->SetTremplinStarted();
}
void Service::OnVmToolsStateChangedSignal(dbus::Signal* signal) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::string owner_id, vm_name;
bool running;
if (!pvm::dispatcher::ParseVmToolsChangedSignal(signal, &owner_id, &vm_name,
&running)) {
return;
}
VmId vm_id(owner_id, vm_name);
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Received signal from an unknown VM " << vm_id.name();
return;
}
LOG(INFO) << "Received request: " << __func__ << " for " << iter->first;
iter->second->VmToolsStateChanged(running);
}
void Service::OnSignalConnected(const std::string& interface_name,
const std::string& signal_name,
bool is_connected) {
if (!is_connected) {
LOG(ERROR) << "Failed to connect to interface name: " << interface_name
<< " for signal " << signal_name;
} else {
LOG(INFO) << "Connected to interface name: " << interface_name
<< " for signal " << signal_name;
}
if (interface_name == vm_tools::cicerone::kVmCiceroneInterface) {
DCHECK_EQ(signal_name, vm_tools::cicerone::kTremplinStartedSignal);
is_tremplin_started_signal_connected_ = is_connected;
}
}
void Service::HandleSuspendImminent() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const auto& pair : vms_) {
VMT_TRACE(kCategory, "Service::HandleSuspendImminent::vm", "name",
pair.first.name());
auto& vm = pair.second;
if (vm->UsesExternalSuspendSignals()) {
continue;
}
vm->Suspend();
}
}
void Service::HandleSuspendDone() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const auto& vm_entry : vms_) {
VMT_TRACE(kCategory, "Service::HandleSuspendDone::vm", "name",
vm_entry.first.name());
auto& vm = vm_entry.second;
if (vm->UsesExternalSuspendSignals()) {
continue;
}
vm->Resume();
std::string failure_reason;
if (!vm->SetTime(&failure_reason)) {
LOG(ERROR) << "Failed to set VM clock in " << vm_entry.first << ": "
<< failure_reason;
}
vm->SetResolvConfig(nameservers_, search_domains_);
}
}
Service::VmMap::iterator Service::FindVm(const VmId& vm_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return vms_.find(vm_id);
}
// TODO(b/244486983): move this functionality to shadercached
Service::VMGpuCacheSpec Service::PrepareVmGpuCachePaths(
const VmId& vm_id, bool enable_render_server, bool enable_foz_db_list) {
// We want to delete and recreate the cache directory atomically, and in order
// to do that we ensure that this method runs on the main thread always.
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::FilePath cache_path = GetVmGpuCachePathInternal(vm_id);
// Cache ID is either boot id or OS build hash
base::FilePath cache_id_path = cache_path.DirName();
base::FilePath base_path = cache_id_path.DirName();
base::FilePath cache_device_path = cache_path.Append("device");
base::FilePath cache_render_server_path =
enable_render_server ? cache_path.Append("render_server")
: base::FilePath();
base::FilePath foz_db_list_file =
enable_render_server ? cache_render_server_path.Append("foz_db_list.txt")
: base::FilePath();
const base::FilePath* cache_subdir_paths[] = {&cache_device_path,
&cache_render_server_path};
const base::FilePath* permissions_to_update[] = {
&base_path, &cache_id_path, &cache_path, &cache_device_path,
&cache_render_server_path};
// In order to always provide an empty GPU shader cache on each boot or
// build id change, we hash the boot_id or build number, and erase the whole
// GPU cache if a directory matching the current boot id or build number hash
// is not found.
// For example:
// VM cache dir: /run/daemon-store/crosvm/<uid>/gpucache/<cacheid>/<vmid>/
// Cache ID dir: /run/daemon-store/crosvm/<uid>/gpucache/<cacheid>/
// Base dir: /run/daemon-store/crosvm/<uid>/gpucache/
// If Cache ID dir exists we know another VM has already created a fresh base
// dir during this boot or OS release. Otherwise, we erase Base dir to wipe
// out any previous Cache ID dir.
if (!base::DirectoryExists(cache_id_path)) {
LOG(INFO) << "GPU cache dir not found, deleting base directory";
if (!base::DeletePathRecursively(base_path)) {
LOG(WARNING) << "Failed to delete gpu cache directory: " << base_path
<< " shader caching will be disabled.";
return VMGpuCacheSpec{};
}
}
for (const base::FilePath* path : cache_subdir_paths) {
if (path->empty()) {
continue;
}
if (!base::DirectoryExists(*path)) {
base::File::Error dir_error;
if (!base::CreateDirectoryAndGetError(*path, &dir_error)) {
LOG(WARNING) << "Failed to create crosvm gpu cache directory in "
<< *path << ": " << base::File::ErrorToString(dir_error);
base::DeletePathRecursively(cache_path);
return VMGpuCacheSpec{};
}
}
}
for (const base::FilePath* path : permissions_to_update) {
if (base::IsLink(*path)) {
continue;
}
// Group rx permission needed for VM shader cache management by shadercached
if (!base::SetPosixFilePermissions(*path, 0750)) {
LOG(WARNING) << "Failed to set directory permissions for " << *path;
}
}
if (!foz_db_list_file.empty()) {
bool file_exists = base::PathExists(foz_db_list_file);
if (enable_foz_db_list) {
// Initiate foz db file, if it already exists, continue using it
if (!file_exists) {
if (base::WriteFile(foz_db_list_file, "", 0) != 0) {
LOG(WARNING) << "Failed to create foz db list file";
return VMGpuCacheSpec{};
}
}
if (!base::SetPosixFilePermissions(foz_db_list_file, 0774)) {
LOG(WARNING) << "Failed to set file permissions for "
<< foz_db_list_file;
return VMGpuCacheSpec{};
}
} else if (file_exists) {
LOG(WARNING) << "Dynamic GPU RO cache loading is disabled but the "
"feature management file exists";
}
}
return VMGpuCacheSpec{.device = std::move(cache_device_path),
.render_server = std::move(cache_render_server_path),
.foz_db_list = std::move(foz_db_list_file)};
}
void AddGroupPermissionChildren(const base::FilePath& path) {
auto enumerator = base::FileEnumerator(
path, true,
base::FileEnumerator::DIRECTORIES ^ base::FileEnumerator::SHOW_SYM_LINKS);
for (base::FilePath child_path = enumerator.Next(); !child_path.empty();
child_path = enumerator.Next()) {
if (child_path == path) {
// Do not change permission for the root path
continue;
}
int permission;
if (!base::GetPosixFilePermissions(child_path, &permission)) {
LOG(WARNING) << "Failed to get permission for " << path.value();
} else if (!base::SetPosixFilePermissions(
child_path, permission | base::FILE_PERMISSION_GROUP_MASK)) {
LOG(WARNING) << "Failed to change permission for " << child_path.value();
}
}
}
void Service::AddGroupPermissionMesa(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<>> response_cb,
const AddGroupPermissionMesaRequest& request) {
ASYNC_SERVICE_METHOD();
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request,
request /* in place of a response proto */)) {
response_cb->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED,
"Empty or malformed owner ID / VM name");
return;
}
base::FilePath cache_path = GetVmGpuCachePathInternal(vm_id);
AddGroupPermissionChildren(cache_path);
response_cb->Return();
}
void Service::GetVmLaunchAllowed(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
GetVmLaunchAllowedResponse>> response_cb,
const GetVmLaunchAllowedRequest& request) {
ASYNC_SERVICE_METHOD();
std::string reason;
bool allowed = untrusted_vm_utils_.SafeToRunVirtualMachines(&reason);
if (allowed) {
LOG(INFO) << "VM launch allowed";
} else {
LOG(INFO) << "VM launch not allowed: " << reason;
}
GetVmLaunchAllowedResponse response;
response.set_allowed(allowed);
response.set_reason(reason);
response_cb->Return(response);
}
void Service::GetVmLogs(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<GetVmLogsResponse>>
response_cb,
const GetVmLogsRequest& request) {
ASYNC_SERVICE_METHOD();
GetVmLogsResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED,
"Empty or malformed owner ID / VM name");
return;
}
base::FilePath log_path = GetVmLogPath(vm_id, kCrosvmLogFileExt);
std::vector<base::FilePath> paths;
int64_t remaining_log_space = kMaxGetVmLogsSize;
if (base::PathExists(log_path)) {
int64_t size;
bool ok = base::GetFileSize(log_path, &size);
if (!ok) {
response_cb->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED, "Failed to get log size");
return;
}
remaining_log_space -= size;
paths.push_back(log_path);
for (int i = 1; i <= 5; i++) {
base::FilePath older_log_path =
log_path.AddExtension(base::NumberToString(i));
// Don't read older logs if the total log size read is above the limit.
if (base::PathExists(older_log_path) && remaining_log_space > 0) {
ok = base::GetFileSize(older_log_path, &size);
if (!ok) {
break;
}
remaining_log_space -= size;
paths.push_back(older_log_path);
} else {
break;
}
}
}
for (auto& path : std::ranges::reverse_view(paths)) {
std::string file_contents;
if (!base::ReadFileToString(path, &file_contents)) {
continue;
}
std::string_view contents_view{file_contents};
// Truncate the earliest log, if it would exceed the log size limit.
if (remaining_log_space < 0) {
contents_view.remove_prefix(-remaining_log_space);
remaining_log_space = 0;
}
response.mutable_log()->append(contents_view);
}
response_cb->Return(response);
}
void Service::SwapVm(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<SwapVmResponse>>
response_cb,
const SwapVmRequest& request) {
ASYNC_SERVICE_METHOD();
SwapVmResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
auto iter = FindVm(vm_id);
if (iter == vms_.end()) {
LOG(ERROR) << "Requested VM " << vm_id.name() << " does not exist";
response.set_failure_reason("Requested VM does not exist");
response_cb->Return(response);
return;
}
iter->second->HandleSwapVmRequest(
request, base::BindOnce(
[](std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
SwapVmResponse>> response_sender,
SwapVmResponse response) {
std::move(response_sender)->Return(response);
},
std::move(response_cb)));
}
void Service::NotifyVmSwapping(const VmId& vm_id,
SwappingState swapping_state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Send the D-Bus signal out to notify everyone that we are swapping a VM.
vm_tools::concierge::VmSwappingSignal proto;
proto.set_owner_id(vm_id.owner_id());
proto.set_name(vm_id.name());
proto.set_state(swapping_state);
concierge_adaptor_->SendVmSwappingSignalSignal(proto);
}
void Service::InstallPflash(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
InstallPflashResponse>> response_cb,
const InstallPflashRequest& request,
const base::ScopedFD& pflash_src_fd) {
ASYNC_SERVICE_METHOD();
InstallPflashResponse response;
VmId vm_id(request.owner_id(), request.vm_name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->Return(response);
return;
}
std::optional<PflashMetadata> pflash_metadata = GetPflashMetadata(vm_id);
if (!pflash_metadata) {
response.set_failure_reason("Failed to get pflash install path");
response_cb->Return(response);
return;
}
// We only allow one Pflash file to be allowed during the lifetime of a VM.
if (pflash_metadata->is_installed) {
response.set_failure_reason("Pflash already installed");
response_cb->Return(response);
return;
}
// No Pflash is installed that means we can associate the given file with the
// VM by copying it to a file derived from the VM's name itself.
base::FilePath pflash_src_path =
base::FilePath(kProcFileDescriptorsPath)
.Append(base::NumberToString(pflash_src_fd.get()));
LOG(INFO) << "Installing Pflash file for VM: " << vm_id.name()
<< " to: " << pflash_metadata->path;
if (!base::CopyFile(pflash_src_path, pflash_metadata->path)) {
response.set_failure_reason("Failed to copy pflash image");
response_cb->Return(response);
return;
}
response.set_success(true);
response_cb->Return(response);
}
// TODO(b/244486983): separate out GPU VM cache methods out of service.cc file
void Service::GetVmGpuCachePath(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<
GetVmGpuCachePathResponse>> response_cb,
const GetVmGpuCachePathRequest& request) {
ASYNC_SERVICE_METHOD();
GetVmGpuCachePathResponse response;
VmId vm_id(request.owner_id(), request.name());
if (!CheckVmNameAndOwner(request, response)) {
response_cb->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED,
"Empty or malformed owner ID / VM name");
return;
}
base::FilePath path = GetVmGpuCachePathInternal(vm_id);
if (!base::DirectoryExists(path)) {
response_cb->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED,
"GPU cache path does not exist");
return;
} else if (path.empty()) {
response_cb->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED, "GPU cache path is empty");
return;
}
response.set_path(path.value());
response_cb->Return(response);
return;
}
int Service::GetCpuQuota() {
const feature::PlatformFeatures::ParamsResult result =
feature::PlatformFeatures::Get()->GetParamsAndEnabledBlocking(
{&kArcVmInitialThrottleFeature});
const auto result_iter = result.find(kArcVmInitialThrottleFeatureName);
if (result_iter == result.end()) {
LOG(ERROR) << "Failed to get params for "
<< kArcVmInitialThrottleFeatureName;
return kCpuPercentUnlimited;
}
const auto& entry = result_iter->second;
if (!entry.enabled) {
return kCpuPercentUnlimited; // cfs_quota feature is disabled.
}
auto quota =
FindIntValue(entry.params, kArcVmInitialThrottleFeatureQuotaParam);
if (!quota) {
return kCpuPercentUnlimited;
}
return std::min(100, std::max(1, *quota));
}
void Service::OnStatefulDiskSpaceUpdate(
const spaced::StatefulDiskSpaceUpdate& update) {
VMT_TRACE(kCategory, "Service::OnStatefulDiskSpaceUpdate");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (auto& iter : vms_) {
iter.second->HandleStatefulUpdate(update);
}
}
bool Service::BalloonTimerShouldRun() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If there are no VMs, there is no need for the balloon timer.
if (vms_.size() == 0) {
return false;
}
// If there are VMs but VmMemoryManagementService has not been initialized,
// the balloon timer should run.
if (!vm_memory_management_service_) {
return true;
}
// If any VM is not managed by the VM Memory Management Service, the balloon
// timer should run.
for (const auto& vm : vms_) {
if (!mm::MmService::ManagedVms().contains(vm.second->GetInfo().type)) {
return true;
}
}
return false;
}
// Sends a message to the Upstart DBUS service, which should be owned by
// init/root, to run the trim_filesystem.conf script
// (see platform2/vm_tools/init/trim_filesystem.conf). The script runs
// fstrim on the user filesystem if lvm is being used.
void Service::TrimUserFilesystem() {
dbus::ObjectProxy* startup_proxy = bus_->GetObjectProxy(
"com.ubuntu.Upstart",
dbus::ObjectPath("/com/ubuntu/Upstart/jobs/trim_5ffilesystem"));
if (!startup_proxy) {
LOG(ERROR) << "Unable to get dbus proxy for Upstart";
return;
}
dbus::MethodCall method_call("com.ubuntu.Upstart0_6.Job", "Start");
dbus::MessageWriter writer(&method_call);
writer.AppendArrayOfStrings({});
writer.AppendBool(true /* wait for response */);
startup_proxy->CallMethodWithErrorResponse(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce([](dbus::Response* response, dbus::ErrorResponse* error) {
if (response) {
LOG(INFO) << "trim_filesystem returned successfully";
} else {
std::string message;
dbus::MessageReader reader(error);
reader.PopString(&message);
LOG(ERROR) << "trim_filesystem failed: " << message;
}
}));
}
void Service::RejectRequestDuringShutdown(
std::unique_ptr<brillo::dbus_utils::DBusMethodResponseBase> response) {
response->ReplyWithError(FROM_HERE, brillo::errors::dbus::kDomain,
DBUS_ERROR_FAILED, "Shutdown in progress");
}
} // namespace vm_tools::concierge