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/Android.bp b/tests/backcompat_test/Android.bp
new file mode 100644
index 0000000..aa1e089
--- /dev/null
+++ b/tests/backcompat_test/Android.bp
@@ -0,0 +1,36 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_test {
+    name: "avf_backcompat_tests",
+    crate_name: "backcompat_test",
+    srcs: ["src/main.rs"],
+    prefer_rlib: true,
+    edition: "2021",
+    rustlibs: [
+        "android.system.virtualizationservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "liblibc",
+        "libnix",
+        "libvmclient",
+        "liblog_rust",
+    ],
+    test_config: "AndroidTest.xml",
+    data: [
+        "goldens/dt_dump_*",
+        ":vmbase_example_kernel_bin",
+    ],
+    data_bins: [
+        "dtc_static",
+        "dtcompare",
+    ],
+    test_suites: ["general-tests"],
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
diff --git a/tests/backcompat_test/AndroidTest.xml b/tests/backcompat_test/AndroidTest.xml
new file mode 100644
index 0000000..dd8b43d
--- /dev/null
+++ b/tests/backcompat_test/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+<configuration description="Config to run backcompat_tests.">
+  <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push-file" key="avf_backcompat_tests" value="/data/local/tmp/avf_backcompat_tests" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+        <option name="module-name" value="avf_backcompat_tests" />
+        <!-- Run tests serially because the VMs may overwrite the generated Device Tree. -->
+        <option name="native-test-flag" value="--test-threads=1" />
+    </test>
+</configuration>
diff --git a/tests/backcompat_test/goldens/dt_dump_golden.dts b/tests/backcompat_test/goldens/dt_dump_golden.dts
new file mode 100644
index 0000000..a583514
--- /dev/null
+++ b/tests/backcompat_test/goldens/dt_dump_golden.dts
@@ -0,0 +1,143 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <0x02>;
+	#size-cells = <0x02>;
+	compatible = "linux,dummy-virt";
+	interrupt-parent = <0x01>;
+	name = "reference";
+
+	U6_16550A@2e8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x02 0x01>;
+		reg = <0x00 0x2e8 0x00 0x08>;
+	};
+
+	U6_16550A@2f8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x02 0x01>;
+		reg = <0x00 0x2f8 0x00 0x08>;
+	};
+
+	U6_16550A@3e8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x00 0x01>;
+		reg = <0x00 0x3e8 0x00 0x08>;
+	};
+
+	U6_16550A@3f8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x00 0x01>;
+		reg = <0x00 0x3f8 0x00 0x08>;
+	};
+
+	__symbols__ {
+		intc = "/intc";
+	};
+
+	avf {
+		secretkeeper_public_key = [a4 01 01 03 27 20 06 21 58 20 de c2 79 41 b5 2a d8 1e eb dd 8a c5 a0 2f e4 56 12 42 5e b5 a4 c6 6a 8c 32 81 65 75 1c 6e b2 87];
+
+		untrusted {
+			defer-rollback-protection;
+			instance-id = <0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00>;
+		};
+	};
+
+	chosen {
+		bootargs = "panic=-1";
+		kaslr-seed = <0xab3b03c7 0xbb04cfd9>;
+		linux,pci-probe-only = <0x01>;
+		rng-seed = <0xa738baa8 0xf125e39b 0x5016f377 0xe2439805 0x94624c7e 0xac404bf6 0x68ece261 0xd45cca77 0x72328c0d 0xfdb9674f 0x74c1eb50 0x5665af83 0x1e8ccb52 0x120ed001 0xdc057599 0xbb3d33ea 0x6f9eb8e7 0x44f0517e 0x65d1cd16 0xeb4506a7 0x63fe5a00 0x8e330a52 0x2ab37c64 0x9aec3871 0x80f24353 0xfcdea704 0xd0e4fa1b 0x86412d49 0xed12a31d 0x1fbe26f3 0x97e442c5 0x25b31828 0xbe8626eb 0xea8098b8 0x6bf93ad9 0x3676d94a 0xcdbf695a 0x8b68008c 0xf598963b 0x483d0817 0xcea64b84 0xbbe0d7af 0xb09d31d7 0xfa461596 0xc47f9be8 0xd992c480 0x98372ef6 0xe1e70464 0xdc2752e4 0xe40a042c 0x5bb3a936 0x8af0aaff 0xd52f6723 0x8ac81a1b 0x15ed83d 0xee00b9eb 0x107f8ce 0xda99d512 0xed26543c 0x959f76f 0x1b85d5dc 0xa0b36c99 0xcdc8351 0xa4196327>;
+		stdout-path = "/U6_16550A@3f8";
+	};
+
+	config {
+		kernel-address = <0x80000000>;
+		kernel-size = <0x2c880>;
+	};
+
+	cpufreq {
+		compatible = "virtual,kvm-cpufreq";
+	};
+
+	cpus {
+		#address-cells = <0x01>;
+		#size-cells = <0x00>;
+
+		cpu@0 {
+			compatible = "arm,armv8";
+			device_type = "cpu";
+			phandle = <0x100>;
+			reg = <0x00>;
+		};
+	};
+
+	intc {
+		#address-cells = <0x02>;
+		#interrupt-cells = <0x03>;
+		#size-cells = <0x02>;
+		compatible = "arm,gic-v3";
+		interrupt-controller;
+		phandle = <0x01>;
+		reg = <0x00 0x3fff0000 0x00 0x10000 0x00 0x3ffd0000 0x00 0x20000>;
+	};
+
+	memory {
+		device_type = "memory";
+		reg = <0x00 0x80000000 0x00 0x12c00000>;
+	};
+
+	pci {
+		#address-cells = <0x03>;
+		#interrupt-cells = <0x01>;
+		#size-cells = <0x02>;
+		bus-range = <0x00 0x00>;
+		compatible = "pci-host-cam-generic";
+		device_type = "pci";
+		dma-coherent;
+		interrupt-map = <0x800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x04 0x04 0x1000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x05 0x04 0x1800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x06 0x04 0x2000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x07 0x04 0x2800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x08 0x04 0x3000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x09 0x04 0x3800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0a 0x04 0x4000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0b 0x04>;
+		interrupt-map-mask = <0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07>;
+		ranges = <0x3000000 0x00 0x70000000 0x00 0x70000000 0x00 0x2000000 0x43000000 0x00 0x93400000 0x00 0x93400000 0xff 0x6cc00000>;
+		reg = <0x00 0x72000000 0x00 0x1000000>;
+	};
+
+	pclk@3M {
+		#clock-cells = <0x00>;
+		clock-frequency = <0x2fefd8>;
+		compatible = "fixed-clock";
+		phandle = <0x18>;
+	};
+
+	psci {
+		compatible = "arm,psci-1.0\0arm,psci-0.2";
+		method = "hvc";
+	};
+
+	rtc@2000 {
+		arm,primecell-periphid = <0x41030>;
+		clock-names = "apb_pclk";
+		clocks = <0x18>;
+		compatible = "arm,primecell";
+		interrupts = <0x00 0x01 0x04>;
+		reg = <0x00 0x2000 0x00 0x1000>;
+	};
+
+	timer {
+		always-on;
+		compatible = "arm,armv8-timer";
+		interrupts = <0x01 0x0d 0x108 0x01 0x0e 0x108 0x01 0x0b 0x108 0x01 0x0a 0x108>;
+	};
+
+	vmwdt@3000 {
+		clock-frequency = <0x02>;
+		compatible = "qemu,vcpu-stall-detector";
+		interrupts = <0x01 0x0f 0x101>;
+		reg = <0x00 0x3000 0x00 0x1000>;
+		timeout-sec = <0x0a>;
+	};
+};
diff --git a/tests/backcompat_test/goldens/dt_dump_protected_golden.dts b/tests/backcompat_test/goldens/dt_dump_protected_golden.dts
new file mode 100644
index 0000000..656958d
--- /dev/null
+++ b/tests/backcompat_test/goldens/dt_dump_protected_golden.dts
@@ -0,0 +1,157 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <0x02>;
+	#size-cells = <0x02>;
+	compatible = "linux,dummy-virt";
+	interrupt-parent = <0x01>;
+	name = "reference";
+
+	U6_16550A@2e8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x02 0x01>;
+		reg = <0x00 0x2e8 0x00 0x08>;
+	};
+
+	U6_16550A@2f8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x02 0x01>;
+		reg = <0x00 0x2f8 0x00 0x08>;
+	};
+
+	U6_16550A@3e8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x00 0x01>;
+		reg = <0x00 0x3e8 0x00 0x08>;
+	};
+
+	U6_16550A@3f8 {
+		clock-frequency = <0x1c2000>;
+		compatible = "ns16550a";
+		interrupts = <0x00 0x00 0x01>;
+		reg = <0x00 0x3f8 0x00 0x08>;
+	};
+
+	__symbols__ {
+		intc = "/intc";
+	};
+
+	avf {
+		secretkeeper_public_key = [a4 01 01 03 27 20 06 21 58 20 de c2 79 41 b5 2a d8 1e eb dd 8a c5 a0 2f e4 56 12 42 5e b5 a4 c6 6a 8c 32 81 65 75 1c 6e b2 87];
+
+		untrusted {
+			defer-rollback-protection;
+			instance-id = <0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00>;
+		};
+	};
+
+	chosen {
+		bootargs = "panic=-1";
+		kaslr-seed = <0xbbf0472d 0xbef495c>;
+		linux,pci-probe-only = <0x01>;
+		rng-seed = <0xb6e3fa0c 0xa0546147 0xeca61840 0x4f07da9d 0xacb41a21 0x8aa7ff1f 0xd32dd43 0x93fb4ad3 0xab5f9bf1 0x66d5913d 0x2b389a9f 0xc2c268d2 0xfd1d9a22 0xa8dba850 0xd443014d 0x10b3dfcb 0x77597882 0x66008b71 0x3d29575c 0xd917ee2f 0xb6e98504 0x6a5c9fde 0xa02daf16 0x3a60b1d5 0xa4416447 0x9e8a996d 0x3b4bf5e9 0xdf7639cb 0x4b608f7e 0x3434d9b4 0xb84cd15 0x86d724ae 0x404a1353 0x8afc6a43 0x916c4b8d 0xebe878c0 0xd67a99a4 0x94fb22ca 0xef53a3bf 0xaf5fc4b 0xd6d405d8 0xb6ed6cb5 0xc4d13a21 0x6aff3f79 0x93b56581 0x622e8da3 0x59047c4b 0x9a7562ee 0x93762d9a 0xeab995f7 0x33e1cdea 0x5d071401 0x2d57f0d1 0x73367772 0x532a74b6 0x3fb875fe 0x7340d4dd 0x492fa79f 0x7749f27 0xe8eefd10 0xeb00c401 0xd51bd6b3 0x904b5ac8 0x4316f75b>;
+		stdout-path = "/U6_16550A@3f8";
+	};
+
+	config {
+		kernel-address = <0x80000000>;
+		kernel-size = <0x2c880>;
+	};
+
+	cpufreq {
+		compatible = "virtual,kvm-cpufreq";
+	};
+
+	cpus {
+		#address-cells = <0x01>;
+		#size-cells = <0x00>;
+
+		cpu@0 {
+			compatible = "arm,armv8";
+			device_type = "cpu";
+			phandle = <0x100>;
+			reg = <0x00>;
+		};
+	};
+
+	intc {
+		#address-cells = <0x02>;
+		#interrupt-cells = <0x03>;
+		#size-cells = <0x02>;
+		compatible = "arm,gic-v3";
+		interrupt-controller;
+		phandle = <0x01>;
+		reg = <0x00 0x3fff0000 0x00 0x10000 0x00 0x3ffd0000 0x00 0x20000>;
+	};
+
+	memory {
+		device_type = "memory";
+		reg = <0x00 0x80000000 0x00 0x13800000>;
+	};
+
+	pci {
+		#address-cells = <0x03>;
+		#interrupt-cells = <0x01>;
+		#size-cells = <0x02>;
+		bus-range = <0x00 0x00>;
+		compatible = "pci-host-cam-generic";
+		device_type = "pci";
+		dma-coherent;
+		interrupt-map = <0x800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x04 0x04 0x1000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x05 0x04 0x1800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x06 0x04 0x2000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x07 0x04 0x2800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x08 0x04 0x3000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x09 0x04 0x3800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0a 0x04>;
+		interrupt-map-mask = <0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07>;
+		memory-region = <0x02>;
+		ranges = <0x3000000 0x00 0x70000000 0x00 0x70000000 0x00 0x2000000 0x43000000 0x00 0x94000000 0x00 0x94000000 0xff 0x6c000000>;
+		reg = <0x00 0x72000000 0x00 0x1000000>;
+	};
+
+	pclk@3M {
+		#clock-cells = <0x00>;
+		clock-frequency = <0x2fefd8>;
+		compatible = "fixed-clock";
+		phandle = <0x18>;
+	};
+
+	psci {
+		compatible = "arm,psci-1.0\0arm,psci-0.2";
+		method = "hvc";
+	};
+
+	reserved-memory {
+		#address-cells = <0x02>;
+		#size-cells = <0x02>;
+		ranges;
+
+		restricted_dma_reserved {
+			alignment = <0x00 0x1000>;
+			compatible = "restricted-dma-pool";
+			phandle = <0x02>;
+			size = <0x00 0xc00000>;
+		};
+	};
+
+	rtc@2000 {
+		arm,primecell-periphid = <0x41030>;
+		clock-names = "apb_pclk";
+		clocks = <0x18>;
+		compatible = "arm,primecell";
+		interrupts = <0x00 0x01 0x04>;
+		reg = <0x00 0x2000 0x00 0x1000>;
+	};
+
+	timer {
+		always-on;
+		compatible = "arm,armv8-timer";
+		interrupts = <0x01 0x0d 0x108 0x01 0x0e 0x108 0x01 0x0b 0x108 0x01 0x0a 0x108>;
+	};
+
+	vmwdt@3000 {
+		clock-frequency = <0x02>;
+		compatible = "qemu,vcpu-stall-detector";
+		interrupts = <0x01 0x0f 0x101>;
+		reg = <0x00 0x3000 0x00 0x1000>;
+		timeout-sec = <0x0a>;
+	};
+};
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))
+}
diff --git a/tests/dtcompare/Android.bp b/tests/dtcompare/Android.bp
new file mode 100644
index 0000000..988f420
--- /dev/null
+++ b/tests/dtcompare/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+    name: "dtcompare",
+    crate_root: "src/main.rs",
+    srcs: ["src/main.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libanyhow",
+        "libclap",
+        "libhex_nostd",
+        "liblibfdt_nostd",
+        "liblog_rust",
+    ],
+    visibility: ["//packages/modules/Virtualization:__subpackages__"],
+}
diff --git a/tests/dtcompare/src/main.rs b/tests/dtcompare/src/main.rs
new file mode 100644
index 0000000..db3aac2
--- /dev/null
+++ b/tests/dtcompare/src/main.rs
@@ -0,0 +1,192 @@
+// 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.
+
+//! Compare device tree contents.
+//! Allows skipping over fields provided.
+
+use anyhow::anyhow;
+use anyhow::Context;
+use anyhow::Result;
+use clap::Parser;
+use hex::encode;
+use libfdt::Fdt;
+use libfdt::FdtNode;
+
+use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+use std::fs::read;
+use std::path::PathBuf;
+
+#[derive(Debug, Parser)]
+/// Device Tree Compare arguments.
+struct DtCompareArgs {
+    /// first device tree
+    #[arg(long)]
+    dt1: PathBuf,
+    /// second device tree
+    #[arg(long)]
+    dt2: PathBuf,
+    /// list of properties that should exist but are expected to hold different values in the
+    /// trees.
+    #[arg(short = 'I', long)]
+    ignore_path_value: Vec<String>,
+    /// list of paths that will be ignored, whether added, removed, or changed.
+    /// Paths can be nodes, subnodes, or even properties:
+    /// Ex: /avf/unstrusted // this is a path to a subnode. All properties and subnodes underneath
+    ///                     // it will also be ignored.
+    ///     /avf/name       // This is a path for a property. Only this property will be ignored.
+    #[arg(short = 'S', long)]
+    ignore_path: Vec<String>,
+}
+
+fn main() -> Result<()> {
+    let args = DtCompareArgs::parse();
+    let dt1: Vec<u8> = read(args.dt1)?;
+    let dt2: Vec<u8> = read(args.dt2)?;
+    let ignore_value_set = BTreeSet::from_iter(args.ignore_path_value);
+    let ignore_set = BTreeSet::from_iter(args.ignore_path);
+    compare_device_trees(dt1.as_slice(), dt2.as_slice(), ignore_value_set, ignore_set)
+}
+
+// Compare device trees by doing a pre-order traversal of the trees.
+fn compare_device_trees(
+    dt1: &[u8],
+    dt2: &[u8],
+    ignore_value_set: BTreeSet<String>,
+    ignore_set: BTreeSet<String>,
+) -> Result<()> {
+    let fdt1 = Fdt::from_slice(dt1).context("invalid device tree: Dt1")?;
+    let fdt2 = Fdt::from_slice(dt2).context("invalid device tree: Dt2")?;
+    let mut errors = Vec::new();
+    compare_subnodes(
+        &fdt1.root(),
+        &fdt2.root(),
+        &ignore_value_set,
+        &ignore_set,
+        /* path */ &mut ["".to_string()],
+        &mut errors,
+    )?;
+    if !errors.is_empty() {
+        return Err(anyhow!(
+            "Following properties had different values: [\n{}\n]\ndetected {} diffs",
+            errors.join("\n"),
+            errors.len()
+        ));
+    }
+    Ok(())
+}
+
+fn compare_props(
+    root1: &FdtNode,
+    root2: &FdtNode,
+    ignore_value_set: &BTreeSet<String>,
+    ignore_set: &BTreeSet<String>,
+    path: &mut [String],
+    errors: &mut Vec<String>,
+) -> Result<()> {
+    let mut prop_map: BTreeMap<String, &[u8]> = BTreeMap::new();
+    for prop in root1.properties().context("Error getting properties")? {
+        let prop_path =
+            path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?;
+        // Do not add to prop map if skipping
+        if ignore_set.contains(&prop_path) {
+            continue;
+        }
+        let value = prop.value().context("Error getting value")?;
+        if prop_map.insert(prop_path.clone(), value).is_some() {
+            return Err(anyhow!("Duplicate property detected in subnode: {}", prop_path));
+        }
+    }
+    for prop in root2.properties().context("Error getting properties")? {
+        let prop_path =
+            path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?;
+        if ignore_set.contains(&prop_path) {
+            continue;
+        }
+        let Some(prop1_value) = prop_map.remove(&prop_path) else {
+            errors.push(format!("added prop_path: {}", prop_path));
+            continue;
+        };
+        let prop_compare = prop1_value == prop.value().context("Error getting value")?;
+        // Check if value should be ignored. If yes, skip field.
+        if ignore_value_set.contains(&prop_path) {
+            continue;
+        }
+        if !prop_compare {
+            errors.push(format!(
+                "prop {} value mismatch: old: {} -> new: {}",
+                prop_path,
+                encode(prop1_value),
+                encode(prop.value().context("Error getting value")?)
+            ));
+        }
+    }
+    if !prop_map.is_empty() {
+        errors.push(format!("missing properties: {:?}", prop_map));
+    }
+    Ok(())
+}
+
+fn compare_subnodes(
+    node1: &FdtNode,
+    node2: &FdtNode,
+    ignore_value_set: &BTreeSet<String>,
+    ignore_set: &BTreeSet<String>,
+    path: &mut [String],
+    errors: &mut Vec<String>,
+) -> Result<()> {
+    let mut subnodes_map: BTreeMap<String, FdtNode> = BTreeMap::new();
+    for subnode in node1.subnodes().context("Error getting subnodes of first FDT")? {
+        let sn_path = path.join("/")
+            + "/"
+            + subnode.name().context("Error getting property name")?.to_str()?;
+        // Do not add to subnode map if skipping
+        if ignore_set.contains(&sn_path) {
+            continue;
+        }
+        if subnodes_map.insert(sn_path.clone(), subnode).is_some() {
+            return Err(anyhow!("Duplicate subnodes detected: {}", sn_path));
+        }
+    }
+    for sn2 in node2.subnodes().context("Error getting subnodes of second FDT")? {
+        let sn_path =
+            path.join("/") + "/" + sn2.name().context("Error getting subnode name")?.to_str()?;
+        let sn1 = subnodes_map.remove(&sn_path);
+        match sn1 {
+            Some(sn) => {
+                compare_props(
+                    &sn,
+                    &sn2,
+                    ignore_value_set,
+                    ignore_set,
+                    &mut [sn_path.clone()],
+                    errors,
+                )?;
+                compare_subnodes(
+                    &sn,
+                    &sn2,
+                    ignore_value_set,
+                    ignore_set,
+                    &mut [sn_path.clone()],
+                    errors,
+                )?;
+            }
+            None => errors.push(format!("added node: {}", sn_path)),
+        }
+    }
+    if !subnodes_map.is_empty() {
+        errors.push(format!("missing nodes: {:?}", subnodes_map));
+    }
+    Ok(())
+}