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);
+    }
+}