| // Copyright 2020 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 <limits.h> |
| #include <stdlib.h> |
| |
| #include <string> |
| |
| #include <base/files/file_util.h> |
| #include <base/logging.h> |
| #include <vboot/crossystem.h> |
| |
| #include "vm_tools/common/pstore.h" |
| #include "vm_tools/concierge/arc_vm.h" |
| #include "vm_tools/concierge/service.h" |
| #include "vm_tools/concierge/shared_data.h" |
| #include "vm_tools/concierge/vm_util.h" |
| |
| namespace vm_tools { |
| namespace concierge { |
| |
| namespace { |
| |
| // Android data directory. |
| constexpr char kAndroidDataDir[] = "/run/arcvm/android-data"; |
| |
| // Android stub volume directory for MyFiles and removable media. |
| constexpr char kStubVolumeSharedDir[] = "/run/arcvm/media"; |
| |
| // Path to the VM guest kernel. |
| constexpr char kKernelPath[] = "/opt/google/vms/android/vmlinux"; |
| |
| // Path to the VM rootfs image file. |
| constexpr char kRootfsPath[] = "/opt/google/vms/android/system.raw.img"; |
| |
| // Path to the VM fstab file. |
| constexpr char kFstabPath[] = "/run/arcvm/host_generated/fstab"; |
| |
| // Returns |image_path| on production. Returns a canonicalized path of the image |
| // file when in dev mode. |
| base::FilePath GetImagePath(const base::FilePath& image_path, |
| bool is_dev_mode) { |
| if (!is_dev_mode) |
| return image_path; |
| |
| // When in dev mode, the Android images might be on the stateful partition and |
| // |kRootfsPath| might be a symlink to the stateful partition image file. In |
| // that case, we need to use the resolved path so that brillo::SafeFD calls |
| // can handle the path without errors. The same is true for vendor.raw.image |
| // too. On the other hand, when in production mode, we should NEVER do the |
| // special handling. In production, the images files in /opt should NEVER ever |
| // be a symlink. |
| |
| // We cannot use base::NormalizeFilePath because the function fails |
| // if |path| points to a directory (for Windows compatibility.) |
| char buf[PATH_MAX] = {}; |
| if (realpath(image_path.value().c_str(), buf)) |
| return base::FilePath(buf); |
| if (errno != ENOENT) |
| PLOG(WARNING) << "Failed to resolve " << image_path.value(); |
| return image_path; |
| } |
| |
| // Returns true if the path is a valid demo image path. |
| bool IsValidDemoImagePath(const base::FilePath& path) { |
| // A valid demo image path looks like: |
| // /run/imageloader/demo-mode-resources/<version>/android_demo_apps.squash |
| // <version> part looks like 0.12.34.56 ("[0-9]+(.[0-9]+){0,3}" in regex). |
| std::vector<std::string> components; |
| path.GetComponents(&components); |
| return components.size() == 6 && components[0] == "/" && |
| components[1] == "run" && components[2] == "imageloader" && |
| components[3] == "demo-mode-resources" && |
| base::ContainsOnlyChars(components[4], "0123456789.") && |
| !base::StartsWith(components[4], ".") && |
| components[5] == "android_demo_apps.squash"; |
| } |
| |
| // Returns true if the path is a valid data image path. |
| bool IsValidDataImagePath(const base::FilePath& path) { |
| // A valid data image path looks like: |
| // /run/daemon-store/crosvm/<hash>/YXJjdm0=.img |
| std::vector<std::string> components; |
| path.GetComponents(&components); |
| return components.size() == 6 && components[0] == "/" && |
| components[1] == "run" && components[2] == "daemon-store" && |
| components[3] == "crosvm" && |
| base::ContainsOnlyChars(components[4], "0123456789abcdef") && |
| components[5] == "YXJjdm0=.img"; |
| } |
| |
| // TODO(hashimoto): Move VM configuration logic from chrome to concierge and |
| // remove this function. b/219677829 |
| // Returns true if the StartArcVmRequest contains valid ARCVM config values. |
| bool ValidateStartArcVmRequest(StartArcVmRequest* request) { |
| // List of allowed kernel parameters. |
| const std::set<std::string> kAllowedKernelParams = { |
| "androidboot.arc_generate_pai=1", |
| "androidboot.arcvm_mount_debugfs=1", |
| "androidboot.disable_download_provider=1", |
| "androidboot.disable_media_store_maintenance=1", |
| "androidboot.vshd_service_override=vshd_for_test", |
| "androidboot.arc.tts.caching=1", |
| "androidboot.arc_enable_gmscore_lmk_protection=1", |
| "rw", |
| }; |
| |
| // List of allowed kernel parameter prefixes. |
| const std::vector<std::string> kAllowedKernelParamPrefixes = { |
| "androidboot.arc_custom_tabs=", |
| "androidboot.arc_dalvik_memory_profile=", |
| "androidboot.arc_file_picker=", |
| "androidboot.arcvm.logd.size=", |
| "androidboot.arcvm_metrics_mem_psi_period=", |
| "androidboot.arcvm_ureadahead_mode=", |
| "androidboot.arcvm_virtio_blk_data=", |
| "androidboot.chromeos_channel=", |
| "androidboot.dev_mode=", |
| "androidboot.disable_runas=", |
| "androidboot.disable_system_default_app=", |
| "androidboot.enable_notifications_refresh=", |
| "androidboot.host_is_in_vm=", |
| "androidboot.iioservice_present=", |
| "androidboot.keyboard_shortcut_helper_integration=", |
| "androidboot.lcd_density=", |
| "androidboot.native_bridge=", |
| "androidboot.play_store_auto_update=", |
| "androidboot.usap_profile=", |
| "androidboot.zram_size=", |
| // TODO(hashimoto): This param was removed in R98. Remove this. |
| "androidboot.image_copy_paste_compat=", |
| }; |
| // Filter kernel params. |
| const std::vector<std::string> params(request->params().begin(), |
| request->params().end()); |
| request->clear_params(); |
| for (const auto& param : params) { |
| if (kAllowedKernelParams.count(param) != 0) { |
| request->add_params(param); |
| continue; |
| } |
| |
| auto it = std::find_if(kAllowedKernelParamPrefixes.begin(), |
| kAllowedKernelParamPrefixes.end(), |
| [¶m](const std::string& prefix) { |
| return base::StartsWith(param, prefix); |
| }); |
| if (it != kAllowedKernelParamPrefixes.end()) { |
| request->add_params(param); |
| continue; |
| } |
| |
| LOG(WARNING) << param << " was removed because it doesn't match with any " |
| << "allowed param or prefix"; |
| } |
| |
| // Validate disks. |
| constexpr char kEmptyDiskPath[] = "/dev/null"; |
| if (request->disks().size() < 1 || request->disks().size() > 4) { |
| LOG(ERROR) << "Invalid number of disks: " << request->disks().size(); |
| return false; |
| } |
| // Disk #0 must be /opt/google/vms/android/vendor.raw.img. |
| if (request->disks()[0].path() != "/opt/google/vms/android/vendor.raw.img") { |
| LOG(ERROR) << "Disk #0 has invalid path: " << request->disks()[0].path(); |
| return false; |
| } |
| // Disk #1 must be a valid demo image path or /dev/null. |
| if (request->disks().size() >= 2 && |
| !IsValidDemoImagePath(base::FilePath(request->disks()[1].path())) && |
| request->disks()[1].path() != kEmptyDiskPath) { |
| LOG(ERROR) << "Disk #1 has invalid path: " << request->disks()[1].path(); |
| return false; |
| } |
| // Disk #2 must be /opt/google/vms/android/apex/payload.img or /dev/null. |
| if (request->disks().size() >= 3 && |
| request->disks()[2].path() != |
| "/opt/google/vms/android/apex/payload.img" && |
| request->disks()[2].path() != kEmptyDiskPath) { |
| LOG(ERROR) << "Disk #2 has invalid path: " << request->disks()[2].path(); |
| return false; |
| } |
| // Disk #3 must be a valid data image path or /dev/null. |
| if (request->disks().size() >= kDataDiskIndex + 1) { |
| const std::string& disk_path = request->disks()[kDataDiskIndex].path(); |
| if (!IsValidDataImagePath(base::FilePath(disk_path)) && |
| disk_path != kEmptyDiskPath) { |
| LOG(ERROR) << "Disk #3 has invalid path: " << disk_path; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| StartVmResponse Service::StartArcVm(StartArcVmRequest request, |
| std::unique_ptr<dbus::MessageReader> reader, |
| VmMemoryId vm_memory_id) { |
| LOG(INFO) << "Received StartArcVm request"; |
| StartVmResponse response; |
| response.set_status(VM_STATUS_FAILURE); |
| |
| VmInfo* vm_info = response.mutable_vm_info(); |
| vm_info->set_vm_type(VmInfo::ARC_VM); |
| |
| if (request.disks_size() > kMaxExtraDisks) { |
| LOG(ERROR) << "Rejecting request with " << request.disks_size() |
| << " extra disks"; |
| |
| response.set_failure_reason("Too many extra disks"); |
| return response; |
| } |
| |
| // TODO(hashimoto): Move VM configuration logic from chrome to concierge and |
| // remove this check. b/219677829 |
| if (!ValidateStartArcVmRequest(&request)) { |
| response.set_failure_reason("Invalid request"); |
| return response; |
| } |
| |
| std::vector<Disk> disks; |
| // The rootfs can be treated as a disk as well and needs to be added before |
| // other disks. |
| Disk::Config config{}; |
| config.o_direct = false; |
| config.writable = request.rootfs_writable(); |
| const size_t rootfs_block_size = request.rootfs_block_size(); |
| if (rootfs_block_size) { |
| config.block_size = rootfs_block_size; |
| } |
| const bool is_dev_mode = (VbGetSystemPropertyInt("cros_debug") == 1); |
| disks.push_back( |
| Disk(GetImagePath(base::FilePath(kRootfsPath), is_dev_mode), config)); |
| for (const auto& disk : request.disks()) { |
| const base::FilePath path = |
| GetImagePath(base::FilePath(disk.path()), is_dev_mode); |
| if (!base::PathExists(path)) { |
| LOG(ERROR) << "Missing disk path: " << path; |
| response.set_failure_reason("One or more disk paths do not exist"); |
| return response; |
| } |
| config.writable = disk.writable(); |
| const size_t block_size = disk.block_size(); |
| if (block_size) { |
| config.block_size = block_size; |
| } |
| disks.push_back(Disk(path, config)); |
| } |
| |
| base::FilePath data_disk_path; |
| if (request.disks().size() > kDataDiskIndex) { |
| const std::string disk_path = request.disks()[kDataDiskIndex].path(); |
| if (IsValidDataImagePath(base::FilePath(disk_path))) |
| data_disk_path = base::FilePath(disk_path); |
| } |
| |
| // Create the runtime directory. |
| base::FilePath runtime_dir; |
| if (!base::CreateTemporaryDirInDir(base::FilePath(kRuntimeDir), "vm.", |
| &runtime_dir)) { |
| PLOG(ERROR) << "Unable to create runtime directory for VM"; |
| |
| response.set_failure_reason( |
| "Internal error: unable to create runtime directory"); |
| return response; |
| } |
| |
| // 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"); |
| return 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"); |
| return response; |
| } |
| |
| // Map the chronos user (1000) and the chronos-access group (1001) to the |
| // AID_EXTERNAL_STORAGE user and group (1077). |
| 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, |
| {{1000, 1077}}, {{1001, 1077}}); |
| if (!server_proxy) { |
| LOG(ERROR) << "Unable to start shared directory server"; |
| |
| response.set_failure_reason("Unable to start shared directory server"); |
| return response; |
| } |
| |
| uint32_t seneschal_server_handle = server_proxy->handle(); |
| vm_info->set_seneschal_server_handle(seneschal_server_handle); |
| |
| // Build the plugin params. |
| std::vector<std::string> params = { |
| "root=/dev/vda", |
| "init=/init", |
| // Note: Do not change the value "bertha". This string is checked in |
| // platform2/metrics/process_meter.cc to detect ARCVM's crosvm processes, |
| // for example. |
| "androidboot.hardware=bertha", |
| "androidboot.container=1", |
| }; |
| params.insert(params.end(), |
| std::make_move_iterator(request.mutable_params()->begin()), |
| std::make_move_iterator(request.mutable_params()->end())); |
| params.push_back(base::StringPrintf("androidboot.seneschal_server_port=%d", |
| seneschal_server_port)); |
| |
| // Start the VM and build the response. |
| ArcVmFeatures features; |
| features.rootfs_writable = request.rootfs_writable(); |
| features.use_dev_conf = !request.ignore_dev_conf(); |
| |
| if (request.has_balloon_policy()) { |
| const auto& params = request.balloon_policy(); |
| features.balloon_policy_params = (LimitCacheBalloonPolicy::Params){ |
| .reclaim_target_cache = params.reclaim_target_cache(), |
| .critical_target_cache = params.critical_target_cache(), |
| .moderate_target_cache = params.moderate_target_cache()}; |
| } |
| |
| base::FilePath data_dir = base::FilePath(kAndroidDataDir); |
| if (!base::PathExists(data_dir)) { |
| LOG(WARNING) << "Android data directory does not exist"; |
| |
| response.set_failure_reason("Android data directory does not exist"); |
| return response; |
| } |
| |
| VmId vm_id(request.owner_id(), request.name()); |
| SendVmStartingUpSignal(vm_id, *vm_info); |
| |
| const std::vector<uid_t> privileged_quota_uids = {0}; // Root is privileged. |
| std::string shared_data = CreateSharedDataParam( |
| data_dir, "_data", true, false, true, privileged_quota_uids); |
| std::string shared_data_media = CreateSharedDataParam( |
| data_dir, "_data_media", false, true, true, privileged_quota_uids); |
| |
| const base::FilePath stub_dir(kStubVolumeSharedDir); |
| std::string shared_stub = CreateSharedDataParam(stub_dir, "stub", false, true, |
| false, privileged_quota_uids); |
| |
| // TOOD(kansho): |non_rt_cpus_num|, |rt_cpus_num| and |affinity| |
| // should be passed from chrome instead of |enable_rt_vcpu|. |
| |
| // By default we don't request any RT CPUs |
| ArcVmCPUTopology topology(request.cpus(), 0); |
| |
| // We create only 1 RT VCPU for the time being |
| if (request.enable_rt_vcpu()) |
| topology.SetNumRTCPUs(1); |
| |
| topology.CreateCPUAffinity(); |
| |
| if (request.enable_rt_vcpu()) { |
| params.emplace_back("isolcpus=" + topology.RTCPUMask()); |
| params.emplace_back("androidboot.rtcpus=" + topology.RTCPUMask()); |
| params.emplace_back("androidboot.non_rtcpus=" + topology.NonRTCPUMask()); |
| } |
| |
| params.emplace_back("ramoops.record_size=" + |
| std::to_string(kArcVmRamoopsRecordSize)); |
| params.emplace_back("ramoops.console_size=" + |
| std::to_string(kArcVmRamoopsConsoleSize)); |
| params.emplace_back("ramoops.ftrace_size=" + |
| std::to_string(kArcVmRamoopsFtraceSize)); |
| params.emplace_back("ramoops.pmsg_size=" + |
| std::to_string(kArcVmRamoopsPmsgSize)); |
| params.emplace_back("ramoops.dump_oops=1"); |
| |
| VmBuilder vm_builder; |
| vm_builder.AppendDisks(std::move(disks)) |
| .SetCpus(topology.NumCPUs()) |
| .AppendKernelParam(base::JoinString(params, " ")) |
| .AppendCustomParam("--vcpu-cgroup-path", |
| base::FilePath(kArcvmVcpuCpuCgroup).value()) |
| .AppendCustomParam("--android-fstab", kFstabPath) |
| .AppendCustomParam( |
| "--pstore", base::StringPrintf("path=%s,size=%" PRId64, |
| kArcVmPstorePath, kArcVmRamoopsSize)) |
| .AppendSharedDir(shared_data) |
| .AppendSharedDir(shared_data_media) |
| .AppendSharedDir(shared_stub) |
| .EnableSmt(false /* enable */) |
| .EnablePerVmCoreScheduling(request.use_per_vm_core_scheduling()) |
| .SetWaylandSocket(request.vm().wayland_server()); |
| |
| if (request.enable_rt_vcpu()) { |
| vm_builder.AppendCustomParam("--rt-cpus", topology.RTCPUMask()); |
| } |
| |
| if (!topology.IsSymmetricCPU() && !topology.AffinityMask().empty()) { |
| vm_builder.AppendCustomParam("--cpu-affinity", topology.AffinityMask()); |
| } |
| |
| if (!topology.IsSymmetricCPU() && !topology.CapacityMask().empty()) { |
| vm_builder.AppendCustomParam("--cpu-capacity", topology.CapacityMask()); |
| } |
| |
| if (!topology.IsSymmetricCPU() && !topology.PackageMask().empty()) { |
| for (auto& package : topology.PackageMask()) { |
| vm_builder.AppendCustomParam("--cpu-cluster", package); |
| } |
| } |
| |
| if (request.use_hugepages()) { |
| vm_builder.AppendCustomParam("--hugepages", ""); |
| } |
| |
| vm_builder.SetMemory(std::to_string(GetArcVmMemoryMiB(request))); |
| |
| /* Enable THP if the VM has at least 7G of memory */ |
| if (base::SysInfo::AmountOfPhysicalMemoryMB() >= 7 * 1024) { |
| vm_builder.AppendCustomParam("--hugepages", ""); |
| } |
| |
| if (USE_CROSVM_SIBLINGS) { |
| vm_builder.SetVmMemoryId(vm_memory_id); |
| } |
| |
| auto vm = ArcVm::Create(base::FilePath(kKernelPath), vsock_cid, |
| std::move(network_client), std::move(server_proxy), |
| std::move(runtime_dir), std::move(data_disk_path), |
| vm_memory_id, features, std::move(vm_builder)); |
| if (!vm) { |
| LOG(ERROR) << "Unable to start VM"; |
| |
| response.set_failure_reason("Unable to start VM"); |
| return response; |
| } |
| |
| // ARCVM is ready. |
| LOG(INFO) << "Started VM with pid " << vm->pid(); |
| |
| response.set_success(true); |
| response.set_status(VM_STATUS_RUNNING); |
| vm_info->set_ipv4_address(vm->IPv4Address()); |
| vm_info->set_pid(vm->pid()); |
| |
| SendVmStartedSignal(vm_id, *vm_info, response.status()); |
| |
| vms_[vm_id] = std::move(vm); |
| |
| return response; |
| } |
| |
| int64_t Service::GetArcVmMemoryMiB(const StartArcVmRequest& request) { |
| int64_t memory_mib = request.memory_mib(); |
| if (memory_mib <= 0) { |
| memory_mib = ::vm_tools::concierge::GetVmMemoryMiB(); |
| } |
| return memory_mib; |
| } |
| |
| } // namespace concierge |
| } // namespace vm_tools |