Add dtcompare tool and device tree validation test changes
Created dtcompare tool to compare 2 device trees. The tool traverses the
trees depth-first, and allows ignoring values of properties, and
skipping properties completely.
Avoid using diff.
Using FDT allows more flexibility in comparing device trees, such as
skipping over fields or validating their presence.
Some fields in the device tree are expected to exist and contain
separate data, and it should be validated that such fields exist. Other
fields can be ignored completely.
Bug: 360388014
Test: atest avf_backcompat_tests
Change-Id: Ib4d5f89bbc25e90bd47dd422f7966ab3f2910433
diff --git a/tests/backcompat_test/src/main.rs b/tests/backcompat_test/src/main.rs
new file mode 100644
index 0000000..4113881
--- /dev/null
+++ b/tests/backcompat_test/src/main.rs
@@ -0,0 +1,204 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Integration test for VMs on device.
+
+use android_system_virtualizationservice::{
+ aidl::android::system::virtualizationservice::{
+ CpuTopology::CpuTopology, DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
+ VirtualMachineRawConfig::VirtualMachineRawConfig,
+ },
+ binder::{ParcelFileDescriptor, ProcessState},
+};
+use anyhow::anyhow;
+use anyhow::Context;
+use anyhow::Error;
+use log::error;
+use log::info;
+use std::fs::read_to_string;
+use std::fs::File;
+use std::io::Write;
+use std::process::Command;
+use vmclient::VmInstance;
+
+const VMBASE_EXAMPLE_KERNEL_PATH: &str = "vmbase_example_kernel.bin";
+const TEST_DISK_IMAGE_PATH: &str = "test_disk.img";
+const EMPTY_DISK_IMAGE_PATH: &str = "empty_disk.img";
+const GOLDEN_DEVICE_TREE: &str = "./goldens/dt_dump_golden.dts";
+const GOLDEN_DEVICE_TREE_PROTECTED: &str = "./goldens/dt_dump_protected_golden.dts";
+
+/// Runs an unprotected VM and validates it against a golden device tree.
+#[test]
+fn test_device_tree_compat() -> Result<(), Error> {
+ run_test(false, GOLDEN_DEVICE_TREE)
+}
+
+/// Runs a protected VM and validates it against a golden device tree.
+#[test]
+fn test_device_tree_protected_compat() -> Result<(), Error> {
+ run_test(true, GOLDEN_DEVICE_TREE_PROTECTED)
+}
+
+fn run_test(protected: bool, golden_dt: &str) -> Result<(), Error> {
+ let kernel = Some(open_payload(VMBASE_EXAMPLE_KERNEL_PATH)?);
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("backcompat")
+ .with_max_level(log::LevelFilter::Debug),
+ );
+
+ // We need to start the thread pool for Binder to work properly, especially link_to_death.
+ ProcessState::start_thread_pool();
+
+ let virtmgr =
+ vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+ let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
+
+ // Make file for test disk image.
+ let mut test_image = File::options()
+ .create(true)
+ .read(true)
+ .write(true)
+ .truncate(true)
+ .open(TEST_DISK_IMAGE_PATH)
+ .with_context(|| format!("Failed to open test disk image {}", TEST_DISK_IMAGE_PATH))?;
+ // Write 4 sectors worth of 4-byte numbers counting up.
+ for i in 0u32..512 {
+ test_image.write_all(&i.to_le_bytes())?;
+ }
+ let test_image = ParcelFileDescriptor::new(test_image);
+ let disk_image = DiskImage { image: Some(test_image), writable: false, partitions: vec![] };
+
+ // Make file for empty test disk image.
+ let empty_image = File::options()
+ .create(true)
+ .read(true)
+ .write(true)
+ .truncate(true)
+ .open(EMPTY_DISK_IMAGE_PATH)
+ .with_context(|| format!("Failed to open empty disk image {}", EMPTY_DISK_IMAGE_PATH))?;
+ let empty_image = ParcelFileDescriptor::new(empty_image);
+ let empty_disk_image =
+ DiskImage { image: Some(empty_image), writable: false, partitions: vec![] };
+
+ let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
+ name: String::from("VmBaseTest"),
+ kernel,
+ disks: vec![disk_image, empty_disk_image],
+ protectedVm: protected,
+ memoryMib: 300,
+ cpuTopology: CpuTopology::ONE_CPU,
+ platformVersion: "~1.0".to_string(),
+ ..Default::default()
+ });
+
+ let dump_dt = File::options()
+ .create(true)
+ .read(true)
+ .write(true)
+ .truncate(true)
+ .open("dump_dt.dtb")
+ .with_context(|| "Failed to open device tree dump file dump_dt.dtb")?;
+ let vm = VmInstance::create(
+ service.as_ref(),
+ &config,
+ None,
+ /* consoleIn */ None,
+ None,
+ Some(dump_dt),
+ None,
+ )
+ .context("Failed to create VM")?;
+ vm.start().context("Failed to start VM")?;
+ info!("Started example VM.");
+
+ // Wait for VM to finish
+ let _ = vm.wait_for_death();
+
+ if !Command::new("./dtc_static")
+ .arg("-I")
+ .arg("dts")
+ .arg("-O")
+ .arg("dtb")
+ .arg("-qqq")
+ .arg("-f")
+ .arg("-s")
+ .arg("-o")
+ .arg("dump_dt_golden.dtb")
+ .arg(golden_dt)
+ .output()?
+ .status
+ .success()
+ {
+ return Err(anyhow!("failed to execute dtc"));
+ }
+ let dtcompare_res = Command::new("./dtcompare")
+ .arg("--dt1")
+ .arg("dump_dt_golden.dtb")
+ .arg("--dt2")
+ .arg("dump_dt.dtb")
+ .arg("--ignore-path-value")
+ .arg("/chosen/kaslr-seed")
+ .arg("--ignore-path-value")
+ .arg("/chosen/rng-seed")
+ .arg("--ignore-path-value")
+ .arg("/avf/untrusted/instance-id")
+ .arg("--ignore-path-value")
+ .arg("/chosen/linuxinitrd-start")
+ .arg("--ignore-path-value")
+ .arg("/chosen/linuxinitrd-end")
+ .arg("--ignore-path-value")
+ .arg("/avf/secretkeeper_public_key")
+ .arg("--ignore-path")
+ .arg("/avf/name")
+ .output()
+ .context("failed to execute dtcompare")?;
+ if !dtcompare_res.status.success() {
+ if !Command::new("./dtc_static")
+ .arg("-I")
+ .arg("dtb")
+ .arg("-O")
+ .arg("dts")
+ .arg("-qqq")
+ .arg("-f")
+ .arg("-s")
+ .arg("-o")
+ .arg("dump_dt_failed.dts")
+ .arg("dump_dt.dtb")
+ .output()?
+ .status
+ .success()
+ {
+ return Err(anyhow!("failed to execute dtc"));
+ }
+ let dt2 = read_to_string("dump_dt_failed.dts")?;
+ error!(
+ "Device tree 2 does not match golden DT.\n
+ Device Tree 2: {}",
+ dt2
+ );
+ return Err(anyhow!(
+ "stdout: {:?}\n stderr: {:?}",
+ dtcompare_res.stdout,
+ dtcompare_res.stderr
+ ));
+ }
+
+ Ok(())
+}
+
+fn open_payload(path: &str) -> Result<ParcelFileDescriptor, Error> {
+ let file = File::open(path).with_context(|| format!("Failed to open VM image {path}"))?;
+ Ok(ParcelFileDescriptor::new(file))
+}