1 /*
2  * Copyright (C) 2019 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 
17 package com.android.build.tests;
18 
19 import static org.junit.Assert.assertEquals;
20 
21 import com.android.tradefed.build.IBuildInfo;
22 import com.android.tradefed.config.Option;
23 import com.android.tradefed.config.OptionClass;
24 import com.android.tradefed.log.LogUtil.CLog;
25 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
26 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
27 import com.android.tradefed.util.CommandResult;
28 import com.android.tradefed.util.CommandStatus;
29 import com.android.tradefed.util.FileUtil;
30 import com.android.tradefed.util.RunUtil;
31 
32 import org.junit.Before;
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 
36 import java.io.File;
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.stream.Stream;
40 
41 /** A device-less test that test kernel image */
42 @OptionClass(alias = "kernel-image-check")
43 @RunWith(DeviceJUnit4ClassRunner.class)
44 public class KernelImageCheck extends BaseHostJUnit4Test {
45 
46     private static final String KERNEL_IMAGE_NAME = "vmlinux";
47     private static final String KERNEL_IMAGE_REPO = "common";
48     private static final int CMD_TIMEOUT = 1000000;
49 
50     @Option(
51         name = "kernel-image-check-tool",
52         description = "The file path of kernel image check tool (mandatory)",
53         mandatory = true
54     )
55     private File mKernelImageCheckTool = null;
56 
57     @Option(
58         name = "kernel-image-name",
59         description = "The file name of the kernel image. Default: vmlinux"
60     )
61     private String mKernelImageName = KERNEL_IMAGE_NAME;
62 
63     @Option(
64         name = "kernel-image-alt-path",
65         description = "The kernel image alternative path string"
66     )
67     private String mKernelImageAltPath = null;
68 
69     @Option(
70         name = "kernel-abi-file",
71         description = "The file path of kernel ABI file"
72     )
73     private File mKernelAbiFile = null;
74 
75     private IBuildInfo mBuildInfo;
76     private File mKernelImageFile = null;
77     private File mKernelAbiWhitelist = null;
78 
79     @Before
setUp()80     public void setUp() throws Exception {
81         if (mKernelImageCheckTool == null || !mKernelImageCheckTool.exists()) {
82             throw new IOException("Cannot find kernel image tool at: " + mKernelImageCheckTool);
83         }
84 
85         // If --kernel-abi-file has not been specified, try to find 'abi.xml'
86         // within the downloaded files from the build.
87         if (mKernelAbiFile == null) {
88             mKernelAbiFile = getBuild().getFile("abi.xml");
89         }
90 
91         // If there was not any 'abi.xml' and --kernel-abi-file was not set to
92         // point at an external one, throw that.
93         if (mKernelAbiFile == null) {
94             throw new IOException("Cannot find abi.xml within the build results.");
95         }
96 
97         if (!mKernelAbiFile.exists()) {
98             throw new IOException("Cannot find kernel ABI representation at: " + mKernelAbiFile);
99         }
100         // First try to get kernel image from BuildInfo
101         mKernelImageFile = getBuild().getFile(mKernelImageName);
102         if ((mKernelImageFile == null || !mKernelImageFile.exists())
103                 && mKernelImageAltPath != null) {
104             // Then check within alternative path.
105             File imageDir = new File(mKernelImageAltPath);
106             if (imageDir.isDirectory()) {
107                 mKernelImageFile = new File(imageDir, mKernelImageName);
108             }
109         }
110 
111         if (mKernelImageFile == null || !mKernelImageFile.exists()) {
112             throw new RuntimeException("Cannot find kernel image file: " + mKernelImageName);
113         }
114 
115         // This is an optional parameter. No need to check for an error here.
116         mKernelAbiWhitelist = getBuild().getFile("abi_whitelist");
117         if ((mKernelAbiWhitelist == null || !mKernelAbiWhitelist.exists())
118                 && mKernelImageAltPath != null) {
119             // Then check within alternative path.
120             File imageDir = new File(mKernelImageAltPath);
121             if (imageDir.isDirectory()) {
122                 mKernelAbiWhitelist = new File(imageDir, "abi_whitelist");
123             }
124         }
125     }
126 
127     /** Test that kernel ABI is not different from the given ABI representation */
128     @Test
test_stable_abi()129     public void test_stable_abi() throws Exception {
130         // Figure out location of Linux source tree by inspecting vmlinux.
131         String[] cmd = new String[] {"strings", mKernelImageFile.getPath(), "-d"};
132         CommandResult result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, cmd);
133         assertEquals(CommandStatus.SUCCESS, result.getStatus());
134 
135         String vmlinuxStrings = result.getStdout();
136         File vmlinuxStringsFile = FileUtil.createTempFile("vmlinuxStrings", ".txt");
137         FileUtil.writeToFile(vmlinuxStrings, vmlinuxStringsFile);
138 
139         cmd = new String[] {"grep", "-m", "1", "init/main.c", vmlinuxStringsFile.getAbsolutePath()};
140         result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, cmd);
141         assertEquals(CommandStatus.SUCCESS, result.getStatus());
142 
143         String repoRootDir = result.getStdout();
144         // Source file absolute path is /repoRootDir/KERNEL_IMAGE_REPO/location-in-linux-tree.
145         repoRootDir = repoRootDir.split("/" + KERNEL_IMAGE_REPO + "/", 2)[0];
146 
147         // Try to set the executable bit for abidw and abidiff
148         Stream.of("abidw", "abidiff")
149                 .forEach(
150                         bin -> {
151                             String[] chmod =
152                                     new String[] {
153                                         "chmod",
154                                         "u+x",
155                                         mKernelImageCheckTool.getAbsolutePath() + "/" + bin
156                                     };
157                             RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, chmod);
158                         });
159 
160         // Generate kernel ABI
161         ArrayList<String> abidwCmd = new ArrayList<String>();
162         abidwCmd.add(mKernelImageCheckTool.getAbsolutePath() + "/abidw");
163         // omit various sources of indeterministic abidw output
164         abidwCmd.add("--no-corpus-path");
165         abidwCmd.add("--no-comp-dir-path");
166         // the path containing vmlinux and *.ko
167         abidwCmd.add("--linux-tree");
168         abidwCmd.add(mKernelImageFile.getParent());
169         abidwCmd.add("--out-file");
170         abidwCmd.add("abi-new.xml");
171         if (mKernelAbiWhitelist != null && mKernelAbiWhitelist.exists()) {
172             abidwCmd.add("--kmi-whitelist");
173             abidwCmd.add(mKernelAbiWhitelist.getAbsolutePath());
174         }
175         result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, abidwCmd.toArray(new String[0]));
176         CLog.i("Result stdout: %s", result.getStdout());
177         // TODO: differentiate non-zero exit codes.
178         if (result.getExitCode() != 0) {
179             CLog.e("Result stderr: %s", result.getStderr());
180             CLog.e("Result exit code: %d", result.getExitCode());
181         }
182         assertEquals(CommandStatus.SUCCESS, result.getStatus());
183 
184         // Sanitize abi-new.xml by removing any occurences of the kernel path.
185         cmd =
186                 new String[] {
187                     "sed",
188                     "-i",
189                     "s#" + repoRootDir + "/" + KERNEL_IMAGE_REPO + "/##g",
190                     "abi-new.xml"
191                 };
192         result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, cmd);
193         assertEquals(CommandStatus.SUCCESS, result.getStatus());
194 
195         cmd = new String[] {"sed", "-i", "s#" + repoRootDir + "/##g", "abi-new.xml"};
196         result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, cmd);
197         assertEquals(CommandStatus.SUCCESS, result.getStatus());
198 
199         // Diff kernel ABI with the given ABI file
200         ArrayList<String> abidiffCmd = new ArrayList<String>();
201         abidiffCmd.add(mKernelImageCheckTool.getAbsolutePath() + "/abidiff");
202         abidiffCmd.add("--impacted-interfaces");
203         abidiffCmd.add("--leaf-changes-only");
204         abidiffCmd.add("--dump-diff-tree");
205         abidiffCmd.add(mKernelAbiFile.getAbsolutePath());
206         abidiffCmd.add("abi-new.xml");
207         if (mKernelAbiWhitelist != null && mKernelAbiWhitelist.exists()) {
208             abidiffCmd.add("--kmi-whitelist");
209             abidiffCmd.add(mKernelAbiWhitelist.getAbsolutePath());
210         }
211         result = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT, abidiffCmd.toArray(new String[0]));
212         CLog.i("Result stdout: %s", result.getStdout());
213         if (result.getExitCode() != 0) {
214             CLog.e("Result stderr: %s", result.getStderr());
215             CLog.e("Result exit code: %d", result.getExitCode());
216         }
217         assertEquals(
218                 "Kernel's ABI has changed. See go/kernel-abi-monitoring for details.\n",
219                 CommandStatus.SUCCESS,
220                 result.getStatus());
221     }
222 }
223