| // Copyright 2017 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "vm_tools/concierge/service.h" |
| |
| #include <arpa/inet.h> |
| #include <fcntl.h> |
| #include <google/protobuf/repeated_field.h> |
| #include <grp.h> |
| #include <linux/capability.h> |
| #include <net/route.h> |
| #include <signal.h> |
| #include <stdint.h> |
| #include <sys/mount.h> |
| #include <sys/prctl.h> |
| #include <sys/sendfile.h> |
| #include <sys/signalfd.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <sys/utsname.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 <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/base64url.h> |
| #include <base/bind.h> |
| #include <base/callback.h> |
| #include <base/callback_helpers.h> |
| #include <base/check.h> |
| #include <base/check_op.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/format_macros.h> |
| #include <base/guid.h> |
| #include <base/hash/md5.h> |
| #include <base/location.h> |
| #include <base/logging.h> |
| #include <base/memory/ref_counted.h> |
| #include <base/memory/ptr_util.h> |
| #include <base/optional.h> |
| #include <base/single_thread_task_runner.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/strings/string_split.h> |
| #include <base/synchronization/waitable_event.h> |
| #include <base/system/sys_info.h> |
| #include <base/threading/thread_task_runner_handle.h> |
| #include <base/time/time.h> |
| #include <base/version.h> |
| #include <brillo/dbus/dbus_proxy_util.h> |
| #include <chromeos/dbus/service_constants.h> |
| #include <crosvm/qcow_utils.h> |
| #include <dbus/object_proxy.h> |
| #include <chromeos/patchpanel/dbus/client.h> |
| #include <vm_cicerone/proto_bindings/cicerone_service.pb.h> |
| #include <vm_concierge/proto_bindings/concierge_service.pb.h> |
| #include <vm_protos/proto_bindings/vm_guest.pb.h> |
| #include <chromeos/constants/vm_tools.h> |
| #include <vboot/crossystem.h> |
| |
| #include "vm_tools/common/naming.h" |
| #include "vm_tools/concierge/arc_vm.h" |
| #include "vm_tools/concierge/dlc_helper.h" |
| #include "vm_tools/concierge/future.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/shared_data.h" |
| #include "vm_tools/concierge/ssh_keys.h" |
| #include "vm_tools/concierge/vm_builder.h" |
| #include "vm_tools/concierge/vm_permission_interface.h" |
| #include "vm_tools/concierge/vmplugin_dispatcher_interface.h" |
| |
| using std::string; |
| |
| namespace vm_tools { |
| namespace concierge { |
| |
| namespace { |
| |
| // Default path to VM kernel image and rootfs. |
| constexpr char kVmDefaultPath[] = "/run/imageloader/cros-termina"; |
| |
| // Name of the VM kernel image. |
| constexpr char kVmKernelName[] = "vm_kernel"; |
| |
| // Name of the VM rootfs image. |
| constexpr char kVmRootfsName[] = "vm_rootfs.img"; |
| |
| // Name of the VM tools image to be mounted at kToolsMountPath. |
| constexpr char kVmToolsDiskName[] = "vm_tools.img"; |
| |
| // Filesystem location to mount VM tools image. |
| constexpr char kToolsMountPath[] = "/opt/google/cros-containers"; |
| |
| // Filesystem type of VM tools image. |
| constexpr char kToolsFsType[] = "ext4"; |
| |
| // 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::TimeDelta::FromSeconds(30); |
| |
| // crosvm log directory name. |
| constexpr char kCrosvmLogDir[] = "log"; |
| |
| // 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 disk size and it should |
| // not be resized. |
| constexpr char kDiskImageUserChosenSizeXattr[] = |
| "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}; |
| |
| // Default name to use for a container. |
| constexpr char kDefaultContainerName[] = "penguin"; |
| |
| // Path to process file descriptors. |
| constexpr char kProcFileDescriptorsPath[] = "/proc/self/fd/"; |
| |
| constexpr uint64_t kMinimumDiskSize = 1ll * 1024 * 1024 * 1024; // 1 GiB |
| 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 = 1024 * 1024; // 1 Mib |
| |
| // How often we should broadcast state of a disk operation (import or export). |
| constexpr base::TimeDelta kDiskOpReportInterval = |
| base::TimeDelta::FromSeconds(15); |
| |
| // The minimum kernel version of the host which supports untrusted VMs or a |
| // trusted VM with nested VM support. |
| constexpr KernelVersionAndMajorRevision |
| kMinKernelVersionForUntrustedAndNestedVM = std::make_pair(4, 19); |
| |
| // The minimum kernel version of the host which supports virtio-pmem. |
| constexpr KernelVersionAndMajorRevision kMinKernelVersionForVirtioPmem = |
| std::make_pair(4, 4); |
| |
| // File path that reports the L1TF vulnerability status. |
| constexpr const char kL1TFFilePath[] = |
| "/sys/devices/system/cpu/vulnerabilities/l1tf"; |
| |
| // File path that reports the MDS vulnerability status. |
| constexpr const char kMDSFilePath[] = |
| "/sys/devices/system/cpu/vulnerabilities/mds"; |
| |
| constexpr gid_t kCrosvmUGid = 299; |
| |
| // Used with the |IsUntrustedVMAllowed| function. |
| struct UntrustedVMCheckResult { |
| UntrustedVMCheckResult(bool untrusted_vm_allowed, bool skip_host_checks) |
| : untrusted_vm_allowed(untrusted_vm_allowed), |
| skip_host_checks(skip_host_checks) {} |
| |
| // Is an untrusted VM allowed on the host. |
| bool untrusted_vm_allowed; |
| |
| // Should checking for security patches on the host be skipped while starting |
| // untrusted VMs. |
| bool skip_host_checks; |
| }; |
| |
| // Passes |method_call| to |handler| and passes the response to |
| // |response_sender|. If |handler| returns NULL, an empty response is created |
| // and sent. |
| void HandleSynchronousDBusMethodCall( |
| base::Callback<std::unique_ptr<dbus::Response>(dbus::MethodCall*)> handler, |
| dbus::MethodCall* method_call, |
| dbus::ExportedObject::ResponseSender response_sender) { |
| std::unique_ptr<dbus::Response> response = handler.Run(method_call); |
| if (!response) |
| response = dbus::Response::FromMethodCall(method_call); |
| std::move(response_sender).Run(std::move(response)); |
| } |
| |
| void HandleAsynchronousDBusMethodCall( |
| base::Callback<void(dbus::MethodCall*, |
| dbus::ExportedObject::ResponseSender)> handler, |
| dbus::MethodCall* method_call, |
| dbus::ExportedObject::ResponseSender response_sender) { |
| handler.Run(method_call, std::move(response_sender)); |
| } |
| |
| // 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(base::Thread* grpc_thread, |
| grpc::Service* listener_impl, |
| const std::string& listener_address, |
| std::shared_ptr<grpc::Server>* server_copy) { |
| // Start the grpc thread. |
| if (!grpc_thread->Start()) { |
| LOG(ERROR) << "Failed to start grpc thread"; |
| return false; |
| } |
| |
| base::WaitableEvent event(base::WaitableEvent::ResetPolicy::AUTOMATIC, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| bool ret = grpc_thread->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&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; |
| } |
| |
| // Converts an IPv4 address to a string. The result will be stored in |str| |
| // on success. |
| bool IPv4AddressToString(const uint32_t address, std::string* str) { |
| CHECK(str); |
| |
| char result[INET_ADDRSTRLEN]; |
| if (inet_ntop(AF_INET, &address, result, sizeof(result)) != result) { |
| return false; |
| } |
| *str = std::string(result); |
| return true; |
| } |
| |
| // Get the path to the latest available cros-termina component. |
| base::FilePath GetLatestVMPath() { |
| base::FilePath component_dir(kVmDefaultPath); |
| base::FileEnumerator dir_enum(component_dir, false, |
| base::FileEnumerator::DIRECTORIES); |
| |
| base::Version latest_version("0"); |
| base::FilePath latest_path; |
| |
| for (base::FilePath path = dir_enum.Next(); !path.empty(); |
| path = dir_enum.Next()) { |
| base::Version version(path.BaseName().value()); |
| if (!version.IsValid()) |
| continue; |
| |
| if (version > latest_version) { |
| latest_version = version; |
| latest_path = path; |
| } |
| } |
| |
| return latest_path; |
| } |
| |
| // Gets the path to a VM disk given the name, user id, and location. |
| bool GetDiskPathFromName( |
| const std::string& vm_name, |
| const std::string& cryptohome_id, |
| StorageLocation storage_location, |
| bool create_parent_dir, |
| 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(cryptohome_id, vm_name, storage_location, |
| kQcowImageExtension, create_parent_dir); |
| if (!qcow2_path) { |
| if (create_parent_dir) |
| LOG(ERROR) << "Failed to get qcow2 path"; |
| return false; |
| } |
| const auto raw_path = |
| GetFilePathFromName(cryptohome_id, vm_name, storage_location, |
| kRawImageExtension, create_parent_dir); |
| if (!raw_path) { |
| if (create_parent_dir) |
| LOG(ERROR) << "Failed to get 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_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(cryptohome_id, vm_name, storage_location, |
| kPluginVmImageExtension, create_parent_dir); |
| if (!plugin_path) { |
| if (create_parent_dir) |
| LOG(ERROR) << "failed to get plugin path"; |
| return false; |
| } |
| *path_out = *plugin_path; |
| return true; |
| } |
| default: |
| LOG(ERROR) << "Unknown storage location type"; |
| return false; |
| } |
| } |
| |
| bool CheckVmExists(const std::string& vm_name, |
| const std::string& cryptohome_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_name, cryptohome_id, location, |
| false, /* create_parent_dir */ |
| &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). |
| uint64_t CalculateDesiredDiskSize(base::FilePath disk_location, |
| uint64_t current_usage) { |
| 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 size was specified by the user and should not be |
| // automatically resized. |
| bool IsDiskUserChosenSize(std::string disk_path) { |
| return getxattr(disk_path.c_str(), kDiskImageUserChosenSizeXattr, NULL, 0) >= |
| 0; |
| } |
| |
| // Mark a disk with an xattr indicating its size has been chosen by the user. |
| bool SetUserChosenSizeAttr(const base::ScopedFD& fd) { |
| // The xattr value doesn't matter, only its existence. |
| // Store something human-readable for debugging. |
| constexpr char val[] = "1"; |
| return fsetxattr(fd.get(), kDiskImageUserChosenSizeXattr, 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; |
| } |
| |
| // Returns the current kernel version. If there is a failure to retrieve the |
| // version it returns <INT_MIN, INT_MIN>. |
| KernelVersionAndMajorRevision GetKernelVersion() { |
| struct utsname buf; |
| if (uname(&buf)) |
| return std::make_pair(INT_MIN, INT_MIN); |
| |
| // Parse uname result in the form of x.yy.zzz. The parsed data should be in |
| // the expected format. |
| std::vector<base::StringPiece> versions = base::SplitStringPiece( |
| buf.release, ".", base::WhitespaceHandling::TRIM_WHITESPACE, |
| base::SplitResult::SPLIT_WANT_ALL); |
| DCHECK_EQ(versions.size(), 3); |
| DCHECK(!versions[0].empty()); |
| DCHECK(!versions[1].empty()); |
| int version; |
| bool result = base::StringToInt(versions[0], &version); |
| DCHECK(result); |
| int major_revision; |
| result = base::StringToInt(versions[1], &major_revision); |
| DCHECK(result); |
| return std::make_pair(version, major_revision); |
| } |
| |
| // vm_name should always be less then kMaxVmNameLength characters long. |
| base::FilePath GetVmLogPath(const std::string& owner_id, |
| const std::string& vm_name, |
| bool log_to_cryptohome = true) { |
| if (!log_to_cryptohome) { |
| return base::FilePath(); |
| } |
| std::string encoded_vm_name = GetEncodedName(vm_name); |
| |
| base::FilePath path = base::FilePath(kCryptohomeRoot) |
| .Append(kCrosvmDir) |
| .Append(owner_id) |
| .Append(kCrosvmLogDir) |
| .Append(encoded_vm_name) |
| .AddExtension(".lsock"); |
| |
| base::FilePath parent_dir = path.DirName(); |
| if (!base::DirectoryExists(parent_dir)) { |
| base::File::Error dir_error; |
| if (!base::CreateDirectoryAndGetError(parent_dir, &dir_error)) { |
| LOG(ERROR) << "Failed to create crosvm log directory in " << parent_dir |
| << ": " << base::File::ErrorToString(dir_error); |
| return base::FilePath(); |
| } |
| } |
| 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(str.data(), str.size(), &digest); |
| base::StringPiece 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; |
| } |
| |
| base::FilePath GetVmGpuCachePath(const std::string& owner_id, |
| const std::string& vm_name) { |
| 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_name, base::Base64UrlEncodePolicy::OMIT_PADDING, |
| &vm_dir); |
| |
| std::string bootid_dir; |
| CHECK(base::ReadFileToString(base::FilePath(kBootIdFile), &bootid_dir)); |
| bootid_dir = GetMd5HashForFilename(bootid_dir); |
| |
| return base::FilePath(kCryptohomeRoot) |
| .Append(kCrosvmDir) |
| .Append(owner_id) |
| .Append(kCrosvmGpuCacheDir) |
| .Append(bootid_dir) |
| .Append(vm_dir); |
| } |
| |
| bool IsDevModeEnabled() { |
| return VbGetSystemPropertyInt("cros_debug") == 1; |
| } |
| |
| // Returns whether the VM is trusted or untrusted based on the source image, |
| // whether we're passing custom kernel args, the host kernel version and a |
| // flag passed down by the user. |
| bool IsUntrustedVM(bool run_as_untrusted, |
| bool is_trusted_image, |
| bool has_custom_kernel_params, |
| KernelVersionAndMajorRevision host_kernel_version) { |
| // Nested virtualization is enabled for all kernels >= |
| // |kMinKernelVersionForUntrustedAndNestedVM|. This means that even with a |
| // trusted image the VM started will essentially be untrusted. |
| if (host_kernel_version >= kMinKernelVersionForUntrustedAndNestedVM) |
| return true; |
| |
| // Any untrusted image definitely results in an unstrusted VM. |
| if (!is_trusted_image) |
| return true; |
| |
| // Arbitrary kernel params cannot be trusted. |
| if (has_custom_kernel_params) |
| return true; |
| |
| if (run_as_untrusted) |
| return true; |
| |
| return false; |
| } |
| |
| // Returns whether an untrusted VM is allowed on the host and whether checking |
| // for security patches while starting the untrusted VM should be skipped. |
| UntrustedVMCheckResult IsUntrustedVMAllowed( |
| bool run_as_untrusted, KernelVersionAndMajorRevision host_kernel_version) { |
| // For host >= |kMinKernelVersionForUntrustedAndNestedVM| untrusted VMs are |
| // always allowed. But the host still needs to be checked for vulnerabilities, |
| // even in developer mode. This is done because it'd be a huge error to not |
| // have required security patches on these kernels regardless of dev or |
| // production mode. |
| if (host_kernel_version >= kMinKernelVersionForUntrustedAndNestedVM) { |
| return UntrustedVMCheckResult(true /* untrusted_vm_allowed */, |
| false /* skip_host_checks */); |
| } |
| |
| // On lower kernel versions |run_as_untrusted| is only respected in developer |
| // mode. The user wants to start the VM irrespective of the host's kernel |
| // version or security mitigation state. In this mode, allow untrusted VMs |
| // without any restrictions on the host having security mitigations. |
| if (run_as_untrusted && IsDevModeEnabled()) { |
| return UntrustedVMCheckResult(true /* untrusted_vm_allowed */, |
| true /* skip_host_checks */); |
| } |
| |
| // Lower kernel version are deemed insecure to handle untrusted VMs. |
| // Note: |skip_host_checks| is redundant in this scenario as |
| // |untrusted_vm_allowed| is set to false. |
| return UntrustedVMCheckResult(false /* untrusted_vm_allowed */, |
| false /* skip_host_checks */); |
| } |
| |
| // Clears close-on-exec flag for a file descriptor to pass it to a subprocess |
| // such as crosvm. Returns a failure reason on failure. |
| string RemoveCloseOnExec(int raw_fd) { |
| int flags = fcntl(raw_fd, F_GETFD); |
| if (flags == -1) { |
| return "Failed to get flags for passed fd"; |
| } |
| |
| flags &= ~FD_CLOEXEC; |
| if (fcntl(raw_fd, F_SETFD, flags) == -1) { |
| return "Failed to clear close-on-exec flag for fd"; |
| } |
| |
| return ""; |
| } |
| |
| // 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. |
| std::unique_ptr<dbus::Response> ReclaimVmMemoryInternal( |
| pid_t pid, std::unique_ptr<dbus::Response> dbus_response) { |
| dbus::MessageWriter writer(dbus_response.get()); |
| ReclaimVmMemoryResponse response; |
| response.set_success(false); |
| |
| const std::string path = base::StringPrintf("/proc/%d/reclaim", pid); |
| const std::string value = "shmem"; |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| if (HANDLE_EINTR(write(fd.get(), value.c_str(), value.size())) != |
| value.size()) { |
| PLOG(ERROR) << "Failed to write to " << path; |
| response.set_failure_reason("Failed to write to /proc filesystem"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| LOG(INFO) << "Successfully reclaimed VM memory. PID=" << pid; |
| |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| } // namespace |
| |
| base::Optional<int64_t> Service::GetAvailableMemory() { |
| dbus::MethodCall method_call(resource_manager::kResourceManagerInterface, |
| resource_manager::kGetAvailableMemoryKBMethod); |
| auto dbus_response = brillo::dbus_utils::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 base::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 base::nullopt; |
| } |
| return available_kb * KIB; |
| } |
| |
| base::Optional<int64_t> Service::GetForegroundAvailableMemory() { |
| dbus::MethodCall method_call( |
| resource_manager::kResourceManagerInterface, |
| resource_manager::kGetForegroundAvailableMemoryKBMethod); |
| auto dbus_response = brillo::dbus_utils::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 base::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 base::nullopt; |
| } |
| return available_kb * KIB; |
| } |
| |
| base::Optional<MemoryMargins> Service::GetMemoryMargins() { |
| dbus::MethodCall method_call(resource_manager::kResourceManagerInterface, |
| resource_manager::kGetMemoryMarginsKBMethod); |
| auto dbus_response = brillo::dbus_utils::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 base::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 base::nullopt; |
| } |
| if (!reader.PopUint64(&margins.moderate)) { |
| LOG(ERROR) |
| << "Failed to read available moderate margin from the D-Bus response"; |
| return base::nullopt; |
| } |
| margins.critical *= KIB; |
| margins.moderate *= KIB; |
| return margins; |
| } |
| |
| base::Optional<resource_manager::GameMode> Service::GetGameMode() { |
| dbus::MethodCall method_call(resource_manager::kResourceManagerInterface, |
| resource_manager::kGetGameModeMethod); |
| auto dbus_response = brillo::dbus_utils::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 base::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 base::nullopt; |
| } |
| return static_cast<resource_manager::GameMode>(game_mode); |
| } |
| |
| static base::Optional<std::string> GameModeToForegroundVmName( |
| resource_manager::GameMode game_mode) { |
| using resource_manager::GameMode; |
| if (game_mode == GameMode::BOREALIS) { |
| return "borealis"; |
| } |
| if (game_mode == GameMode::OFF) { |
| return base::nullopt; |
| } |
| LOG(ERROR) << "Unexpected game mode value " << static_cast<int>(game_mode); |
| return base::nullopt; |
| } |
| |
| // Runs balloon policy against each VM to balance memory. |
| // This will be called periodically by balloon_resizing_timer_. |
| void Service::RunBalloonPolicy() { |
| // TODO(b/191946183): Design and migrate to a new D-Bus API |
| // that is less chatty for implementing balloon logic. |
| if (!memory_margins_) { |
| // Lazily initialize memory_margins_. Done here so we don't delay VM startup |
| // with a D-Bus call. |
| memory_margins_ = GetMemoryMargins(); |
| if (!memory_margins_) { |
| LOG(ERROR) << "Failed to get ChromeOS memory margins, stopping balloon " |
| << "policy"; |
| balloon_resizing_timer_.Stop(); |
| return; |
| } |
| } |
| const auto available_memory = GetAvailableMemory(); |
| if (!available_memory.has_value()) { |
| return; |
| } |
| const auto game_mode = GetGameMode(); |
| if (!game_mode.has_value()) { |
| return; |
| } |
| base::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; |
| } |
| } |
| 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; |
| } |
| |
| // 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; |
| auto stats_opt = vm->GetBalloonStats(); |
| if (!stats_opt) { |
| // Stats not available. Skip running policies. |
| continue; |
| } |
| BalloonStats stats = *stats_opt; |
| |
| int64_t delta = policy->ComputeBalloonDelta( |
| stats, available_memory_for_vm, is_in_game_mode, vm_entry.first.name()); |
| int64_t target = std::max(INT64_C(0), stats.balloon_actual + delta); |
| if (target != stats.balloon_actual) { |
| vm->SetBalloonSize(target); |
| } |
| } |
| } |
| |
| bool Service::ListVmDisksInLocation(const string& cryptohome_id, |
| StorageLocation location, |
| const string& lookup_name, |
| ListVmDisksResponse* response) { |
| 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_success(false); |
| 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()) { |
| 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; |
| auto iter = FindVm(cryptohome_id, image_name); |
| if (iter == vms_.end()) { |
| // 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; |
| } else { |
| 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(IsDiskUserChosenSize(path.value())); |
| image->set_path(path.value()); |
| } |
| |
| response->set_total_size(response->total_size() + total_size); |
| return true; |
| } |
| |
| std::unique_ptr<Service> Service::Create(base::Closure quit_closure) { |
| auto service = base::WrapUnique(new Service(std::move(quit_closure))); |
| |
| if (!service->Init()) { |
| service.reset(); |
| } |
| |
| return service; |
| } |
| |
| Service::Service(base::Closure quit_closure) |
| : next_seneschal_server_port_(kFirstSeneschalServerPort), |
| quit_closure_(std::move(quit_closure)), |
| host_kernel_version_(GetKernelVersion()), |
| weak_ptr_factory_(this) {} |
| |
| Service::~Service() { |
| if (grpc_server_vm_) { |
| grpc_server_vm_->Shutdown(); |
| } |
| } |
| |
| void Service::OnSignalReadable() { |
| struct signalfd_siginfo siginfo; |
| if (read(signal_fd_.get(), &siginfo, sizeof(siginfo)) != sizeof(siginfo)) { |
| PLOG(ERROR) << "Failed to read from signalfd"; |
| return; |
| } |
| |
| if (siginfo.ssi_signo == SIGCHLD) { |
| HandleChildExit(); |
| } else if (siginfo.ssi_signo == SIGTERM) { |
| HandleSigterm(); |
| } else { |
| LOG(ERROR) << "Received unknown signal from signal fd: " |
| << strsignal(siginfo.ssi_signo); |
| } |
| } |
| |
| bool Service::Init() { |
| // It's not possible to ask minijail to set up a user namespace and switch to |
| // a non-0 uid/gid, or to set up supplemental groups. Concierge needs both |
| // supplemental groups and to run as a user whose id is unchanged from the |
| // root namespace (dbus authentication requires this), so we configure this |
| // here. |
| if (setresuid(kCrosvmUGid, kCrosvmUGid, kCrosvmUGid) < 0) { |
| PLOG(ERROR) << "Failed to set uid to crosvm"; |
| return false; |
| } |
| if (setresgid(kCrosvmUGid, kCrosvmUGid, kCrosvmUGid) < 0) { |
| PLOG(ERROR) << "Failed to set gid to crosvm"; |
| return false; |
| } |
| // Ideally we would just call initgroups("crosvm") here, but internally glibc |
| // interprets EINVAL as signaling that the list of supplemental groups is too |
| // long and truncates the list, when it could also indicate that some of the |
| // gids are unmapped in the current namespace. Instead we look up the groups |
| // ourselves so we can log a useful error if the mapping is wrong. |
| int ngroups = 0; |
| getgrouplist("crosvm", kCrosvmUGid, nullptr, &ngroups); |
| std::vector<gid_t> groups(ngroups); |
| if (getgrouplist("crosvm", kCrosvmUGid, groups.data(), &ngroups) < 0) { |
| PLOG(ERROR) << "Failed to get supplemental groups for user crosvm"; |
| return false; |
| } |
| if (setgroups(ngroups, groups.data()) < 0) { |
| PLOG(ERROR) |
| << "Failed to set supplemental groups. This probably means you have " |
| "added user crosvm to groups that are not mapped in the concierge " |
| "user namespace and need to update vm_concierge.conf."; |
| return false; |
| } |
| |
| // Change the umask so that the runtime directory for each VM will get the |
| // right permissions. |
| umask(002); |
| |
| // Set up the signalfd for receiving SIGCHLD and SIGTERM. |
| // This applies to all threads created afterwards. |
| sigset_t mask; |
| sigemptyset(&mask); |
| sigaddset(&mask, SIGCHLD); |
| sigaddset(&mask, SIGTERM); |
| |
| // Restore process' "dumpable" flag so that /proc will be writable. |
| // We need it to properly set up jail for Plugin VM helper process. |
| if (prctl(PR_SET_DUMPABLE, 1) < 0) { |
| PLOG(ERROR) << "Failed to set PR_SET_DUMPABLE"; |
| return false; |
| } |
| |
| signal_fd_.reset(signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)); |
| if (!signal_fd_.is_valid()) { |
| PLOG(ERROR) << "Failed to create signalfd"; |
| return false; |
| } |
| |
| watcher_ = base::FileDescriptorWatcher::WatchReadable( |
| signal_fd_.get(), |
| base::BindRepeating(&Service::OnSignalReadable, base::Unretained(this))); |
| if (!watcher_) { |
| LOG(ERROR) << "Failed to watch signalfd"; |
| return false; |
| } |
| |
| // Now block signals from the normal signal handling path so that we will get |
| // them via the signalfd. |
| if (sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) { |
| PLOG(ERROR) << "Failed to block signals via sigprocmask"; |
| return false; |
| } |
| |
| base::Thread::Options thread_options; |
| thread_options.message_pump_type = base::MessagePumpType::IO; |
| if (!dbus_thread_.StartWithOptions(thread_options)) { |
| LOG(ERROR) << "Failed to start dbus thread"; |
| return false; |
| } |
| |
| dbus::Bus::Options opts; |
| opts.bus_type = dbus::Bus::SYSTEM; |
| opts.dbus_task_runner = dbus_thread_.task_runner(); |
| bus_ = new dbus::Bus(std::move(opts)); |
| |
| if (!AsyncNoReject(dbus_thread_.task_runner(), |
| base::BindOnce( |
| [](scoped_refptr<dbus::Bus> bus) { |
| if (!bus->Connect()) { |
| LOG(ERROR) << "Failed to connect to system bus"; |
| return false; |
| } |
| return true; |
| }, |
| bus_)) |
| .Get() |
| .val) { |
| return false; |
| } |
| |
| exported_object_ = |
| bus_->GetExportedObject(dbus::ObjectPath(kVmConciergeServicePath)); |
| if (!exported_object_) { |
| LOG(ERROR) << "Failed to export " << kVmConciergeServicePath << " object"; |
| return false; |
| } |
| |
| untrusted_vm_utils_ = std::make_unique<UntrustedVMUtils>( |
| base::FilePath(kL1TFFilePath), base::FilePath(kMDSFilePath)); |
| |
| dlcservice_client_ = std::make_unique<DlcHelper>(bus_); |
| |
| using ServiceMethod = |
| std::unique_ptr<dbus::Response> (Service::*)(dbus::MethodCall*); |
| static const std::map<const char*, ServiceMethod> kServiceMethods = { |
| {kStartVmMethod, &Service::StartVm}, |
| {kStartPluginVmMethod, &Service::StartPluginVm}, |
| {kStartArcVmMethod, &Service::StartArcVm}, |
| {kStopVmMethod, &Service::StopVm}, |
| {kStopAllVmsMethod, &Service::StopAllVms}, |
| {kSuspendVmMethod, &Service::SuspendVm}, |
| {kResumeVmMethod, &Service::ResumeVm}, |
| {kGetVmInfoMethod, &Service::GetVmInfo}, |
| {kGetVmEnterpriseReportingInfoMethod, |
| &Service::GetVmEnterpriseReportingInfo}, |
| {kMakeRtVcpuMethod, &Service::MakeRtVcpu}, |
| {kAdjustVmMethod, &Service::AdjustVm}, |
| {kCreateDiskImageMethod, &Service::CreateDiskImage}, |
| {kDestroyDiskImageMethod, &Service::DestroyDiskImage}, |
| {kResizeDiskImageMethod, &Service::ResizeDiskImage}, |
| {kExportDiskImageMethod, &Service::ExportDiskImage}, |
| {kImportDiskImageMethod, &Service::ImportDiskImage}, |
| {kDiskImageStatusMethod, &Service::CheckDiskImageStatus}, |
| {kCancelDiskImageMethod, &Service::CancelDiskImageOperation}, |
| {kListVmDisksMethod, &Service::ListVmDisks}, |
| {kGetContainerSshKeysMethod, &Service::GetContainerSshKeys}, |
| {kSyncVmTimesMethod, &Service::SyncVmTimes}, |
| {kAttachUsbDeviceMethod, &Service::AttachUsbDevice}, |
| {kDetachUsbDeviceMethod, &Service::DetachUsbDevice}, |
| {kListUsbDeviceMethod, &Service::ListUsbDevices}, |
| {kGetDnsSettingsMethod, &Service::GetDnsSettings}, |
| {kSetVmCpuRestrictionMethod, &Service::SetVmCpuRestriction}, |
| {kSetVmIdMethod, &Service::SetVmId}, |
| }; |
| |
| using AsyncServiceMethod = void (Service::*)( |
| dbus::MethodCall*, dbus::ExportedObject::ResponseSender); |
| static const std::map<const char*, AsyncServiceMethod> kAsyncServiceMethods = |
| { |
| {kReclaimVmMemoryMethod, &Service::ReclaimVmMemory}, |
| }; |
| |
| if (!AsyncNoReject( |
| dbus_thread_.task_runner(), |
| base::BindOnce( |
| [](Service* service, dbus::ExportedObject* exported_object_, |
| scoped_refptr<dbus::Bus> bus) { |
| for (const auto& iter : kServiceMethods) { |
| bool ret = exported_object_->ExportMethodAndBlock( |
| kVmConciergeInterface, iter.first, |
| base::Bind( |
| &HandleSynchronousDBusMethodCall, |
| base::Bind(iter.second, base::Unretained(service)))); |
| if (!ret) { |
| LOG(ERROR) << "Failed to export method " << iter.first; |
| return false; |
| } |
| } |
| for (const auto& iter : kAsyncServiceMethods) { |
| bool ret = exported_object_->ExportMethodAndBlock( |
| kVmConciergeInterface, iter.first, |
| base::Bind( |
| &HandleAsynchronousDBusMethodCall, |
| base::Bind(iter.second, base::Unretained(service)))); |
| if (!ret) { |
| LOG(ERROR) |
| << "Failed to export async method " << iter.first; |
| return false; |
| } |
| } |
| |
| if (!bus->RequestOwnershipAndBlock( |
| kVmConciergeServiceName, dbus::Bus::REQUIRE_PRIMARY)) { |
| LOG(ERROR) << "Failed to take ownership of " |
| << kVmConciergeServiceName; |
| return false; |
| } |
| |
| return true; |
| }, |
| base::Unretained(this), base::Unretained(exported_object_), |
| bus_)) |
| .Get() |
| .val) { |
| return false; |
| } |
| |
| // Set up the D-Bus client for shill. |
| shill_client_ = std::make_unique<ShillClient>(bus_); |
| shill_client_->RegisterResolvConfigChangedHandler(base::Bind( |
| &Service::OnResolvConfigChanged, weak_ptr_factory_.GetWeakPtr())); |
| shill_client_->RegisterDefaultServiceChangedHandler( |
| base::Bind(&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::Bind(&Service::HandleSuspendImminent, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::Bind(&Service::HandleSuspendDone, weak_ptr_factory_.GetWeakPtr())); |
| |
| // Get the D-Bus proxy for communicating with cicerone. |
| cicerone_service_proxy_ = bus_->GetObjectProxy( |
| vm_tools::cicerone::kVmCiceroneServiceName, |
| dbus::ObjectPath(vm_tools::cicerone::kVmCiceroneServicePath)); |
| if (!cicerone_service_proxy_) { |
| LOG(ERROR) << "Unable to get dbus proxy for " |
| << vm_tools::cicerone::kVmCiceroneServiceName; |
| return false; |
| } |
| cicerone_service_proxy_->ConnectToSignal( |
| vm_tools::cicerone::kVmCiceroneServiceName, |
| vm_tools::cicerone::kTremplinStartedSignal, |
| base::Bind(&Service::OnTremplinStartedSignal, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::Bind(&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)); |
| if (!seneschal_service_proxy_) { |
| LOG(ERROR) << "Unable to get dbus proxy for " |
| << vm_tools::seneschal::kSeneschalServiceName; |
| return false; |
| } |
| |
| // Get the D-Bus proxy for communicating with Plugin VM dispatcher. |
| vm_permission_service_proxy_ = vm_permission::GetServiceProxy(bus_); |
| if (!vm_permission_service_proxy_) { |
| LOG(ERROR) << "Unable to get dbus proxy for VM permission service"; |
| return false; |
| } |
| |
| // Get the D-Bus proxy for communicating with Plugin VM dispatcher. |
| vmplugin_service_proxy_ = pvm::dispatcher::GetServiceProxy(bus_); |
| if (!vmplugin_service_proxy_) { |
| LOG(ERROR) << "Unable to get dbus proxy for Plugin VM dispatcher service"; |
| return false; |
| } |
| pvm::dispatcher::RegisterVmToolsChangedCallbacks( |
| vmplugin_service_proxy_, |
| base::Bind(&Service::OnVmToolsStateChangedSignal, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::Bind(&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)); |
| if (!resource_manager_service_proxy_) { |
| LOG(ERROR) << "Unable to get dbus proxy for " |
| << resource_manager::kResourceManagerServiceName; |
| return false; |
| } |
| |
| // Setup & start the gRPC listener services. |
| if (!SetupListenerService( |
| &grpc_thread_vm_, &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 (!reclaim_thread_.Start()) { |
| LOG(ERROR) << "Failed to start memory reclaim thread"; |
| return false; |
| } |
| |
| balloon_resizing_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1), |
| this, &Service::RunBalloonPolicy); |
| |
| return true; |
| } |
| |
| void Service::HandleChildExit() { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| // 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) { |
| VmInterface::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); |
| } |
| } |
| } |
| |
| void Service::HandleSigterm() { |
| LOG(INFO) << "Shutting down due to SIGTERM"; |
| |
| StopAllVmsImpl(SERVICE_SHUTDOWN); |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, quit_closure_); |
| } |
| |
| std::unique_ptr<dbus::Response> Service::StartVm( |
| dbus::MethodCall* method_call) { |
| LOG(INFO) << "Received StartVm request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| StartVmRequest request; |
| StartVmResponse response; |
| auto helper_result = StartVmHelper<StartVmRequest>( |
| method_call, &reader, &writer, true /* allow_zero_cpus */); |
| if (!helper_result) { |
| return dbus_response; |
| } |
| std::tie(request, response) = *helper_result; |
| VmInfo* vm_info = response.mutable_vm_info(); |
| vm_info->set_vm_type(request.start_termina() ? VmInfo::TERMINA |
| : VmInfo::UNKNOWN); |
| |
| base::Optional<base::ScopedFD> kernel_fd, rootfs_fd, initrd_fd, storage_fd, |
| bios_fd; |
| for (const auto& fdType : request.fds()) { |
| base::ScopedFD fd; |
| if (!reader.PopFileDescriptor(&fd)) { |
| std::stringstream ss; |
| ss << "failed to get a " << StartVmRequest_FdType_Name(fdType) << " FD"; |
| LOG(ERROR) << ss.str(); |
| response.set_failure_reason(ss.str()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| switch (fdType) { |
| case StartVmRequest_FdType_KERNEL: |
| kernel_fd = std::move(fd); |
| break; |
| case StartVmRequest_FdType_ROOTFS: |
| rootfs_fd = std::move(fd); |
| break; |
| case StartVmRequest_FdType_INITRD: |
| initrd_fd = std::move(fd); |
| break; |
| case StartVmRequest_FdType_STORAGE: |
| storage_fd = std::move(fd); |
| break; |
| case StartVmRequest_FdType_BIOS: |
| bios_fd = std::move(fd); |
| break; |
| default: |
| LOG(WARNING) << "received request with unknown FD type " << fdType |
| << ". Ignoring."; |
| } |
| } |
| |
| // Make sure we have our signal connected if starting a Termina VM. |
| if (request.start_termina() && !is_tremplin_started_signal_connected_) { |
| LOG(ERROR) << "Can't start Termina VM without TremplinStartedSignal"; |
| response.set_failure_reason("TremplinStartedSignal not connected"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.disks_size() > kMaxExtraDisks) { |
| LOG(ERROR) << "Rejecting request with " << request.disks_size() |
| << " extra disks"; |
| response.set_failure_reason("Too many extra disks"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| string failure_reason; |
| VMImageSpec image_spec = |
| GetImageSpec(request.vm(), kernel_fd, rootfs_fd, initrd_fd, bios_fd, |
| request.start_termina(), &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); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!image_spec.kernel.empty() && !base::PathExists(image_spec.kernel)) { |
| LOG(ERROR) << "Missing VM kernel path: " << image_spec.kernel.value(); |
| response.set_failure_reason("Kernel path does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!image_spec.bios.empty() && !base::PathExists(image_spec.bios)) { |
| LOG(ERROR) << "Missing VM BIOS path: " << image_spec.bios.value(); |
| response.set_failure_reason("BIOS path does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (image_spec.kernel.empty() && image_spec.bios.empty()) { |
| LOG(ERROR) << "neither a kernel nor a BIOS were provided"; |
| response.set_failure_reason("neither a kernel nor a BIOS were provided"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!image_spec.initrd.empty() && !base::PathExists(image_spec.initrd)) { |
| LOG(ERROR) << "Missing VM initrd path: " << image_spec.initrd.value(); |
| response.set_failure_reason("Initrd path does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!image_spec.rootfs.empty() && !base::PathExists(image_spec.rootfs)) { |
| LOG(ERROR) << "Missing VM rootfs path: " << image_spec.rootfs.value(); |
| response.set_failure_reason("Rootfs path does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| const bool is_untrusted_vm = |
| IsUntrustedVM(request.run_as_untrusted(), image_spec.is_trusted_image, |
| !request.kernel_params().empty(), host_kernel_version_); |
| if (is_untrusted_vm) { |
| const auto untrusted_vm_check_result = |
| IsUntrustedVMAllowed(request.run_as_untrusted(), host_kernel_version_); |
| if (!untrusted_vm_check_result.untrusted_vm_allowed) { |
| std::stringstream ss; |
| ss << "Untrusted VMs are not allowed: " |
| << "the host kernel version (" << host_kernel_version_.first << "." |
| << host_kernel_version_.second << ") must be newer than or equal to " |
| << kMinKernelVersionForUntrustedAndNestedVM.first << "." |
| << kMinKernelVersionForUntrustedAndNestedVM.second |
| << ", or the device must be in the developer mode"; |
| LOG(ERROR) << ss.str(); |
| response.set_failure_reason(ss.str()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // For untrusted VMs - |
| // Check if l1tf and mds mitigations are present on the host. Skip the |
| // checks if untrusted VMs are requested in developer mode on insecure |
| // kernels. This is done to support testing by developers. |
| if (!untrusted_vm_check_result.skip_host_checks) { |
| switch (untrusted_vm_utils_->CheckUntrustedVMMitigationStatus()) { |
| // If the host kernel version isn't supported or the host doesn't have |
| // l1tf and mds mitigations then fail to start an untrusted VM. |
| case UntrustedVMUtils::MitigationStatus::VULNERABLE: { |
| LOG(ERROR) << "Host vulnerable against untrusted VM"; |
| response.set_failure_reason("Host vulnerable against untrusted VM"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // At this point SMT should not be a security issue. As |
| // |kMinKernelVersionForUntrustedAndNestedVM| has security patches to |
| // make nested VMs co-exist securely with SMT. |
| case UntrustedVMUtils::MitigationStatus::VULNERABLE_DUE_TO_SMT_ENABLED: |
| case UntrustedVMUtils::MitigationStatus::NOT_VULNERABLE: |
| break; |
| } |
| } |
| } |
| |
| // 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 = host_kernel_version_ >= kMinKernelVersionForVirtioPmem && |
| USE_PMEM_DEVICE_FOR_ROOTFS; |
| string rootfs_device = use_pmem ? "/dev/pmem0" : "/dev/vda"; |
| unsigned char disk_letter = use_pmem ? 'a' : 'b'; |
| std::vector<Disk> disks; |
| |
| // 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. |
| string tools_device; |
| if (base::PathExists(image_spec.tools_disk)) { |
| disks.push_back( |
| Disk(std::move(image_spec.tools_disk), false /* writable */)); |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Assume the stateful device is the first disk in the request. |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| for (const auto& disk : request.disks()) { |
| if (!base::PathExists(base::FilePath(disk.path()))) { |
| LOG(ERROR) << "Missing disk path: " << disk.path(); |
| response.set_failure_reason("One or more disk paths do not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| Disk::Config config{}; |
| config.writable = disk.writable(); |
| config.sparse = !IsDiskUserChosenSize(disk.path()); |
| disks.push_back(Disk(base::FilePath(disk.path()), config)); |
| } |
| |
| // Check if an opened storage image was passed over D-BUS. |
| if (storage_fd.has_value()) { |
| // We only allow untrusted VMs to mount extra storage. |
| if (!is_untrusted_vm) { |
| LOG(ERROR) << "storage fd passed for a trusted VM"; |
| |
| response.set_failure_reason("storage fd is passed for a trusted VM"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| int raw_fd = storage_fd.value().get(); |
| string failure_reason = 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); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| disks.push_back(Disk(base::FilePath(kProcFileDescriptorsPath) |
| .Append(base::NumberToString(raw_fd)), |
| true /* writable */)); |
| } |
| |
| // 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.name().size() > kMaxVmNameLength) { |
| LOG(ERROR) << "VM name is too long"; |
| |
| response.set_failure_reason("VM name is too long"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| base::FilePath log_path = GetVmLogPath(request.owner_id(), request.name()); |
| |
| base::FilePath gpu_cache_path; |
| if (request.enable_gpu()) { |
| gpu_cache_path = PrepareVmGpuCachePath(request.owner_id(), request.name()); |
| } |
| |
| if (request.enable_vulkan() && !request.enable_gpu()) { |
| LOG(ERROR) << "Vulkan enabled without GPU"; |
| response.set_failure_reason("Vulkan enabled without GPU"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Allocate resources for the VM. |
| uint32_t vsock_cid = vsock_cid_pool_.Allocate(); |
| if (vsock_cid == 0) { |
| LOG(ERROR) << "Unable to allocate vsock context id"; |
| |
| response.set_failure_reason("Unable to allocate vsock cid"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| vm_info->set_cid(vsock_cid); |
| |
| std::unique_ptr<patchpanel::Client> network_client = |
| patchpanel::Client::New(bus_); |
| if (!network_client) { |
| LOG(ERROR) << "Unable to open networking service client"; |
| |
| response.set_failure_reason("Unable to open network service client"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| uint32_t seneschal_server_handle = server_proxy->handle(); |
| vm_info->set_seneschal_server_handle(seneschal_server_handle); |
| |
| // Associate a WaitableEvent with this VM. This needs to happen before |
| // starting the VM to avoid a race where the VM reports that it's ready |
| // before it gets added as a pending VM. |
| base::WaitableEvent event(base::WaitableEvent::ResetPolicy::AUTOMATIC, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| startup_listener_.AddPendingVm(vsock_cid, &event); |
| |
| // Start the VM and build the response. |
| VmFeatures features{ |
| .gpu = request.enable_gpu(), |
| .vulkan = request.enable_vulkan(), |
| .big_gl = request.enable_big_gl(), |
| .software_tpm = request.software_tpm(), |
| .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); |
| |
| // 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. |
| VmId vm_id(request.owner_id(), request.name()); |
| SendVmStartingUpSignal(vm_id, *vm_info); |
| |
| VmBuilder vm_builder; |
| vm_builder.SetKernel(std::move(image_spec.kernel)) |
| .SetBios(std::move(image_spec.bios)) |
| .SetInitrd(std::move(image_spec.initrd)) |
| .SetCpus(cpus) |
| .AppendDisks(std::move(disks)) |
| .EnableSmt(false /* enable */) |
| .SetGpuCachePath(std::move(gpu_cache_path)); |
| if (!image_spec.rootfs.empty()) { |
| vm_builder.SetRootfs({.device = std::move(rootfs_device), |
| .path = std::move(image_spec.rootfs), |
| .writable = request.writable_rootfs()}); |
| } |
| |
| // Group the CPUs by their physical package ID to determine CPU cluster |
| // layout. |
| std::vector<std::vector<std::string>> cpu_clusters; |
| std::map<int32_t, std::vector<std::string>> cpu_capacity_groups; |
| std::vector<std::string> cpu_capacity; |
| for (int32_t cpu = 0; cpu < cpus; cpu++) { |
| auto physical_package_id = GetCpuPackageId(cpu); |
| if (physical_package_id) { |
| CHECK_GE(*physical_package_id, 0); |
| if (*physical_package_id + 1 > cpu_clusters.size()) |
| cpu_clusters.resize(*physical_package_id + 1); |
| cpu_clusters[*physical_package_id].push_back(std::to_string(cpu)); |
| } |
| |
| auto capacity = GetCpuCapacity(cpu); |
| if (capacity) { |
| CHECK_GE(*capacity, 0); |
| cpu_capacity.push_back(base::StringPrintf("%d=%d", cpu, *capacity)); |
| auto group = cpu_capacity_groups.find(*capacity); |
| if (group != cpu_capacity_groups.end()) { |
| group->second.push_back(std::to_string(cpu)); |
| } else { |
| auto g = {std::to_string(cpu)}; |
| cpu_capacity_groups.insert({*capacity, g}); |
| } |
| } |
| } |
| |
| base::Optional<std::string> cpu_affinity = |
| GetCpuAffinityFromClusters(cpu_clusters, cpu_capacity_groups); |
| if (cpu_affinity) { |
| vm_builder.AppendCustomParam("--cpu-affinity", *cpu_affinity); |
| } |
| |
| if (!cpu_capacity.empty()) { |
| vm_builder.AppendCustomParam("--cpu-capacity", |
| base::JoinString(cpu_capacity, ",")); |
| } |
| |
| if (!cpu_clusters.empty()) { |
| for (const auto& cluster : cpu_clusters) { |
| auto cpu_list = base::JoinString(cluster, ","); |
| vm_builder.AppendCustomParam("--cpu-cluster", cpu_list); |
| } |
| } |
| |
| auto vm = TerminaVm::Create( |
| vsock_cid, std::move(network_client), std::move(server_proxy), |
| std::move(runtime_dir), std::move(log_path), std::move(stateful_device), |
| std::move(stateful_size), features, vm_permission_service_proxy_, bus_, |
| vm_id, request.start_termina(), std::move(vm_builder)); |
| if (!vm) { |
| LOG(ERROR) << "Unable to start VM"; |
| |
| startup_listener_.RemovePendingVm(vsock_cid); |
| response.set_failure_reason("Unable to start VM"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Wait for the VM to finish starting up and for maitre'd to signal that it's |
| // ready. |
| base::TimeDelta timeout = kVmStartupDefaultTimeout; |
| if (request.timeout() != 0) { |
| timeout = base::TimeDelta::FromSeconds(request.timeout()); |
| } |
| if (!event.TimedWait(timeout)) { |
| LOG(ERROR) << "VM failed to start in " << timeout.InSeconds() << " seconds"; |
| |
| startup_listener_.RemovePendingVm(vsock_cid); |
| response.set_failure_reason("VM failed to start in time"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Mount the tools disk if it exists. |
| if (!tools_device.empty()) { |
| if (!vm->Mount(tools_device, kToolsMountPath, kToolsFsType, MS_RDONLY, |
| "")) { |
| LOG(ERROR) << "Failed to mount tools disk"; |
| response.set_failure_reason("Failed to mount tools disk"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| } |
| |
| // Do all the mounts. |
| for (const auto& disk : request.disks()) { |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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::GenerateGUID(); |
| |
| // 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->GetInfo().pid, vm_token); |
| |
| vm_tools::StartTerminaResponse::MountResult mount_result = |
| vm_tools::StartTerminaResponse::UNKNOWN; |
| int64_t free_bytes = -1; |
| // Allow untrusted VMs to have privileged containers. |
| if (request.start_termina() && |
| !StartTermina(vm.get(), is_untrusted_vm /* allow_privileged_containers */, |
| request.features(), &failure_reason, &mount_result, |
| &free_bytes)) { |
| response.set_failure_reason(std::move(failure_reason)); |
| response.set_mount_result((StartVmResponse::MountResult)mount_result); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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, &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 (storage_fd.has_value()) { |
| const 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| } |
| |
| response.set_success(true); |
| response.set_status(request.start_termina() ? VM_STATUS_STARTING |
| : VM_STATUS_RUNNING); |
| vm_info->set_ipv4_address(vm->IPv4Address()); |
| vm_info->set_pid(vm->pid()); |
| vm_info->set_permission_token(vm->PermissionToken()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| SendVmStartedSignal(vm_id, *vm_info, response.status()); |
| |
| vms_[vm_id] = std::move(vm); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::StopVm(dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received StopVm request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| StopVmRequest request; |
| StopVmResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse StopVmRequest from message"; |
| |
| response.set_failure_reason("Unable to parse protobuf"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| // This is not an error to Chrome |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Notify that we are about to stop a VM. |
| NotifyVmStopping(iter->first, iter->second->GetInfo().cid); |
| |
| if (!iter->second->Shutdown()) { |
| LOG(ERROR) << "Unable to shut down VM"; |
| |
| response.set_failure_reason("Unable to shut down VM"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Notify that we have stopped a VM. |
| NotifyVmStopped(iter->first, iter->second->GetInfo().cid, STOP_VM_REQUESTED); |
| |
| vms_.erase(iter); |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| // Wrapper to destroy VM in another thread |
| class VMDelegate : public base::PlatformThread::Delegate { |
| public: |
| VMDelegate() = default; |
| ~VMDelegate() override = default; |
| VMDelegate& operator=(VMDelegate&& other) = default; |
| explicit VMDelegate(const Service&) = delete; |
| VMDelegate& operator=(const Service&) = delete; |
| explicit VMDelegate(std::unique_ptr<VmInterface> vm) : vm_(std::move(vm)) {} |
| void ThreadMain() override { vm_.reset(); } |
| |
| private: |
| std::unique_ptr<VmInterface> vm_; |
| }; |
| |
| std::unique_ptr<dbus::Response> Service::StopAllVms( |
| dbus::MethodCall* method_call) { |
| StopAllVmsImpl(STOP_ALL_VMS_REQUESTED); |
| return nullptr; |
| } |
| |
| void Service::StopAllVmsImpl(VmStopReason reason) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received StopAllVms request"; |
| |
| struct ThreadContext { |
| base::PlatformThreadHandle handle; |
| uint32_t cid; |
| VMDelegate delegate; |
| }; |
| std::vector<ThreadContext> ctxs(vms_.size()); |
| |
| // Spawn a thread for each VM to shut it down. |
| int i = 0; |
| for (auto& iter : vms_) { |
| ThreadContext& ctx = ctxs[i++]; |
| |
| // Copy out cid from the VM object, as we will need it after the VM has been |
| // destroyed. |
| ctx.cid = iter.second->GetInfo().cid; |
| |
| // Notify that we are about to stop a VM. |
| NotifyVmStopping(iter.first, ctx.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?... |
| ctx.delegate = VMDelegate(std::move(iter.second)); |
| base::PlatformThread::Create(0, &ctx.delegate, &ctx.handle); |
| } |
| |
| i = 0; |
| for (auto& iter : vms_) { |
| ThreadContext& ctx = ctxs[i++]; |
| base::PlatformThread::Join(ctx.handle); |
| |
| // Notify that we have stopped a VM. |
| NotifyVmStopped(iter.first, ctx.cid, reason); |
| } |
| |
| vms_.clear(); |
| |
| if (!ctxs.empty()) { |
| LOG(INFO) << "Stopped all Vms"; |
| } |
| } |
| |
| std::unique_ptr<dbus::Response> Service::SuspendVm( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received SuspendVm request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| SuspendVmRequest request; |
| SuspendVmResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse SuspendVmRequest from message"; |
| |
| response.set_failure_reason("Unable to parse protobuf"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| // This is not an error to Chrome |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| 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."); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| vm->Suspend(); |
| |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::ResumeVm( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received ResumeVm request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ResumeVmRequest request; |
| ResumeVmResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ResumeVmRequest from message"; |
| |
| response.set_failure_reason("Unable to parse protobuf"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| // This is not an error to Chrome |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| 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."); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| vm->Resume(); |
| |
| 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); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::GetVmInfo( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received GetVmInfo request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| GetVmInfoRequest request; |
| GetVmInfoResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse GetVmInfoRequest from message"; |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| VmInterface::Info vm = iter->second->GetInfo(); |
| |
| VmInfo* vm_info = response.mutable_vm_info(); |
| vm_info->set_ipv4_address(vm.ipv4_address); |
| vm_info->set_pid(vm.pid); |
| vm_info->set_cid(vm.cid); |
| vm_info->set_seneschal_server_handle(vm.seneschal_server_handle); |
| vm_info->set_permission_token(vm.permission_token); |
| vm_info->set_vm_type(vm.type); |
| |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::GetVmEnterpriseReportingInfo( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received GetVmEnterpriseReportingInfo request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| GetVmEnterpriseReportingInfoRequest request; |
| GetVmEnterpriseReportingInfoResponse response; |
| |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| const std::string error_message = |
| "Unable to parse GetVmEnterpriseReportingInfo from message"; |
| LOG(ERROR) << error_message; |
| response.set_failure_reason(error_message); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.vm_name()); |
| if (iter == vms_.end()) { |
| const std::string error_message = "Requested VM does not exist"; |
| LOG(ERROR) << error_message; |
| response.set_failure_reason(error_message); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // failure_reason and success will be set by GetVmEnterpriseReportingInfo. |
| if (!iter->second->GetVmEnterpriseReportingInfo(&response)) { |
| LOG(ERROR) << "Failed to get VM enterprise reporting info"; |
| } |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::MakeRtVcpu( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received MakeRtVcpu request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| MakeRtVcpuRequest request; |
| MakeRtVcpuResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse MakeRtVcpuRequest from message"; |
| response.set_failure_reason("Unable to parse protobuf"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| const std::string error_message = "Requested VM does not exist"; |
| LOG(ERROR) << error_message; |
| response.set_failure_reason(error_message); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto& vm = iter->second; |
| vm->MakeRtVcpu(); |
| |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::AdjustVm( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received AdjustVm request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| AdjustVmRequest request; |
| AdjustVmResponse response; |
| |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| const std::string error_message = |
| "Unable to parse AdjustVmRequest from message"; |
| LOG(ERROR) << error_message; |
| response.set_failure_reason(error_message); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| StorageLocation location; |
| if (!CheckVmExists(request.name(), request.owner_id(), nullptr, &location)) { |
| response.set_failure_reason("Requested VM does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::vector<string> params( |
| std::make_move_iterator(request.mutable_params()->begin()), |
| std::make_move_iterator(request.mutable_params()->end())); |
| |
| 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 if (CheckVmExists(params[0], request.owner_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(request.owner_id(), request.name(), params[0], |
| &failure_reason); |
| } |
| } else { |
| failure_reason = "Unrecognized operation"; |
| } |
| |
| response.set_success(success); |
| response.set_failure_reason(failure_reason); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::SyncVmTimes( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received SyncVmTimes request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| SyncVmTimesResponse response; |
| int failures = 0; |
| int requests = 0; |
| for (auto& vm_entry : vms_) { |
| requests++; |
| 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); |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| bool Service::StartTermina(TerminaVm* vm, |
| bool allow_privileged_containers, |
| const google::protobuf::RepeatedField<int>& features, |
| string* failure_reason, |
| vm_tools::StartTerminaResponse::MountResult* result, |
| int64_t* out_free_bytes) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| DCHECK(result); |
| LOG(INFO) << "Starting Termina-specific services"; |
| |
| std::string dst_addr; |
| IPv4AddressToString(vm->ContainerSubnet(), &dst_addr); |
| size_t prefix_length = vm->ContainerPrefixLength(); |
| |
| std::string container_subnet_cidr = |
| base::StringPrintf("%s/%zu", dst_addr.c_str(), prefix_length); |
| |
| string error; |
| vm_tools::StartTerminaResponse response; |
| if (!vm->StartTermina(std::move(container_subnet_cidr), |
| allow_privileged_containers, 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; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::CreateDiskImage( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received CreateDiskImage request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| CreateDiskImageRequest request; |
| CreateDiskImageResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse CreateDiskImageRequest from message"; |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason("Unable to parse CreateImageDiskRequest"); |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| base::FilePath disk_path; |
| StorageLocation disk_location; |
| if (CheckVmExists(request.vm_name(), request.cryptohome_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| struct stat st; |
| if (stat(disk_path.value().c_str(), &st) < 0) { |
| PLOG(ERROR) << "stat() of existing VM image failed for " |
| << disk_path.value(); |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason( |
| "internal error: image exists but stat() failed"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| uint64_t current_size = st.st_size; |
| uint64_t current_usage = st.st_blocks * 512ull; |
| LOG(INFO) << "Found existing disk at " << disk_path.value() |
| << " with current size " << current_size << " and usage " |
| << current_usage; |
| |
| // Automatically extend existing disk images if disk_size was not specified. |
| if (request.disk_size() == 0) { |
| // If the user.crostini.user_chosen_size xattr exists, don't resize the |
| // disk. (The value stored in the xattr is ignored; only its existence |
| // matters.) |
| if (IsDiskUserChosenSize(disk_path.value())) { |
| LOG(INFO) << "Disk image has " << kDiskImageUserChosenSizeXattr |
| << " xattr - keeping existing size " << current_size; |
| } else { |
| uint64_t disk_size = CalculateDesiredDiskSize(disk_path, current_usage); |
| if (disk_size > current_size) { |
| LOG(INFO) << "Expanding disk image from " << current_size << " to " |
| << disk_size; |
| if (expand_disk_image(disk_path.value().c_str(), disk_size) != 0) { |
| // If expanding the disk failed, continue with a warning. |
| // Currently, raw images can be resized, and qcow2 images cannot. |
| LOG(WARNING) << "Failed to expand disk image " << disk_path.value(); |
| } |
| } else { |
| LOG(INFO) << "Current size " << current_size |
| << " is already at least requested size " << disk_size |
| << " - not expanding"; |
| } |
| } |
| } |
| |
| response.set_status(DISK_STATUS_EXISTS); |
| response.set_disk_path(disk_path.value()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!GetDiskPathFromName(request.vm_name(), request.cryptohome_id(), |
| request.storage_location(), |
| true, /* create_parent_dir */ |
| &disk_path, request.image_type())) { |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason("Failed to create vm image"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| if (request.storage_location() == STORAGE_CRYPTOHOME_PLUGINVM) { |
| // Get the FD to fill with disk image data. |
| base::ScopedFD in_fd; |
| if (!reader.PopFileDescriptor(&in_fd)) { |
| LOG(ERROR) << "CreateDiskImage: no fd found"; |
| response.set_failure_reason("no source fd found"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // 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(request.vm_name(), request.cryptohome_id(), |
| false /* create */, &iso_dir)) { |
| LOG(ERROR) << "Unable to determine directory for ISOs"; |
| |
| response.set_failure_reason("Unable to determine ISO directory"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::vector<string> params( |
| std::make_move_iterator(request.mutable_params()->begin()), |
| std::make_move_iterator(request.mutable_params()->end())); |
| |
| auto 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(DiskOpInfo(std::move(op))); |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::Bind(&Service::RunDiskImageOperation, |
| weak_ptr_factory_.GetWeakPtr(), std::move(uuid))); |
| } |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| uint64_t disk_size = request.disk_size() |
| ? request.disk_size() |
| : CalculateDesiredDiskSize(disk_path, 0); |
| |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| if (request.disk_size() != 0) { |
| LOG(INFO) |
| << "Disk size specified in request; creating user-chosen-size image"; |
| if (!SetUserChosenSizeAttr(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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| LOG(INFO) << "Disk image preallocated"; |
| response.set_status(DISK_STATUS_CREATED); |
| response.set_disk_path(disk_path.value()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| response.set_status(DISK_STATUS_CREATED); |
| response.set_disk_path(disk_path.value()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| LOG(INFO) << "Creating qcow2 disk at: " << disk_path.value() << " size " |
| << disk_size; |
| int ret = create_qcow_with_size(disk_path.value().c_str(), disk_size); |
| if (ret != 0) { |
| LOG(ERROR) << "Failed to create qcow2 disk image: " << strerror(ret); |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason("Failed to create qcow2 disk image"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| response.set_disk_path(disk_path.value()); |
| response.set_status(DISK_STATUS_CREATED); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::DestroyDiskImage( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received DestroyDiskImage request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| DestroyDiskImageRequest request; |
| DestroyDiskImageResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse DestroyDiskImageRequest from message"; |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason("Unable to parse DestroyDiskRequest"); |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Stop the associated VM if it is still running. |
| auto iter = FindVm(request.cryptohome_id(), request.vm_name()); |
| if (iter != vms_.end()) { |
| LOG(INFO) << "Shutting down VM"; |
| |
| // Notify that we are about to stop a VM. |
| NotifyVmStopping(iter->first, iter->second->GetInfo().cid); |
| if (!iter->second->Shutdown()) { |
| LOG(ERROR) << "Unable to shut down VM"; |
| |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason("Unable to shut down VM"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Notify that we have stopped a VM. |
| NotifyVmStopped(iter->first, iter->second->GetInfo().cid, |
| DESTROY_DISK_IMAGE_REQUESTED); |
| vms_.erase(iter); |
| } |
| |
| base::FilePath disk_path; |
| StorageLocation location; |
| if (!CheckVmExists(request.vm_name(), request.cryptohome_id(), &disk_path, |
| &location)) { |
| response.set_status(DISK_STATUS_DOES_NOT_EXIST); |
| response.set_failure_reason("No such image"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!EraseGuestSshKeys(request.cryptohome_id(), request.vm_name())) { |
| // 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. |
| VmId vm_id(request.cryptohome_id(), request.vm_name()); |
| bool registered; |
| if (!pvm::dispatcher::IsVmRegistered(bus_, vmplugin_service_proxy_, vm_id, |
| ®istered)) { |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason( |
| "failed to check Plugin VM registration status"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| base::FilePath iso_dir; |
| if (GetPluginIsoDirectory(vm_id.name(), vm_id.owner_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| // Delete GPU shader disk cache. |
| base::FilePath gpu_cache_path = |
| GetVmGpuCachePath(request.cryptohome_id(), request.vm_name()); |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| response.set_status(DISK_STATUS_DESTROYED); |
| writer.AppendProtoAsArrayOfBytes(response); |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::ResizeDiskImage( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received ResizeDiskImage request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ResizeDiskImageRequest request; |
| ResizeDiskImageResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ResizeDiskImageRequest from message"; |
| response.set_status(DISK_STATUS_FAILED); |
| response.set_failure_reason("Unable to parse ResizeDiskImageRequest"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| base::FilePath disk_path; |
| StorageLocation location; |
| if (!CheckVmExists(request.vm_name(), request.cryptohome_id(), &disk_path, |
| &location)) { |
| response.set_status(DISK_STATUS_DOES_NOT_EXIST); |
| response.set_failure_reason("Resize image doesn't exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| 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( |
| VmId(request.cryptohome_id(), request.vm_name()), location, disk_path, |
| size, base::Bind(&Service::ResizeDisk, weak_ptr_factory_.GetWeakPtr()), |
| base::Bind(&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(DiskOpInfo(std::move(op))); |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&Service::RunDiskImageOperation, |
| weak_ptr_factory_.GetWeakPtr(), std::move(uuid))); |
| } else if (op->status() == DISK_STATUS_RESIZED) { |
| DiskImageStatus status = DISK_STATUS_RESIZED; |
| std::string failure_reason; |
| FinishResize(request.cryptohome_id(), request.vm_name(), location, &status, |
| &failure_reason); |
| if (status != DISK_STATUS_RESIZED) { |
| response.set_status(status); |
| response.set_failure_reason(failure_reason); |
| } |
| } |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| void Service::ResizeDisk(const std::string& owner_id, |
| const std::string& vm_name, |
| StorageLocation location, |
| uint64_t new_size, |
| DiskImageStatus* status, |
| std::string* failure_reason) { |
| auto iter = FindVm(owner_id, vm_name); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Unable to find VM " << vm_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 std::string& owner_id, |
| const std::string& vm_name, |
| StorageLocation location, |
| uint64_t target_size, |
| DiskImageStatus* status, |
| std::string* failure_reason) { |
| auto iter = FindVm(owner_id, vm_name); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Unable to find VM " << vm_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(owner_id, vm_name, location, status, failure_reason); |
| } |
| } |
| |
| void Service::FinishResize(const std::string& owner_id, |
| const std::string& vm_name, |
| StorageLocation location, |
| DiskImageStatus* status, |
| std::string* failure_reason) { |
| base::FilePath disk_path; |
| if (!GetDiskPathFromName(vm_name, owner_id, location, |
| false, /* create_parent_dir */ |
| &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 (!SetUserChosenSizeAttr(fd)) { |
| LOG(ERROR) << "Failed to set user-chosen size xattr"; |
| *failure_reason = "Failed to set user-chosen size xattr"; |
| *status = DISK_STATUS_FAILED; |
| return; |
| } |
| } |
| |
| std::unique_ptr<dbus::Response> Service::ExportDiskImage( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received ExportDiskImage request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ExportDiskImageResponse response; |
| response.set_status(DISK_STATUS_FAILED); |
| |
| ExportDiskImageRequest request; |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ExportDiskImageRequest from message"; |
| response.set_failure_reason("Unable to parse ExportDiskRequest"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| base::FilePath disk_path; |
| StorageLocation location; |
| if (!CheckVmExists(request.vm_name(), request.cryptohome_id(), &disk_path, |
| &location)) { |
| response.set_status(DISK_STATUS_DOES_NOT_EXIST); |
| response.set_failure_reason("Export image doesn't exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Get the FD to fill with disk image data. |
| base::ScopedFD storage_fd; |
| if (!reader.PopFileDescriptor(&storage_fd)) { |
| LOG(ERROR) << "export: no fd found"; |
| response.set_failure_reason("export: no fd found"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| base::ScopedFD digest_fd; |
| if (request.generate_sha256_digest() && |
| !reader.PopFileDescriptor(&digest_fd)) { |
| LOG(ERROR) << "export: no digest fd found"; |
| response.set_failure_reason("export: no digest fd found"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| VmId vm_id(request.cryptohome_id(), request.vm_name()); |
| |
| if (!request.force()) { |
| if (FindVm(vm_id) != vms_.end()) { |
| LOG(ERROR) << "VM is currently running"; |
| response.set_failure_reason("VM is currently running"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| if (!is_shut_down) { |
| LOG(ERROR) << "VM is not shut down"; |
| response.set_failure_reason("VM needs to be shut down for exporting"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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(DiskOpInfo(std::move(op))); |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&Service::RunDiskImageOperation, |
| weak_ptr_factory_.GetWeakPtr(), std::move(uuid))); |
| } |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::ImportDiskImage( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received ImportDiskImage request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ImportDiskImageResponse response; |
| response.set_status(DISK_STATUS_FAILED); |
| |
| ImportDiskImageRequest request; |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ImportDiskImageRequest from message"; |
| response.set_failure_reason("Unable to parse ImportDiskRequest"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (CheckVmExists(request.vm_name(), request.cryptohome_id())) { |
| response.set_status(DISK_STATUS_EXISTS); |
| response.set_failure_reason("VM/disk with such name already exists"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| 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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| base::FilePath disk_path; |
| if (!GetDiskPathFromName(request.vm_name(), request.cryptohome_id(), |
| request.storage_location(), |
| true, /* create_parent_dir */ |
| &disk_path)) { |
| response.set_failure_reason("Failed to set up vm image name"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // Get the FD to fill with disk image data. |
| base::ScopedFD in_fd; |
| if (!reader.PopFileDescriptor(&in_fd)) { |
| LOG(ERROR) << "import: no fd found"; |
| response.set_failure_reason("import: no fd found"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto op = PluginVmImportOperation::Create( |
| std::move(in_fd), disk_path, request.source_size(), |
| VmId(request.cryptohome_id(), request.vm_name()), 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(DiskOpInfo(std::move(op))); |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&Service::RunDiskImageOperation, |
| weak_ptr_factory_.GetWeakPtr(), std::move(uuid))); |
| } |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| 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); |
| dbus::Signal signal(kVmConciergeInterface, kDiskImageProgressSignal); |
| dbus::MessageWriter(&signal).AppendProtoAsArrayOfBytes(status); |
| exported_object_->SendSignal(&signal); |
| |
| // 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::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&Service::RunDiskImageOperation, |
| weak_ptr_factory_.GetWeakPtr(), std::move(uuid))); |
| } |
| } |
| |
| std::unique_ptr<dbus::Response> Service::CheckDiskImageStatus( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received DiskImageStatus request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| DiskImageStatusResponse response; |
| response.set_status(DISK_STATUS_FAILED); |
| |
| DiskImageStatusRequest request; |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse DiskImageStatusRequest from message"; |
| response.set_failure_reason("Unable to parse DiskImageStatusRequest"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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() || iter->canceled) { |
| LOG(ERROR) << "Unknown command uuid in DiskImageStatusRequest"; |
| response.set_failure_reason("Unknown command uuid"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto op = iter->op.get(); |
| FormatDiskImageStatus(op, &response); |
| writer.AppendProtoAsArrayOfBytes(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); |
| } |
| |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::CancelDiskImageOperation( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received CancelDiskImage request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| CancelDiskImageResponse response; |
| response.set_success(false); |
| |
| CancelDiskImageRequest request; |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse CancelDiskImageRequest from message"; |
| response.set_failure_reason("Unable to parse CancelDiskImageRequest"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto op = iter->op.get(); |
| if (op->status() != DISK_STATUS_IN_PROGRESS) { |
| response.set_failure_reason("Command is no longer in progress"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| // 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); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::ListVmDisks( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ListVmDisksRequest request; |
| ListVmDisksResponse response; |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ListVmDisksRequest from message"; |
| response.set_success(false); |
| response.set_failure_reason("Unable to parse ListVmDisksRequest"); |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| 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; |
| } |
| } |
| } |
| |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::GetContainerSshKeys( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received GetContainerSshKeys request"; |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ContainerSshKeysRequest request; |
| ContainerSshKeysResponse response; |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ContainerSshKeysRequest from message"; |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.cryptohome_id().empty()) { |
| LOG(ERROR) << "Cryptohome ID is not set in ContainerSshKeysRequest"; |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.cryptohome_id(), request.vm_name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist:" << request.vm_name(); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::string container_name = request.container_name().empty() |
| ? kDefaultContainerName |
| : request.container_name(); |
| response.set_container_public_key(GetGuestSshPublicKey( |
| request.cryptohome_id(), request.vm_name(), container_name)); |
| response.set_container_private_key(GetGuestSshPrivateKey( |
| request.cryptohome_id(), request.vm_name(), container_name)); |
| response.set_host_public_key(GetHostSshPublicKey(request.cryptohome_id())); |
| response.set_host_private_key(GetHostSshPrivateKey(request.cryptohome_id())); |
| response.set_hostname(base::StringPrintf( |
| "%s.%s.linux.test", container_name.c_str(), request.vm_name().c_str())); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::AttachUsbDevice( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received AttachUsbDevice request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| AttachUsbDeviceRequest request; |
| AttachUsbDeviceResponse response; |
| base::ScopedFD fd; |
| |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse AttachUsbDeviceRequest from message"; |
| response.set_reason("Unable to parse protobuf"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (!reader.PopFileDescriptor(&fd)) { |
| LOG(ERROR) << "Unable to parse file descriptor from dbus message"; |
| response.set_reason("Unable to parse file descriptor"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.vm_name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM " << request.vm_name() << " does not exist"; |
| response.set_reason("Requested VM does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.bus_number() > 0xFF) { |
| LOG(ERROR) << "Bus number out of valid range " << request.bus_number(); |
| response.set_reason("Invalid bus number"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.port_number() > 0xFF) { |
| LOG(ERROR) << "Port number out of valid range " << request.port_number(); |
| response.set_reason("Invalid port number"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.vendor_id() > 0xFFFF) { |
| LOG(ERROR) << "Vendor ID out of valid range " << request.vendor_id(); |
| response.set_reason("Invalid vendor ID"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.product_id() > 0xFFFF) { |
| LOG(ERROR) << "Product ID out of valid range " << request.product_id(); |
| response.set_reason("Invalid product ID"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| UsbControlResponse usb_response; |
| if (!iter->second->AttachUsbDevice( |
| request.bus_number(), request.port_number(), request.vendor_id(), |
| request.product_id(), fd.get(), &usb_response)) { |
| LOG(ERROR) << "Failed to attach USB device: " << usb_response.reason; |
| response.set_reason(std::move(usb_response.reason)); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| response.set_success(true); |
| response.set_guest_port(usb_response.port); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::DetachUsbDevice( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received DetachUsbDevice request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| DetachUsbDeviceRequest request; |
| DetachUsbDeviceResponse response; |
| |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse DetachUsbDeviceRequest from message"; |
| response.set_reason("Unable to parse protobuf"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.vm_name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| response.set_reason("Requested VM does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| if (request.guest_port() > 0xFF) { |
| LOG(ERROR) << "Guest port number out of valid range " |
| << request.guest_port(); |
| response.set_reason("Invalid guest port number"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| UsbControlResponse usb_response; |
| if (!iter->second->DetachUsbDevice(request.guest_port(), &usb_response)) { |
| LOG(ERROR) << "Failed to detach USB device"; |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::ListUsbDevices( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received ListUsbDevices request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| ListUsbDeviceRequest request; |
| ListUsbDeviceResponse response; |
| |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ListUsbDeviceRequest from message"; |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.vm_name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::vector<UsbDevice> usb_list; |
| if (!iter->second->ListUsbDevice(&usb_list)) { |
| LOG(ERROR) << "Failed to list USB devices"; |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| 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.vid); |
| usb_proto->set_product_id(usb.pid); |
| } |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| void Service::ComposeDnsResponse(dbus::MessageWriter* writer) { |
| 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); |
| } |
| writer->AppendProtoAsArrayOfBytes(dns_settings); |
| } |
| |
| std::unique_ptr<dbus::Response> Service::GetDnsSettings( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received GetDnsSettings request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageWriter writer(dbus_response.get()); |
| ComposeDnsResponse(&writer); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::SetVmCpuRestriction( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| VLOG(3) << "Received SetVmCpuRestriction request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| SetVmCpuRestrictionRequest request; |
| SetVmCpuRestrictionResponse response; |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse SetVmCpuRestrictionRequest from message"; |
| response.set_success(false); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_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); |
| break; |
| default: |
| LOG(ERROR) << "Unknown cpu_group"; |
| break; |
| } |
| |
| response.set_success(success); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| std::unique_ptr<dbus::Response> Service::SetVmId( |
| dbus::MethodCall* method_call) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received SetVmId request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| dbus::MessageWriter writer(dbus_response.get()); |
| |
| SetVmIdRequest request; |
| SetVmIdResponse response; |
| |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse SetVmIdRequest from message"; |
| response.set_failure_reason("Unable to parse SetVmIdRequest from message"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto iter = FindVm(request.src_owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| response.set_failure_reason("Requested VM does not exist"); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| auto vm = std::move(iter->second); |
| auto cid = vm->GetInfo().cid; |
| auto old_id = iter->first; |
| vms_.erase(iter); |
| VmId new_id(request.dest_owner_id(), request.name()); |
| vms_[new_id] = std::move(vm); |
| vms_[new_id]->VmIdChanged(); |
| |
| SendVmIdChangedSignal(new_id, old_id, cid); |
| |
| response.set_success(true); |
| writer.AppendProtoAsArrayOfBytes(response); |
| return dbus_response; |
| } |
| |
| void Service::ReclaimVmMemory( |
| dbus::MethodCall* method_call, |
| dbus::ExportedObject::ResponseSender response_sender) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| LOG(INFO) << "Received ReclaimVmMemory request"; |
| |
| std::unique_ptr<dbus::Response> dbus_response( |
| dbus::Response::FromMethodCall(method_call)); |
| |
| dbus::MessageReader reader(method_call); |
| ReclaimVmMemoryRequest request; |
| ReclaimVmMemoryResponse response; |
| response.set_success(false); |
| |
| if (!reader.PopArrayOfBytesAsProto(&request)) { |
| LOG(ERROR) << "Unable to parse ReclaimVmMemoryRequest from message"; |
| response.set_failure_reason( |
| "Unable to parse ReclaimVmMemoryRequest from message"); |
| dbus::MessageWriter writer(dbus_response.get()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| std::move(response_sender).Run(std::move(dbus_response)); |
| return; |
| } |
| |
| auto iter = FindVm(request.owner_id(), request.name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Requested VM does not exist"; |
| response.set_failure_reason("Requested VM does not exist"); |
| dbus::MessageWriter writer(dbus_response.get()); |
| writer.AppendProtoAsArrayOfBytes(response); |
| std::move(response_sender).Run(std::move(dbus_response)); |
| return; |
| } |
| |
| const pid_t pid = iter->second->GetInfo().pid; |
| reclaim_thread_.task_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&ReclaimVmMemoryInternal, pid, std::move(dbus_response)), |
| base::BindOnce(&Service::OnReclaimVmMemory, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(response_sender))); |
| } |
| |
| void Service::OnReclaimVmMemory( |
| dbus::ExportedObject::ResponseSender response_sender, |
| std::unique_ptr<dbus::Response> dbus_response) { |
| DCHECK(dbus_response); |
| std::move(response_sender).Run(std::move(dbus_response)); |
| } |
| |
| void Service::OnResolvConfigChanged(std::vector<string> nameservers, |
| std::vector<string> search_domains) { |
| if (nameservers_ == nameservers && search_domains_ == search_domains) { |
| // Only update guests if the nameservers and search domains changed. |
| 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. |
| dbus::Signal signal(kVmConciergeInterface, kDnsSettingsChangedSignal); |
| dbus::MessageWriter writer(&signal); |
| ComposeDnsResponse(&writer); |
| exported_object_->SendSignal(&signal); |
| } |
| |
| void Service::OnDefaultNetworkServiceChanged() { |
| 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) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| 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); |
| writer.AppendProtoAsArrayOfBytes(request); |
| if (!brillo::dbus_utils::CallDBusMethod( |
| bus_, cicerone_service_proxy_, &method_call, |
| dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)) { |
| LOG(ERROR) << "Failed notifying cicerone of VM startup"; |
| } |
| } |
| |
| void Service::SendVmStartedSignal(const VmId& vm_id, |
| const vm_tools::concierge::VmInfo& vm_info, |
| vm_tools::concierge::VmStatus status) { |
| dbus::Signal signal(kVmConciergeInterface, kVmStartedSignal); |
| vm_tools::concierge::VmStartedSignal proto; |
| proto.set_owner_id(vm_id.owner_id()); |
| proto.set_name(vm_id.name()); |
| proto.mutable_vm_info()->CopyFrom(vm_info); |
| proto.set_status(status); |
| dbus::MessageWriter(&signal).AppendProtoAsArrayOfBytes(proto); |
| exported_object_->SendSignal(&signal); |
| } |
| |
| void Service::SendVmStartingUpSignal( |
| const VmId& vm_id, const vm_tools::concierge::VmInfo& vm_info) { |
| dbus::Signal signal(kVmConciergeInterface, kVmStartingUpSignal); |
| vm_tools::concierge::VmStartedSignal proto; |
| proto.set_owner_id(vm_id.owner_id()); |
| proto.set_name(vm_id.name()); |
| proto.mutable_vm_info()->CopyFrom(vm_info); |
| dbus::MessageWriter(&signal).AppendProtoAsArrayOfBytes(proto); |
| exported_object_->SendSignal(&signal); |
| } |
| |
| void Service::NotifyVmStopping(const VmId& vm_id, int64_t cid) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| // 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 (!brillo::dbus_utils::CallDBusMethod( |
| bus_, cicerone_service_proxy_, &method_call, |
| dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)) { |
| LOG(ERROR) << "Failed notifying cicerone of stopping VM"; |
| } |
| } |
| |
| void Service::NotifyVmStopped(const VmId& vm_id, |
| int64_t cid, |
| VmStopReason reason) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| // 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 (!brillo::dbus_utils::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. |
| dbus::Signal signal(kVmConciergeInterface, kVmStoppedSignal); |
| 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); |
| dbus::MessageWriter(&signal).AppendProtoAsArrayOfBytes(proto); |
| exported_object_->SendSignal(&signal); |
| } |
| |
| void Service::SendVmIdChangedSignal(const VmId& id, |
| const VmId& prev_id, |
| int64_t cid) { |
| dbus::Signal signal(kVmConciergeInterface, kVmIdChangedSignal); |
| vm_tools::concierge::VmIdChangedSignal proto; |
| proto.set_owner_id(id.owner_id()); |
| proto.set_name(id.name()); |
| proto.set_cid(cid); |
| proto.set_prev_owner_id(prev_id.owner_id()); |
| dbus::MessageWriter(&signal).AppendProtoAsArrayOfBytes(proto); |
| exported_object_->SendSignal(&signal); |
| } |
| |
| std::string Service::GetContainerToken(const VmId& vm_id, |
| const std::string& container_name) { |
| DCHECK(sequence_checker_.CalledOnValidSequence()); |
| 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 = |
| brillo::dbus_utils::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(); |
| } |
| |
| void Service::OnTremplinStartedSignal(dbus::Signal* signal) { |
| 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; |
| } |
| |
| auto iter = FindVm(tremplin_started_signal.owner_id(), |
| tremplin_started_signal.vm_name()); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Received signal from an unknown vm." |
| << VmId(tremplin_started_signal.owner_id(), |
| tremplin_started_signal.vm_name()); |
| return; |
| } |
| LOG(INFO) << "Received TremplinStartedSignal for " << iter->first; |
| iter->second->SetTremplinStarted(); |
| } |
| |
| void Service::OnVmToolsStateChangedSignal(dbus::Signal* signal) { |
| string owner_id, vm_name; |
| bool running; |
| if (!pvm::dispatcher::ParseVmToolsChangedSignal(signal, &owner_id, &vm_name, |
| &running)) { |
| return; |
| } |
| |
| auto iter = FindVm(owner_id, vm_name); |
| if (iter == vms_.end()) { |
| LOG(ERROR) << "Received signal from an unknown vm " |
| << VmId(owner_id, vm_name); |
| return; |
| } |
| LOG(INFO) << "Received VmToolsStateChangedSignal 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() { |
| for (const auto& pair : vms_) { |
| auto& vm = pair.second; |
| if (vm->UsesExternalSuspendSignals()) { |
| continue; |
| } |
| vm->Suspend(); |
| } |
| } |
| |
| void Service::HandleSuspendDone() { |
| for (const auto& vm_entry : vms_) { |
| auto& vm = vm_entry.second; |
| if (vm->UsesExternalSuspendSignals()) { |
| continue; |
| } |
| |
| vm->Resume(); |
| |
| 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) { |
| return vms_.find(vm_id); |
| } |
| |
| Service::VmMap::iterator Service::FindVm(const std::string& owner_id, |
| const std::string& vm_name) { |
| return vms_.find(VmId(owner_id, vm_name)); |
| } |
| |
| base::FilePath Service::GetVmImagePath(const std::string& dlc_id, |
| std::string* failure_reason) { |
| DCHECK(failure_reason); |
| base::Optional<std::string> dlc_root = |
| AsyncNoReject(bus_->GetDBusTaskRunner(), |
| base::BindOnce( |
| [](DlcHelper* dlc_helper, const std::string& dlc_id, |
| std::string* out_failure_reason) { |
| return dlc_helper->GetRootPath(dlc_id, |
| out_failure_reason); |
| }, |
| dlcservice_client_.get(), dlc_id, failure_reason)) |
| .Get() |
| .val; |
| if (!dlc_root.has_value()) { |
| // On an error, failure_reason will be set by GetRootPath(). |
| return {}; |
| } |
| return base::FilePath(dlc_root.value()); |
| } |
| |
| Service::VMImageSpec Service::GetImageSpec( |
| const vm_tools::concierge::VirtualMachineSpec& vm, |
| const base::Optional<base::ScopedFD>& kernel_fd, |
| const base::Optional<base::ScopedFD>& rootfs_fd, |
| const base::Optional<base::ScopedFD>& initrd_fd, |
| const base::Optional<base::ScopedFD>& bios_fd, |
| bool is_termina, |
| string* failure_reason) { |
| DCHECK(failure_reason); |
| DCHECK(failure_reason->empty()); |
| |
| // A VM image is trusted when both: |
| // 1) This daemon (or a trusted daemon) chooses the kernel and rootfs path. |
| // 2) The chosen VM is a first-party VM. |
| // In practical terms this is true iff we are booting termina without |
| // specifying kernel and rootfs image. |
| bool is_trusted_image = is_termina; |
| |
| base::FilePath kernel, rootfs, initrd, bios; |
| if (kernel_fd.has_value()) { |
| // User-chosen kernel is untrusted. |
| is_trusted_image = false; |
| |
| int raw_fd = kernel_fd.value().get(); |
| *failure_reason = RemoveCloseOnExec(raw_fd); |
| if (!failure_reason->empty()) |
| return {}; |
| kernel = base::FilePath(kProcFileDescriptorsPath) |
| .Append(base::NumberToString(raw_fd)); |
| } else { |
| kernel = base::FilePath(vm.kernel()); |
| } |
| |
| if (rootfs_fd.has_value()) { |
| // User-chosen rootfs is untrusted. |
| is_trusted_image = false; |
| |
| int raw_fd = rootfs_fd.value().get(); |
| *failure_reason = RemoveCloseOnExec(raw_fd); |
| if (!failure_reason->empty()) |
| return {}; |
| rootfs = base::FilePath(kProcFileDescriptorsPath) |
| .Append(base::NumberToString(raw_fd)); |
| } else { |
| rootfs = base::FilePath(vm.rootfs()); |
| } |
| |
| if (initrd_fd.has_value()) { |
| // User-chosen initrd is untrusted. |
| is_trusted_image = false; |
| |
| int raw_fd = initrd_fd.value().get(); |
| *failure_reason = RemoveCloseOnExec(raw_fd); |
| if (!failure_reason->empty()) |
| return {}; |
| initrd = base::FilePath(kProcFileDescriptorsPath) |
| .Append(base::NumberToString(raw_fd)); |
| } else { |
| initrd = base::FilePath(vm.initrd()); |
| } |
| |
| if (bios_fd.has_value()) { |
| // User-chosen bios is untrusted. |
| is_trusted_image = false; |
| |
| int raw_fd = bios_fd.value().get(); |
| *failure_reason = RemoveCloseOnExec(raw_fd); |
| if (!failure_reason->empty()) |
| return {}; |
| bios = base::FilePath(kProcFileDescriptorsPath) |
| .Append(base::NumberToString(raw_fd)); |
| } |
| |
| base::FilePath vm_path; |
| // As a legacy fallback, use the component rather than the DLC. |
| // |
| // TODO(crbug/953544): remove this once we no longer distribute termina as a |
| // component. |
| if (vm.dlc_id().empty() && is_termina) { |
| vm_path = GetLatestVMPath(); |
| if (vm_path.empty()) { |
| *failure_reason = "Termina component is not loaded"; |
| return {}; |
| } |
| } else if (!vm.dlc_id().empty()) { |
| vm_path = GetVmImagePath(vm.dlc_id(), failure_reason); |
| if (vm_path.empty()) |
| return {}; |
| } |
| |
| // Pull in the DLC-provided files if requested. |
| if (!kernel_fd.has_value() && !vm_path.empty()) |
| kernel = vm_path.Append(kVmKernelName); |
| if (!rootfs_fd.has_value() && !vm_path.empty()) |
| rootfs = vm_path.Append(kVmRootfsName); |
| |
| base::FilePath tools_disk; |
| if (!vm.tools_dlc_id().empty()) { |
| base::FilePath tools_disk_path = |
| GetVmImagePath(vm.tools_dlc_id(), failure_reason); |
| if (tools_disk_path.empty()) |
| return {}; |
| tools_disk = tools_disk_path.Append(kVmToolsDiskName); |
| } |
| if (tools_disk.empty() && !vm_path.empty()) |
| tools_disk = vm_path.Append(kVmToolsDiskName); |
| |
| return VMImageSpec{ |
| .kernel = std::move(kernel), |
| .initrd = std::move(initrd), |
| .rootfs = std::move(rootfs), |
| .bios = std::move(bios), |
| .tools_disk = std::move(tools_disk), |
| .is_trusted_image = is_trusted_image, |
| }; |
| } |
| |
| base::FilePath Service::PrepareVmGpuCachePath(const std::string& owner_id, |
| const std::string& vm_name) { |
| base::FilePath cache_path = GetVmGpuCachePath(owner_id, vm_name); |
| base::FilePath bootid_path = cache_path.DirName(); |
| base::FilePath base_path = bootid_path.DirName(); |
| |
| base::AutoLock guard(cache_mutex_); |
| |
| // In order to always provide an empty GPU shader cache on each boot, we hash |
| // the boot_id and erase the whole GPU cache if a directory matching the |
| // current boot_id is not found. |
| // For example: |
| // VM cache dir: /run/daemon-store/crosvm/<uid>/gpucache/<bootid>/<vmid>/ |
| // Boot dir: /run/daemon-store/crosvm/<uid>/gpucache/<bootid>/ |
| // Base dir: /run/daemon-store/crosvm/<uid>/gpucache/ |
| // If Boot dir exists we know another VM has already created a fresh base |
| // dir during this boot. Otherwise, we erase Base dir to wipe out any |
| // previous Boot dir. |
| if (!base::DirectoryExists(bootid_path)) { |
| if (!base::DeletePathRecursively(base_path)) { |
| LOG(ERROR) << "Failed to delete gpu cache directory: " << base_path |
| << " shader caching will be disabled."; |
| return base::FilePath(); |
| } |
| } |
| |
| if (!base::DirectoryExists(cache_path)) { |
| base::File::Error dir_error; |
| if (!base::CreateDirectoryAndGetError(cache_path, &dir_error)) { |
| LOG(ERROR) << "Failed to create crosvm gpu cache directory in " |
| << cache_path << ": " << base::File::ErrorToString(dir_error); |
| return base::FilePath(); |
| } |
| } |
| return cache_path; |
| } |
| |
| } // namespace concierge |
| } // namespace vm_tools |