Specify virtual platform version and enforce it

VM config can now specify the requirement on the virtual platform
version. At runtime, the requirement is matched against the actual
virtual platform version that crosvm implements. If they don't match,
the VM can't be created. The version format follows SemVer, allowing us
to express backwards compatible and incompatible changes in the future.

Bug: 193504487
Test: atest VirtualizationTestCases
Change-Id: I23d370081e10399502178b9cfe8a46b05addf186
diff --git a/libs/vmconfig/Android.bp b/libs/vmconfig/Android.bp
index 321eba0..1aee1ce 100644
--- a/libs/vmconfig/Android.bp
+++ b/libs/vmconfig/Android.bp
@@ -10,8 +10,9 @@
     rustlibs: [
         "android.system.virtualizationservice-rust",
         "libanyhow",
-        "libserde_json",
+        "libsemver",
         "libserde",
+        "libserde_json",
     ],
     apex_available: [
         "com.android.virt",
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index 9c3e671..607b347 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -22,6 +22,7 @@
 };
 
 use anyhow::{bail, Context, Error, Result};
+use semver::VersionReq;
 use serde::{Deserialize, Serialize};
 use std::convert::TryInto;
 use std::fs::{File, OpenOptions};
@@ -51,6 +52,9 @@
     /// The amount of RAM to give the VM, in MiB.
     #[serde(default)]
     pub memory_mib: Option<NonZeroU32>,
+    /// Version or range of versions of the virtual platform that this config is compatible with.
+    /// The format follows SemVer (https://semver.org).
+    pub platform_version: VersionReq,
 }
 
 impl VmConfig {
@@ -95,6 +99,7 @@
             disks: self.disks.iter().map(DiskImage::to_parcelable).collect::<Result<_, Error>>()?,
             protectedVm: self.protected,
             memoryMib: memory_mib,
+            platformVersion: self.platform_version.to_string(),
             ..Default::default()
         })
     }
diff --git a/microdroid/microdroid.json b/microdroid/microdroid.json
index cb27a24..aff0b7b 100644
--- a/microdroid/microdroid.json
+++ b/microdroid/microdroid.json
@@ -37,5 +37,6 @@
       "writable": true
     }
   ],
-  "memory_mib": 256
+  "memory_mib": 256,
+  "platform_version": "~1.0"
 }
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 1218b68..58935d8 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -122,6 +122,7 @@
         int memory_mib;
         @SerializedName("protected")
         boolean isProtected;
+        String platform_version;
     }
 
     static class Disk {
diff --git a/tests/vsock_test.cc b/tests/vsock_test.cc
index c478f64..0fc451d 100644
--- a/tests/vsock_test.cc
+++ b/tests/vsock_test.cc
@@ -48,6 +48,7 @@
 static constexpr const char kVmInitrdPath[] = "/data/local/tmp/virt-test/initramfs";
 static constexpr const char kVmParams[] = "rdinit=/bin/init bin/vsock_client 2 45678 HelloWorld";
 static constexpr const char kTestMessage[] = "HelloWorld";
+static constexpr const char kPlatformVersion[] = "~1.0";
 
 /** Returns true if the kernel supports unprotected VMs. */
 bool isUnprotectedVmSupported() {
@@ -82,6 +83,7 @@
     raw_config.initrd = ParcelFileDescriptor(unique_fd(open(kVmInitrdPath, O_RDONLY | O_CLOEXEC)));
     raw_config.params = kVmParams;
     raw_config.protectedVm = false;
+    raw_config.platformVersion = kPlatformVersion;
 
     VirtualMachineConfig config(std::move(raw_config));
     sp<IVirtualMachine> vm;
@@ -112,4 +114,17 @@
     ASSERT_EQ(msg, kTestMessage);
 }
 
+TEST_F(VirtualizationTest, RejectIncompatiblePlatformVersion) {
+    VirtualMachineRawConfig raw_config;
+    raw_config.kernel = ParcelFileDescriptor(unique_fd(open(kVmKernelPath, O_RDONLY | O_CLOEXEC)));
+    raw_config.initrd = ParcelFileDescriptor(unique_fd(open(kVmInitrdPath, O_RDONLY | O_CLOEXEC)));
+    raw_config.params = kVmParams;
+    raw_config.platformVersion = "~2.0"; // The current platform version is 1.0.0.
+
+    VirtualMachineConfig config(std::move(raw_config));
+    sp<IVirtualMachine> vm;
+    auto status = mVirtualizationService->createVm(config, std::nullopt, std::nullopt, &vm);
+    ASSERT_FALSE(status.isOk());
+}
+
 } // namespace virt
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index b82064b..981a041 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -37,6 +37,7 @@
         "libonce_cell",
         "libregex",
         "librustutils",
