1 /*
2  * Copyright (C) 2016 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 android.compilation.cts;
18 
19 import com.google.common.io.ByteStreams;
20 import com.google.common.io.Files;
21 
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.ITestDevice;
24 import com.android.tradefed.testtype.DeviceTestCase;
25 import com.android.tradefed.util.FileUtil;
26 
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.EnumSet;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Objects;
38 import java.util.Set;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41 
42 /**
43  * Various integration tests for dex to oat compilation, with or without profiles.
44  * When changing this test, make sure it still passes in each of the following
45  * configurations:
46  * <ul>
47  *     <li>On a 'user' build</li>
48  *     <li>On a 'userdebug' build with system property 'dalvik.vm.usejitprofiles' set to false</li>
49  *     <li>On a 'userdebug' build with system property 'dalvik.vm.usejitprofiles' set to true</li>
50  * </ul>
51  */
52 public class AdbRootDependentCompilationTest extends DeviceTestCase {
53     private static final String APPLICATION_PACKAGE = "android.compilation.cts";
54 
55     enum ProfileLocation {
56         CUR("/data/misc/profiles/cur/0/" + APPLICATION_PACKAGE),
57         REF("/data/misc/profiles/ref/" + APPLICATION_PACKAGE);
58 
59         private String directory;
60 
ProfileLocation(String directory)61         ProfileLocation(String directory) {
62             this.directory = directory;
63         }
64 
getDirectory()65         public String getDirectory() {
66             return directory;
67         }
68 
getPath()69         public String getPath() {
70             return directory + "/primary.prof";
71         }
72     }
73 
74     private ITestDevice mDevice;
75     private File textProfileFile;
76     private byte[] initialOdexFileContents;
77     private File apkFile;
78     private boolean mCanEnableDeviceRootAccess;
79 
80     private Matcher mAdbLineFilter;
81 
82     @Override
setUp()83     protected void setUp() throws Exception {
84         super.setUp();
85         mDevice = getDevice();
86 
87         String buildType = mDevice.getProperty("ro.build.type");
88         assertTrue("Unknown build type: " + buildType,
89                 Arrays.asList("user", "userdebug", "eng").contains(buildType));
90         boolean wasRoot = mDevice.isAdbRoot();
91         // We can only enable root access on userdebug and eng builds.
92         mCanEnableDeviceRootAccess = buildType.equals("userdebug") || buildType.equals("eng");
93 
94         apkFile = File.createTempFile("CtsCompilationApp", ".apk");
95         try (OutputStream outputStream = new FileOutputStream(apkFile)) {
96             InputStream inputStream = getClass().getResourceAsStream("/CtsCompilationApp.apk");
97             ByteStreams.copy(inputStream, outputStream);
98         }
99         mDevice.uninstallPackage(APPLICATION_PACKAGE); // in case it's still installed
100         String error = mDevice.installPackage(apkFile, false);
101         assertNull("Got install error: " + error, error);
102 
103         // Write the text profile to a temporary file so that we can run profman on it to create a
104         // real profile.
105         byte[] profileBytes = ByteStreams.toByteArray(
106                 getClass().getResourceAsStream("/primary.prof.txt"));
107         assertTrue("empty profile", profileBytes.length > 0); // validity check
108         textProfileFile = File.createTempFile("compilationtest", "prof.txt");
109         Files.write(profileBytes, textProfileFile);
110 
111         // Ignore issues in cmd.
112         mAdbLineFilter = Pattern.compile("FORTIFY: pthread_mutex_lock.*").matcher("");
113     }
114 
115     @Override
tearDown()116     protected void tearDown() throws Exception {
117         FileUtil.deleteFile(apkFile);
118         FileUtil.deleteFile(textProfileFile);
119         mDevice.uninstallPackage(APPLICATION_PACKAGE);
120         super.tearDown();
121     }
122 
123     /**
124      * Tests compilation using {@code -r bg-dexopt -f}.
125      */
testCompile_bgDexopt()126     public void testCompile_bgDexopt() throws Exception {
127         if (!canRunTest(EnumSet.noneOf(ProfileLocation.class))) {
128             return;
129         }
130 
131         resetProfileState();
132 
133         // Copy the profile to the reference location so that the bg-dexopt
134         // can actually do work if it's configured to speed-profile.
135         for (ProfileLocation profileLocation : EnumSet.of(ProfileLocation.REF)) {
136             writeProfile(profileLocation);
137         }
138 
139         // Usually "interpret-only"
140         String expectedInstallFilter = Objects.requireNonNull(mDevice.getProperty("pm.dexopt.install"));
141         if (expectedInstallFilter.equals("speed-profile")) {
142             // If the filter is speed-profile but no profile is present, the compiler
143             // will change it to verify.
144             expectedInstallFilter = "verify";
145         }
146         // Usually "speed-profile"
147         String expectedBgDexoptFilter = Objects.requireNonNull(mDevice.getProperty("pm.dexopt.bg-dexopt"));
148 
149         String odexPath = getOdexFilePath();
150         assertEquals(expectedInstallFilter, getCompilerFilter(odexPath));
151 
152         // Without -f, the compiler would only run if it judged the bg-dexopt filter to
153         // be "better" than the install filter. However manufacturers can change those
154         // values so we don't want to depend here on the resulting filter being better.
155         executeCompile("-r", "bg-dexopt", "-f");
156 
157         assertEquals(expectedBgDexoptFilter, getCompilerFilter(odexPath));
158     }
159 
160     /*
161      The tests below test the remaining combinations of the "ref" (reference) and
162      "cur" (current) profile being available. The "cur" profile gets moved/merged
163      into the "ref" profile when it differs enough; as of 2016-05-10, "differs
164      enough" is based on number of methods and classes in profile_assistant.cc.
165 
166      No nonempty profile exists right after an app is installed.
167      Once the app runs, a profile will get collected in "cur" first but
168      may make it to "ref" later. While the profile is being processed by
169      profile_assistant, it may only be available in "ref".
170      */
171 
testCompile_noProfile()172     public void testCompile_noProfile() throws Exception {
173         compileWithProfilesAndCheckFilter(false /* expectOdexChange */,
174                 EnumSet.noneOf(ProfileLocation.class));
175     }
176 
testCompile_curProfile()177     public void testCompile_curProfile() throws Exception {
178         boolean didRun = compileWithProfilesAndCheckFilter(true  /* expectOdexChange */,
179                  EnumSet.of(ProfileLocation.CUR));
180         if (didRun) {
181             assertTrue("ref profile should have been created by the compiler",
182                     doesFileExist(ProfileLocation.REF.getPath()));
183         }
184     }
185 
testCompile_refProfile()186     public void testCompile_refProfile() throws Exception {
187         compileWithProfilesAndCheckFilter(true /* expectOdexChange */,
188                  EnumSet.of(ProfileLocation.REF));
189         // expect a change in odex because the of the change form
190         // verify -> speed-profile
191     }
192 
testCompile_curAndRefProfile()193     public void testCompile_curAndRefProfile() throws Exception {
194         compileWithProfilesAndCheckFilter(true /* expectOdexChange */,
195                 EnumSet.of(ProfileLocation.CUR, ProfileLocation.REF));
196         // expect a change in odex because the of the change form
197         // verify -> speed-profile
198     }
199 
readFileOnClient(String clientPath)200     private byte[] readFileOnClient(String clientPath) throws Exception {
201         assertTrue("File not found on client: " + clientPath,
202                 doesFileExist(clientPath));
203         File copyOnHost = File.createTempFile("host", "copy");
204         try {
205             executePull(clientPath, copyOnHost.getPath());
206             return Files.toByteArray(copyOnHost);
207         } finally {
208             FileUtil.deleteFile(copyOnHost);
209         }
210     }
211 
212     /**
213      * Places the profile in the specified locations, recompiles (without -f)
214      * and checks the compiler-filter in the odex file.
215      *
216      * @return whether the test ran (as opposed to early exit)
217      */
compileWithProfilesAndCheckFilter(boolean expectOdexChange, Set<ProfileLocation> profileLocations)218     private boolean compileWithProfilesAndCheckFilter(boolean expectOdexChange,
219             Set<ProfileLocation> profileLocations)
220             throws Exception {
221         if (!canRunTest(profileLocations)) {
222             return false;
223         }
224 
225         resetProfileState();
226 
227         executeCompile("-m", "speed-profile", "-f");
228         String odexFilePath = getOdexFilePath();
229         byte[] initialOdexFileContents = readFileOnClient(odexFilePath);
230         assertTrue("empty odex file", initialOdexFileContents.length > 0); // validity check
231 
232         for (ProfileLocation profileLocation : profileLocations) {
233             writeProfile(profileLocation);
234         }
235         executeCompile("-m", "speed-profile");
236 
237         // Confirm the compiler-filter used in creating the odex file
238         String compilerFilter = getCompilerFilter(odexFilePath);
239 
240         // Without profiles, the compiler filter should be verify.
241         String expectedCompilerFilter = profileLocations.isEmpty() ? "verify" : "speed-profile";
242         assertEquals("compiler-filter", expectedCompilerFilter, compilerFilter);
243 
244         byte[] odexFileContents = readFileOnClient(odexFilePath);
245         boolean odexChanged = !(Arrays.equals(initialOdexFileContents, odexFileContents));
246         if (odexChanged && !expectOdexChange) {
247             String msg = String.format(Locale.US, "Odex file without filters (%d bytes) "
248                     + "unexpectedly different from odex file (%d bytes) compiled with filters: %s",
249                     initialOdexFileContents.length, odexFileContents.length, profileLocations);
250             fail(msg);
251         } else if (!odexChanged && expectOdexChange) {
252             fail("odex file should have changed when recompiling with " + profileLocations);
253         }
254         return true;
255     }
256 
resetProfileState()257     public void resetProfileState() throws Exception {
258         executeSuShellAdbCommand(0, "rm", "-f", ProfileLocation.REF.getPath());
259         executeSuShellAdbCommand(0, "truncate", "-s", "0", ProfileLocation.CUR.getPath());
260     }
261 
262     /**
263      * Invokes the dex2oat compiler on the client.
264      *
265      * @param compileOptions extra options to pass to the compiler on the command line
266      */
executeCompile(String... compileOptions)267     private void executeCompile(String... compileOptions) throws Exception {
268         List<String> command = new ArrayList<>(Arrays.asList("cmd", "package", "compile"));
269         command.addAll(Arrays.asList(compileOptions));
270         command.add(APPLICATION_PACKAGE);
271         String[] commandArray = command.toArray(new String[0]);
272         assertEquals("Success", executeSuShellAdbCommand(1, commandArray)[0]);
273     }
274 
275     /**
276      * Copies {@link #textProfileFile} to the device and convert it to a binary profile on the
277      * client device.
278      */
writeProfile(ProfileLocation location)279     private void writeProfile(ProfileLocation location) throws Exception {
280         String targetPath = location.getPath();
281         // Get the owner of the parent directory so we can set it on the file
282         String targetDir = location.getDirectory();
283         if (!doesFileExist(targetDir)) {
284             fail("Not found: " + targetPath);
285         }
286         // in format group:user so we can directly pass it to chown
287         String owner = executeSuShellAdbCommand(1, "stat", "-c", "%U:%g", targetDir)[0];
288         // for some reason, I've observed the output starting with a single space
289         while (owner.startsWith(" ")) {
290             owner = owner.substring(1);
291         }
292 
293         String targetPathTemp = targetPath + ".tmp";
294         executePush(textProfileFile.getAbsolutePath(), targetPathTemp, targetDir);
295         assertTrue("Failed to push text profile", doesFileExist(targetPathTemp));
296 
297         String targetPathApk = targetPath + ".apk";
298         executePush(apkFile.getAbsolutePath(), targetPathApk, targetDir);
299         assertTrue("Failed to push APK from ", doesFileExist(targetPathApk));
300         // Run profman to create the real profile on device.
301         String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0];
302         pathSpec = pathSpec.replace("package:", "");
303         assertTrue("Failed find APK " + pathSpec, doesFileExist(pathSpec));
304         executeSuShellAdbCommand(
305                 "profman",
306                 "--create-profile-from=" + targetPathTemp,
307                 "--apk=" + pathSpec,
308                 "--dex-location=" + pathSpec,
309                 "--reference-profile-file=" + targetPath);
310         executeSuShellAdbCommand(0, "chown", owner, targetPath);
311         // Verify that the file was written successfully
312         assertTrue("failed to create profile file", doesFileExist(targetPath));
313         String[] result = executeSuShellAdbCommand(1, "stat", "-c", "%s", targetPath);
314         assertTrue("profile " + targetPath + " is " + Integer.parseInt(result[0]) + " bytes",
315                    Integer.parseInt(result[0]) > 0);
316     }
317 
318     /**
319      * Parses the value for the key "compiler-filter" out of the output from
320      * {@code oatdump --header-only}.
321      */
getCompilerFilter(String odexFilePath)322     private String getCompilerFilter(String odexFilePath) throws DeviceNotAvailableException {
323         String[] response = executeSuShellAdbCommand(
324                 "oatdump", "--header-only", "--oat-file=" + odexFilePath);
325         String prefix = "compiler-filter =";
326         for (String line : response) {
327             line = line.trim();
328             if (line.startsWith(prefix)) {
329                 return line.substring(prefix.length()).trim();
330             }
331         }
332         fail("No occurence of \"" + prefix + "\" in: " + Arrays.toString(response));
333         return null;
334     }
335 
336     /**
337      * Returns the path to the application's base.odex file that should have
338      * been created by the compiler.
339      */
getOdexFilePath()340     private String getOdexFilePath() throws DeviceNotAvailableException {
341         // Something like "package:/data/app/android.compilation.cts-1/base.apk"
342         String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0];
343         Matcher matcher = Pattern.compile("^package:(.+/)base\\.apk$").matcher(pathSpec);
344         boolean found = matcher.find();
345         assertTrue("Malformed spec: " + pathSpec, found);
346         String apkDir = matcher.group(1);
347         // E.g. /data/app/android.compilation.cts-1/oat/arm64/base.odex
348         String result = executeSuShellAdbCommand(1, "find", apkDir, "-name", "base.odex")[0];
349         assertTrue("odex file not found: " + result, doesFileExist(result));
350         return result;
351     }
352 
353     /**
354      * Returns whether a test that uses the given profileLocations can run
355      * in the current device configuration. This allows tests to exit early.
356      *
357      * <p>Ideally we'd like tests to be marked as skipped/ignored or similar
358      * rather than passing if they can't run on the current device, but that
359      * doesn't seem to be supported by CTS as of 2016-05-24.
360      * TODO: Use Assume.assumeTrue() if this test gets converted to JUnit 4.
361      */
canRunTest(Set<ProfileLocation> profileLocations)362     private boolean canRunTest(Set<ProfileLocation> profileLocations) throws Exception {
363         boolean result = mCanEnableDeviceRootAccess &&
364                 (profileLocations.isEmpty() || isUseJitProfiles());
365         if (!result) {
366             System.err.printf("Skipping test [mCanEnableDeviceRootAccess=%s, %d profiles] on %s\n",
367                     mCanEnableDeviceRootAccess, profileLocations.size(), mDevice);
368         }
369         return result;
370     }
371 
isUseJitProfiles()372     private boolean isUseJitProfiles() throws Exception {
373         boolean propUseJitProfiles = Boolean.parseBoolean(
374                 executeSuShellAdbCommand(1, "getprop", "dalvik.vm.usejitprofiles")[0]);
375         return propUseJitProfiles;
376     }
377 
filterAdbLines(String[] lines)378     private String[] filterAdbLines(String[] lines) {
379         List<String> linesList = new ArrayList<String>(Arrays.asList(lines));
380         Iterator<String> it = linesList.iterator();
381         while (it.hasNext()) {
382             String line = it.next();
383             mAdbLineFilter.reset(line);
384             if (mAdbLineFilter.matches()) {
385                 it.remove();
386             }
387         }
388         if (linesList.size() != lines.length) {
389             return linesList.toArray(new String[linesList.size()]);
390         }
391         return lines;
392     }
393 
executeSuShellAdbCommand(int numLinesOutputExpected, String... command)394     private String[] executeSuShellAdbCommand(int numLinesOutputExpected, String... command)
395             throws DeviceNotAvailableException {
396         String[] lines = filterAdbLines(executeSuShellAdbCommand(command));
397         assertEquals(
398                 String.format(Locale.US, "Expected %d lines output, got %d running %s: %s",
399                         numLinesOutputExpected, lines.length, Arrays.toString(command),
400                         Arrays.toString(lines)),
401                 numLinesOutputExpected, lines.length);
402         return lines;
403     }
404 
executeSuShellAdbCommand(String... command)405     private String[] executeSuShellAdbCommand(String... command)
406             throws DeviceNotAvailableException {
407         // Add `shell su root` to the adb command.
408         String cmdString = String.join(" ", command);
409         String output = mDevice.executeShellCommand("su root " + cmdString);
410         // "".split() returns { "" }, but we want an empty array
411         String[] lines = output.equals("") ? new String[0] : output.split("\n");
412         return filterAdbLines(lines);
413     }
414 
getSelinuxLabel(String path)415     private String getSelinuxLabel(String path) throws DeviceNotAvailableException {
416         // ls -aZ (-a so it sees directories, -Z so it prints the label).
417         String[] res = executeSuShellAdbCommand(String.format(
418             "ls -aZ '%s'", path));
419 
420         if (res.length == 0) {
421           return null;
422         }
423 
424         // For directories, it will print many outputs. Filter to first line which contains '.'
425         // The target line will look like
426         //      "u:object_r:shell_data_file:s0 /data/local/tmp/android.compilation.cts.primary.prof"
427         // Remove the second word to only return "u:object_r:shell_data_file:s0".
428 
429         return res[0].replaceAll("\\s+.*","");  // remove everything following the first whitespace
430     }
431 
checkSelinuxLabelMatches(String a, String b)432     private void checkSelinuxLabelMatches(String a, String b) throws DeviceNotAvailableException {
433       String labelA = getSelinuxLabel(a);
434       String labelB = getSelinuxLabel(b);
435 
436       assertEquals("expected the selinux labels to match", labelA, labelB);
437     }
438 
executePush(String hostPath, String targetPath, String targetDirectory)439     private void executePush(String hostPath, String targetPath, String targetDirectory)
440             throws DeviceNotAvailableException {
441         // Cannot push to a privileged directory with one command.
442         // (i.e. there is no single-command equivalent of 'adb root; adb push src dst')
443         //
444         // Push to a tmp directory and then move it to the final destination
445         // after updating the selinux label.
446         String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".push.tmp";
447         assertTrue(mDevice.pushFile(new File(hostPath), tmpPath));
448 
449         // Important: Use "cp" here because it newly copied files will inherit the security context
450         // of the targetDirectory according to the default policy.
451         //
452         // (Other approaches, such as moving the file retain the invalid security context
453         // of the tmp directory - b/37425296)
454         //
455         // This mimics the behavior of 'adb root; adb push $targetPath'.
456         executeSuShellAdbCommand("mv", tmpPath, targetPath);
457 
458         // Important: Use "restorecon" here because the file in tmpPath retains the
459         // incompatible security context of /data/local/tmp.
460         //
461         // This mimics the behavior of 'adb root; adb push $targetPath'.
462         executeSuShellAdbCommand("restorecon", targetPath);
463 
464         // Validate that the security context of the file matches the security context
465         // of the directory it was pushed to.
466         //
467         // This is a reasonable default behavior to check because most selinux policies
468         // are configured to behave like this.
469         checkSelinuxLabelMatches(targetDirectory, targetPath);
470     }
471 
executePull(String targetPath, String hostPath)472     private void executePull(String targetPath, String hostPath)
473             throws DeviceNotAvailableException {
474         String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".pull.tmp";
475         executeSuShellAdbCommand("cp", targetPath, tmpPath);
476         try {
477             executeSuShellAdbCommand("chmod", "606", tmpPath);
478             assertTrue(mDevice.pullFile(tmpPath, new File(hostPath)));
479         } finally {
480             executeSuShellAdbCommand("rm", tmpPath);
481         }
482     }
483 
doesFileExist(String path)484     private boolean doesFileExist(String path) throws DeviceNotAvailableException {
485         String[] result = executeSuShellAdbCommand("ls", path);
486         // Testing for empty directories will return an empty array.
487         return !(result.length > 0 && result[0].contains("No such file"));
488     }
489 }
490