1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tests.firmwaredtbo;
17 
18 import com.android.tradefed.device.ITestDevice;
19 import com.android.tradefed.log.LogUtil.CLog;
20 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
21 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
22 import com.android.tradefed.util.CommandResult;
23 import com.android.tradefed.util.CommandStatus;
24 import com.android.tradefed.util.FileUtil;
25 import com.android.tradefed.util.RunUtil;
26 import com.android.tradefed.util.TargetFileUtils;
27 import java.io.BufferedInputStream;
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.RandomAccessFile;
33 import java.nio.ByteBuffer;
34 import java.nio.ByteOrder;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.zip.DataFormatException;
38 import java.util.zip.GZIPInputStream;
39 import java.util.zip.Inflater;
40 import org.junit.AfterClass;
41 import org.junit.Assert;
42 import org.junit.Assume;
43 import org.junit.Before;
44 import org.junit.BeforeClass;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 
48 /* Validates DTBO partition and DT overlay application. */
49 @RunWith(DeviceJUnit4ClassRunner.class)
50 public class FirmwareDtboVerification extends BaseHostJUnit4Test {
51     // Path to platform block devices
52     private static final String BLOCK_DEV_PATH = "/dev/block/platform";
53     // Temporary dir in device.
54     private static final String DEVICE_TEMP_DIR = "/data/local/tmp/";
55     // Path to device tree.
56     private static final String FDT_PATH = "/sys/firmware/fdt";
57     // Indicates current slot suffix for A/B devices.
58     private static final String PROPERTY_SLOT_SUFFIX = "ro.boot.slot_suffix";
59     // Offset in DT header to read compression info.
60     private static final int DT_HEADER_FLAGS_OFFSET = 16;
61     // Bit mask to get compression format from flags field of DT header for version 1 DTBO header.
62     private static final int COMPRESSION_FLAGS_BIT_MASK = 0x0f;
63     // FDT Magic.
64     private static final int FDT_MAGIC = 0xd00dfeed;
65     // mkdtboimg.py tool name
66     private static final String MKDTBOIMG_TOOL = "mkdtboimg.py";
67 
68     private static File mTemptFolder = null;
69     private ITestDevice mDevice;
70     private String mDeviceTestRoot = null;
71 
72     private int NO_COMPRESSION = 0x00;
73     private int ZLIB_COMPRESSION = 0x01;
74     private int GZIP_COMPRESSION = 0x02;
75 
76     @BeforeClass
oneTimeSetup()77     public static void oneTimeSetup() throws Exception {
78         mTemptFolder = FileUtil.createTempDir("firmware-dtbo-verify");
79         CLog.d("mTemptFolder: %s", mTemptFolder);
80     }
81 
82     @Before
setUp()83     public void setUp() throws Exception {
84         Assume.assumeFalse("Skipping test for x86 ABI", getAbi().getName().contains("x86"));
85         mDevice = getDevice();
86         mDeviceTestRoot = new File(DEVICE_TEMP_DIR, getAbi().getBitness()).getAbsolutePath();
87     }
88 
89     @AfterClass
postTestTearDown()90     public static void postTestTearDown() {
91         mTemptFolder.delete();
92     }
93 
94     /* Validates DTBO partition using mkdtboimg.py */
95     @Test
testCheckDTBOPartition()96     public void testCheckDTBOPartition() throws Exception {
97         // Dump dtbo image from device.
98         String slot_suffix = mDevice.getProperty(PROPERTY_SLOT_SUFFIX);
99         if (slot_suffix == null) {
100             slot_suffix = "";
101         }
102         String currentDtboPartition = "dtbo" + slot_suffix;
103         String[] options = {"-type", "l"};
104         ArrayList<String> dtboPaths = TargetFileUtils.findFile(
105                 BLOCK_DEV_PATH, currentDtboPartition, Arrays.asList(options), mDevice);
106         CLog.d("DTBO path %s", dtboPaths);
107         Assert.assertFalse("Unable to find path to dtbo image on device.", dtboPaths.isEmpty());
108         File hostDtboImage = new File(mTemptFolder, "dtbo");
109         Assert.assertTrue("Pull " + dtboPaths.get(0) + " failed!",
110                 mDevice.pullFile(dtboPaths.get(0), hostDtboImage));
111         CLog.d("hostDtboImage is %s", hostDtboImage);
112         // Using mkdtboimg.py to extract dtbo image.
113         File mkdtboimgBin = getTestInformation().getDependencyFile(MKDTBOIMG_TOOL, false);
114         File unpackedDtbo = new File(mTemptFolder, "dumped_dtbo");
115         RunUtil runUtil = new RunUtil();
116         String[] cmds = {mkdtboimgBin.getAbsolutePath(), "dump", hostDtboImage.getAbsolutePath(),
117                 "-b", unpackedDtbo.getAbsolutePath()};
118         long timeoutMs = 5 * 60 * 1000;
119         CommandResult result = runUtil.runTimedCmd(timeoutMs, cmds);
120         Assert.assertEquals("Invalid DTBO Image: " + result.getStderr(), CommandStatus.SUCCESS,
121                 result.getStatus());
122         decompressDTEntries(hostDtboImage, unpackedDtbo);
123     }
124 
125     /**
126      * Decompresses DT entries based on the flag field in the DT Entry header.
127      *
128      * @param hostDtboImage DTBO image on host.
129      * @param unpackedDtbo DTBO was unpacked.
130      */
decompressDTEntries(File hostDtboImage, File unpackedDtbo)131     private void decompressDTEntries(File hostDtboImage, File unpackedDtbo)
132             throws IOException, DataFormatException {
133         try (RandomAccessFile raf = new RandomAccessFile(hostDtboImage, "r")) {
134             byte[] bytes = new byte[4];
135             int[] dtboItems = new int[8];
136             final int DTBO_MAGIC_IDX = 0;
137             final int DTBO_TOTAL_SIZE_IDX = 1;
138             final int DTBO_HEADER_SIZE_IDX = 2;
139             final int DTBO_DT_ENTRY_SIZE_IDX = 3;
140             final int DTBO_DT_ENTRY_COUNT_IDX = 4;
141             final int DTBO_DT_ENTRY_OFFSET_IDX = 5;
142             final int DTBO_PAGE_SIZE_IDX = 6;
143             final int DTBO_VERSION_IDX = 7;
144             for (int i = 0; i < 8; i++) {
145                 raf.read(bytes);
146                 dtboItems[i] = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
147                 CLog.d("dtboItems[%s] is %s", i, dtboItems[i]);
148             }
149             if (dtboItems[DTBO_VERSION_IDX] > 0) {
150                 for (int dt_entry_idx = 0; dt_entry_idx < dtboItems[DTBO_DT_ENTRY_COUNT_IDX];
151                         dt_entry_idx++) {
152                     File dt_entry_file = new File(
153                             String.format("%s.%s", unpackedDtbo.getAbsolutePath(), dt_entry_idx));
154                     CLog.d("Checking %s", dt_entry_file.getAbsolutePath());
155                     raf.seek(dtboItems[DTBO_DT_ENTRY_OFFSET_IDX] + DT_HEADER_FLAGS_OFFSET);
156                     dtboItems[DTBO_DT_ENTRY_OFFSET_IDX] += dtboItems[DTBO_DT_ENTRY_SIZE_IDX];
157                     raf.read(bytes);
158                     int flags = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
159                     int compression_format = flags & COMPRESSION_FLAGS_BIT_MASK;
160                     CLog.d("compression_format= %s", compression_format);
161                     if ((compression_format != ZLIB_COMPRESSION)
162                             && (compression_format != GZIP_COMPRESSION)) {
163                         Assert.assertEquals(
164                                 String.format("Unknown compression format %d", compression_format),
165                                 compression_format, NO_COMPRESSION);
166                     }
167                     try (InputStream fileInputStream = new FileInputStream(dt_entry_file)) {
168                         byte[] cpio_header = new byte[6];
169                         if (compression_format == ZLIB_COMPRESSION) {
170                             CLog.d("Decompressing %s with ZLIB_COMPRESSION",
171                                     dt_entry_file.getAbsolutePath());
172                             byte[] compressedData = new byte[(int) dt_entry_file.length()];
173                             fileInputStream.read(compressedData);
174                             Inflater inflater = new Inflater();
175                             inflater.setInput(compressedData);
176                             inflater.inflate(cpio_header);
177                         } else if (compression_format == GZIP_COMPRESSION) {
178                             CLog.d("Decompressing %s with GZIP_COMPRESSION",
179                                     dt_entry_file.getAbsolutePath());
180                             try (GZIPInputStream gzipStream = new GZIPInputStream(
181                                          new BufferedInputStream(fileInputStream))) {
182                                 gzipStream.read(cpio_header);
183                             }
184                         } else {
185                             fileInputStream.read(cpio_header);
186                         }
187                         int fdt_magic =
188                                 ByteBuffer.wrap(cpio_header).order(ByteOrder.BIG_ENDIAN).getInt();
189                         CLog.d("fdt_magic: 0x%s", Integer.toHexString(fdt_magic));
190                         Assert.assertEquals(
191                                 "Bad FDT(Flattened Device Tree) Format", fdt_magic, FDT_MAGIC);
192                     }
193                 }
194             }
195         }
196     }
197 
198     /* Verifies application of DT overlays. */
199     @Test
testVerifyOverlay()200     public void testVerifyOverlay() throws Exception {
201         // testVerifyOverlay depend on testCheckDTBOPartition, check if previous test artifacts
202         // exist, if not force run testCheckDTBOPartition().
203         File hostDtboImage = new File(mTemptFolder, "dtbo");
204         if (!hostDtboImage.exists()) {
205             testCheckDTBOPartition();
206         }
207         String cmd = "cat /proc/cmdline |"
208                 + "grep -o \"'androidboot.dtbo_idx=[^ ]*'\" |"
209                 + "cut -d \"=\" -f 2 ";
210         CommandResult cmdResult = mDevice.executeShellV2Command(cmd);
211         Assert.assertEquals(String.format("Checking kernel dtbo index: %s", cmdResult.getStderr()),
212                 cmdResult.getExitCode().intValue(), 0);
213         String overlay_idx_string = cmdResult.getStdout().replace("\n", "");
214         CLog.d("overlay_idx_string=%s", overlay_idx_string);
215         Assert.assertNotEquals(
216                 "Kernel command line missing androidboot.dtbo_idx", overlay_idx_string.length(), 0);
217         String verificationTestPath = null;
218         String ufdtVerifier = "ufdt_verify_overlay";
219         ArrayList<String> matchedResults =
220                 TargetFileUtils.findFile(DEVICE_TEMP_DIR, ufdtVerifier, null, mDevice);
221         for (String matchedResult : matchedResults) {
222             if (!mDevice.isDirectory(matchedResult)) {
223                 verificationTestPath = matchedResult;
224                 break;
225             }
226         }
227         String chmodCommand = String.format("chmod 755 %s", verificationTestPath);
228         CLog.d(chmodCommand);
229         cmdResult = mDevice.executeShellV2Command(chmodCommand);
230         Assert.assertEquals("Unable to chmod:" + cmdResult.getStderr(), cmdResult.getStatus(),
231                 CommandStatus.SUCCESS);
232         String ufdtVerifierParent = new File(verificationTestPath).getParent();
233         String remoteFinalDTPath = new File(ufdtVerifierParent, "final_dt").getAbsolutePath();
234         String copyCommand = String.format("cp %s %s", FDT_PATH, remoteFinalDTPath);
235         CLog.d(copyCommand);
236         cmdResult = mDevice.executeShellV2Command(String.format(copyCommand));
237         Assert.assertEquals("Unable to copy to " + remoteFinalDTPath, cmdResult.getStatus(),
238                 CommandStatus.SUCCESS);
239         ArrayList<String> overlayArg = new ArrayList<>();
240         for (String overlay_idx : overlay_idx_string.split(",")) {
241             String overlayFileName = "dumped_dtbo." + overlay_idx.replaceAll("\\s+$", "");
242             File overlayFile = new File(mTemptFolder, overlayFileName);
243             // Push the dumped overlay dtbo files to the same direcly of ufdt_verify_overlay
244             File remoteOverLayFile = new File(ufdtVerifierParent, overlayFileName);
245             CLog.d("Push remoteOverLayFile %s", remoteOverLayFile);
246             if (!mDevice.pushFile(overlayFile, remoteOverLayFile.getAbsolutePath())) {
247                 Assert.fail("Push " + overlayFile + "fail!");
248             }
249             overlayArg.add(overlayFileName);
250         }
251         String verifyCmd = String.format("cd %s && ./ufdt_verify_overlay final_dt %s",
252                 ufdtVerifierParent, String.join(" ", overlayArg));
253         CLog.d(verifyCmd);
254         cmdResult = mDevice.executeShellV2Command(verifyCmd);
255         Assert.assertEquals("Incorrect Overlay Application:" + cmdResult.getStderr(),
256                 cmdResult.getStatus(), CommandStatus.SUCCESS);
257     }
258 }
259