+        "libsemver",
         "libselinux_bindgen",
         "libserde",
         "libserde_json",
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index bb4eef0..dfd3bff 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -57,4 +57,10 @@
      * Default is no mask which means a vCPU can run on any host CPU.
      */
     @nullable String cpuAffinity;
+
+    /**
+     * A version or range of versions of the virtual platform that this config is compatible with.
+     * The format follows SemVer.
+     */
+    @utf8InCpp String platformVersion;
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 89c6e8a..d9825dc 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -52,6 +52,7 @@
 use log::{debug, error, info, warn, trace};
 use microdroid_payload_config::VmPayloadConfig;
 use rustutils::system_properties;
+use semver::VersionReq;
 use statslog_virtualization_rust::vm_creation_requested::{stats_write, Hypervisor};
 use std::convert::TryInto;
 use std::ffi::CStr;
@@ -239,6 +240,7 @@
             console_fd,
             log_fd,
             indirect_files,
+            platform_version: parse_platform_version_req(&config.platformVersion)?,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -893,6 +895,16 @@
     ParcelFileDescriptor::new(f)
 }
 
+/// Parses the platform version requirement string.
+fn parse_platform_version_req(s: &str) -> Result<VersionReq, Status> {
+    VersionReq::parse(s).map_err(|e| {
+        new_binder_exception(
+            ExceptionCode::BAD_PARCELABLE,
+            format!("Invalid platform version requirement {}: {}", s, e),
+        )
+    })
+}
+
 /// Simple utility for referencing Borrowed or Owned. Similar to std::borrow::Cow, but
 /// it doesn't require that T implements Clone.
 enum BorrowedOrOwned<'a, T> {
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index df7a7d2..94cb78f 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -19,6 +19,7 @@
 use anyhow::{bail, Error};
 use command_fds::CommandFdExt;
 use log::{debug, error, info};
+use semver::{Version, VersionReq};
 use shared_child::SharedChild;
 use std::fs::{remove_dir_all, File};
 use std::io;
@@ -36,6 +37,12 @@
 
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
 
+/// Version of the platform that crosvm currently implements. The format follows SemVer. This
+/// should be updated when there is a platform change in the crosvm side. Having this value here is
+/// fine because virtualizationservice and crosvm are supposed to be updated together in the virt
+/// APEX.
+const CROSVM_PLATFORM_VERSION: &str = "1.0.0";
+
 /// The exit status which crosvm returns when it has an error starting a VM.
 const CROSVM_ERROR_STATUS: i32 = 1;
 /// The exit status which crosvm returns when a VM requests a reboot.
@@ -59,6 +66,7 @@
     pub console_fd: Option<File>,
     pub log_fd: Option<File>,
     pub indirect_files: Vec<File>,
+    pub platform_version: VersionReq,
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -365,6 +373,16 @@
     if config.bootloader.is_some() && (config.kernel.is_some() || config.initrd.is_some()) {
         bail!("Can't have both bootloader and kernel/initrd image.");
     }
+    let version = Version::parse(CROSVM_PLATFORM_VERSION).unwrap();
+    if !config.platform_version.matches(&version) {
+        bail!(
+            "Incompatible platform version. The config is compatible with platform version(s) \
+              {}, but the actual platform version is {}",
+            config.platform_version,
+            version
+        );
+    }
+
     Ok(())
 }