Adding FirmwareBootHeaderVerification
Bug: 149182947
Test: atest FirmwareBootHeaderVerification
Change-Id: I53a609b3fcd0a28e0fe93b0c5b8bfa96605e2225
Merged-In: I53a609b3fcd0a28e0fe93b0c5b8bfa96605e2225
diff --git a/testcases/host/firmware_test/Android.bp b/testcases/host/firmware_test/Android.bp
new file mode 100644
index 0000000..559c68c
--- /dev/null
+++ b/testcases/host/firmware_test/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2020 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.
+
+java_test_host {
+ name: "FirmwareBootHeaderVerification",
+ libs: [
+ "compatibility-tradefed",
+ "tradefed",
+ "tradefed-common-util",
+ "vts-core-tradefed-harness",
+ ],
+ srcs: ["src/**/*.java"],
+ test_suites: [
+ "general-tests",
+ "vts-core",
+ ],
+}
diff --git a/testcases/host/firmware_test/AndroidTest.xml b/testcases/host/firmware_test/AndroidTest.xml
new file mode 100644
index 0000000..50202ff
--- /dev/null
+++ b/testcases/host/firmware_test/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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="Runs FirmwareBootHeaderVerification">
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+ <option name="min-api-level" value="28" />
+ <option name="api-level-prop" value="ro.product.first_api_level" />
+ </object>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="jar" value="FirmwareBootHeaderVerification.jar" />
+ </test>
+</configuration>
diff --git a/testcases/host/firmware_test/src/com/android/tests/firmware/BootImageInfo.java b/testcases/host/firmware_test/src/com/android/tests/firmware/BootImageInfo.java
new file mode 100644
index 0000000..cdaf8ba
--- /dev/null
+++ b/testcases/host/firmware_test/src/com/android/tests/firmware/BootImageInfo.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+package com.android.tests.firmware;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class BootImageInfo implements AutoCloseable {
+ static int KERNEL_SIZE_OFFSET = 2 * 4;
+ static int RAMDISK_SIZE_OFFSET = 4 * 4;
+ static int PAGE_SIZE_OFFSET = 9 * 4;
+ static int HOST_IMG_HEADER_VER_OFFSET = 10 * 4;
+ // Offset of recovery dtbo size in boot header of version 1.
+ static int BOOT_HEADER_DTBO_SIZE_OFFSET = 1632;
+ static int BOOT_HEADER_SIZE_OFFSET = BOOT_HEADER_DTBO_SIZE_OFFSET + 4 + 8;
+ static int DTB_SIZE_OFFSET = BOOT_HEADER_SIZE_OFFSET + 4;
+ private int mKernelSize = 0;
+ private int mRamdiskSize = 0;
+ private int mPageSize = 0;
+ private int mImgHeaderVer = 0;
+ private int mRecoveryDtboSize = 0;
+ private int mBootHeaderSize = 0;
+ private int mDtbSize = 0;
+ private RandomAccessFile mRaf = null;
+
+ /**
+ * Create a {@link BootImageInfo}.
+ */
+ public BootImageInfo(String imagePath) throws IOException {
+ File bootImg = new File(imagePath);
+ mRaf = new RandomAccessFile(bootImg, "r");
+ byte[] tmpBytes = new byte[44];
+ byte[] bytes = new byte[4];
+ mRaf.read(tmpBytes);
+
+ mRaf.seek(KERNEL_SIZE_OFFSET);
+ mRaf.read(bytes);
+ mKernelSize = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+
+ mRaf.seek(RAMDISK_SIZE_OFFSET);
+ mRaf.read(bytes);
+ mRamdiskSize = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+
+ mRaf.seek(PAGE_SIZE_OFFSET);
+ mRaf.read(bytes);
+ mPageSize = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+
+ mRaf.seek(HOST_IMG_HEADER_VER_OFFSET);
+ mRaf.read(bytes);
+ mImgHeaderVer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+
+ mRaf.seek(BOOT_HEADER_DTBO_SIZE_OFFSET);
+ mRaf.read(bytes);
+ mRecoveryDtboSize = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+
+ mRaf.seek(BOOT_HEADER_SIZE_OFFSET);
+ mRaf.read(bytes);
+ mBootHeaderSize = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+
+ if (mImgHeaderVer > 1) {
+ mRaf.seek(DTB_SIZE_OFFSET);
+ mRaf.read(bytes);
+ mDtbSize = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ }
+ }
+
+ /**
+ * Get kernel size of boot image.
+ *
+ * @return the value of kernel size.
+ */
+ public int getKernelSize() {
+ return mKernelSize;
+ }
+
+ public void setKernelSize(int size) {
+ mKernelSize = size;
+ }
+
+ /**
+ * Get ramdisk size of boot image.
+ *
+ * @return the value of ramdisk size.
+ */
+ public int getRamdiskSize() {
+ return mRamdiskSize;
+ }
+
+ public void setRamdiskSize(int size) {
+ mRamdiskSize = size;
+ }
+
+ /**
+ * Get page size of boot image.
+ *
+ * @return the value of page size.
+ */
+ public int getPageSize() {
+ return mPageSize;
+ }
+
+ public void setPageSize(int size) {
+ mPageSize = size;
+ }
+
+ /**
+ * Get image header version of boot image.
+ *
+ * @return the value of host image header version.
+ */
+ public int getImgHeaderVer() {
+ return mImgHeaderVer;
+ }
+
+ public void setImgHeaderVer(int version) {
+ mImgHeaderVer = version;
+ }
+
+ /**
+ * Get recovery dtbo size of boot image.
+ *
+ * @return the value of recovery dtbo size.
+ */
+ public int getRecoveryDtboSize() {
+ return mRecoveryDtboSize;
+ }
+
+ public void setRecoveryDtboSize(int size) {
+ mRecoveryDtboSize = size;
+ }
+
+ /**
+ * Get boot header size of boot image.
+ *
+ * @return the value of boot header size.
+ */
+ public int getBootHeaderSize() {
+ return mBootHeaderSize;
+ }
+
+ public void setBootHeaderSize(int size) {
+ mBootHeaderSize = size;
+ }
+
+ /**
+ * Get dtb size of boot image.
+ *
+ * @return the value of dtb size.
+ */
+ public int getDtbSize() {
+ return mDtbSize;
+ }
+
+ public void setDtbSize(int size) {
+ mDtbSize = size;
+ }
+
+ /**
+ * Get expect header size of boot image.
+ *
+ * @return the value of expected header size.
+ */
+ public int getExpectHeaderSize() {
+ int expectHeaderSize = BOOT_HEADER_SIZE_OFFSET + 4;
+ if (mImgHeaderVer > 1) {
+ expectHeaderSize = expectHeaderSize + 4 + 8;
+ }
+ return expectHeaderSize;
+ }
+
+ /**
+ * Get kernel page numbers of boot image.
+ *
+ * @return the value of kernel page numbers.
+ */
+ int getKernelPageNum() {
+ return (mKernelSize + mPageSize - 1) / mPageSize;
+ }
+
+ /**
+ * Get the content of ramdisk.
+ *
+ * @return the content of ramdisk.
+ */
+ public byte[] getRamdiskStream() throws IOException {
+ byte[] tmpBytes = new byte[mRamdiskSize];
+ int ramDiskOffset = mPageSize * (1 + getKernelPageNum());
+ mRaf.seek(ramDiskOffset);
+ mRaf.read(tmpBytes);
+ return tmpBytes;
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (mRaf != null) {
+ mRaf.close();
+ }
+ }
+}
diff --git a/testcases/host/firmware_test/src/com/android/tests/firmware/FirmwareBootHeaderVerification.java b/testcases/host/firmware_test/src/com/android/tests/firmware/FirmwareBootHeaderVerification.java
new file mode 100644
index 0000000..bf4fb84
--- /dev/null
+++ b/testcases/host/firmware_test/src/com/android/tests/firmware/FirmwareBootHeaderVerification.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+package com.android.tests.firmware;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.AbiFormatter;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.TargetFileUtils;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.zip.GZIPInputStream;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/* VTS test to verify boot/recovery image header. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class FirmwareBootHeaderVerification extends BaseHostJUnit4Test {
+ private static final int PLATFORM_API_LEVEL_P = 28;
+ // Path to platform block devices.
+ private static final String BLOCK_DEV_PATH = "/dev/block/platform";
+ // Indicates current slot suffix for A/B devices.
+ private static final String PROPERTY_SLOT_SUFFIX = "ro.boot.slot_suffix";
+
+ private ITestDevice mDevice;
+ private String mBlockDevPath = BLOCK_DEV_PATH;
+ private int mLaunchApiLevel;
+ private String mSlotSuffix = null;
+ private static File mTemptFolder = null;
+ private ArrayList<String> mSupportedAbis = null;
+
+ @BeforeClass
+ public static void oneTimeSetup() throws Exception {
+ mTemptFolder = FileUtil.createTempDir("firmware-boot-header-verify");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mDevice = getDevice();
+ mLaunchApiLevel = mDevice.getLaunchApiLevel();
+ mSlotSuffix = mDevice.getProperty(PROPERTY_SLOT_SUFFIX);
+ if (mSlotSuffix == null) {
+ mSlotSuffix = "";
+ }
+ CLog.i("Current slot suffix: %s", mSlotSuffix);
+ mSupportedAbis = new ArrayList<>(Arrays.asList(AbiFormatter.getSupportedAbis(mDevice, "")));
+ Assume.assumeTrue("Skipping test for x86 NON-ACPI ABI", isFullfeelPrecondition());
+ }
+
+ private boolean isFullfeelPrecondition() throws DeviceNotAvailableException {
+ if (mSupportedAbis.contains("x86")) {
+ mBlockDevPath = "/dev/block";
+ CommandResult cmdResult = mDevice.executeShellV2Command("cat /proc/cmdline |"
+ + "grep -o \"'androidboot.acpio_idx=[^ ]*'\" |"
+ + "cut -d \"=\" -f 2 ");
+ Assert.assertEquals(String.format("Checking if x86 device is NON-ACPI ABI: %s",
+ cmdResult.getStderr()),
+ cmdResult.getExitCode().intValue(), 0);
+ String acpio_idx_string = cmdResult.getStdout().replace("\n", "");
+ if (acpio_idx_string.equals("")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void checkValidRamdisk(BootImageInfo bootImgInfo) throws IOException {
+ byte[] compressedRamdisk = bootImgInfo.getRamdiskStream();
+ ByteArrayInputStream compressedStream = new ByteArrayInputStream(compressedRamdisk);
+ GZIPInputStream extractRamdisk = new GZIPInputStream(compressedStream);
+ // The CPIO header magic can be "070701" or "070702" as per kernel
+ // documentation: Documentation/early-userspace/buffer-format.txt
+ byte[] cpio_header = new byte[6];
+ extractRamdisk.read(cpio_header);
+ CLog.i("cpio_header_magic: %s", cpio_header);
+ String cpio_header_magic = new String(cpio_header);
+ CLog.i("cpio_header_magic: %s", cpio_header_magic);
+ Assert.assertTrue("cpio archive header magic not found in ramdisk",
+ (cpio_header_magic.equals("070701") || cpio_header_magic.equals("070702")));
+ }
+
+ private void CheckImageHeader(String imagePath, boolean isRecovery) throws IOException {
+ BootImageInfo bootImgInfo = new BootImageInfo(imagePath);
+ // Check kernel size.
+ Assert.assertNotEquals(
+ "boot.img/recovery.img must contain kernel", bootImgInfo.getKernelSize(), 0);
+ if (mLaunchApiLevel > PLATFORM_API_LEVEL_P) {
+ // Check image version.
+ Assert.assertTrue("Device must at least have a boot image of version 2",
+ (bootImgInfo.getImgHeaderVer() >= 2));
+ // Check ramdisk size.
+ Assert.assertNotEquals(
+ "boot.img must contain ramdisk", bootImgInfo.getRamdiskSize(), 0);
+ checkValidRamdisk(bootImgInfo);
+ } else {
+ Assert.assertTrue("Device must at least have a boot image of version 1",
+ (bootImgInfo.getImgHeaderVer() >= 1));
+ }
+ if (isRecovery) {
+ Assert.assertNotEquals(
+ "recovery partition for non-A/B devices must contain the recovery DTBO",
+ bootImgInfo.getRecoveryDtboSize(), 0);
+ }
+ if (bootImgInfo.getImgHeaderVer() > 1) {
+ Assert.assertNotEquals(
+ "Boot/recovery image must contain DTB", bootImgInfo.getDtbSize(), 0);
+ }
+ Assert.assertEquals(
+ String.format(
+ "Test failure due to boot header size mismatch. Expected %s Actual %s",
+ bootImgInfo.getExpectHeaderSize(), bootImgInfo.getBootHeaderSize()),
+ bootImgInfo.getExpectHeaderSize(), bootImgInfo.getBootHeaderSize());
+ }
+
+ @AfterClass
+ public static void postTestTearDown() {
+ mTemptFolder.delete();
+ }
+
+ /* Validates boot image header. */
+ @Test
+ public void testBootImageHeader() throws Exception {
+ String currentBootPartition = "boot" + mSlotSuffix;
+ String[] options = {"-type", "l"};
+ ArrayList<String> bootPaths = TargetFileUtils.findFile(
+ mBlockDevPath, currentBootPartition, Arrays.asList(options), mDevice);
+ CLog.d("Boot path %s", bootPaths);
+ Assert.assertFalse("Unable to find path to boot image on device.", bootPaths.isEmpty());
+ File localBootImg = new File(mTemptFolder, "boot.img");
+ Assert.assertTrue("Pull " + bootPaths.get(0) + " failed!",
+ mDevice.pullFile(bootPaths.get(0), localBootImg));
+ CLog.d("Remote boot path %s", bootPaths);
+ CLog.d("Local boot path %s", localBootImg.getAbsolutePath());
+ CheckImageHeader(localBootImg.getAbsolutePath(), false);
+ }
+
+ /* Validates recovery image header. */
+ @Test
+ public void testRecoveryImageHeader() throws Exception {
+ Assume.assumeTrue("Skipping test: A/B devices do not have a separate recovery partition",
+ mSlotSuffix.equals(""));
+ String[] options = {"-type", "l"};
+ ArrayList<String> recoveryPaths = TargetFileUtils.findFile(
+ mBlockDevPath, "recovery", Arrays.asList(options), mDevice);
+ CLog.d("Recovery path %s", recoveryPaths);
+ Assert.assertFalse(
+ "Unable to find path to recovery image on device.", recoveryPaths.isEmpty());
+ File localRecoveryImg = new File(mTemptFolder, "recovery.img");
+ Assert.assertTrue("Pull " + recoveryPaths.get(0) + " failed!",
+ mDevice.pullFile(recoveryPaths.get(0), localRecoveryImg));
+ CLog.d("Remote boot path %s", recoveryPaths);
+ CLog.d("Local boot path %s", localRecoveryImg.getAbsolutePath());
+ CheckImageHeader(localRecoveryImg.getAbsolutePath(), true);
+ }
+}