1 /* 2 * Copyright (C) 2022 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.test.hostside; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 23 import com.android.tradefed.build.IBuildInfo; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.invoker.IInvocationContext; 27 import com.android.tradefed.invoker.TestInformation; 28 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 29 import com.android.tradefed.testtype.IAbi; 30 import com.android.tradefed.testtype.junit4.AfterClassWithInfo; 31 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 32 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo; 33 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions; 34 import com.android.tradefed.util.CommandResult; 35 36 import com.google.common.io.ByteStreams; 37 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 41 import java.io.File; 42 import java.io.FileOutputStream; 43 import java.io.InputStream; 44 import java.io.OutputStream; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.List; 48 import java.util.stream.Collectors; 49 import java.util.zip.ZipEntry; 50 import java.util.zip.ZipFile; 51 52 /** 53 * Test libnativeloader behavior for apps and libs in various partitions by overlaying them over 54 * the system partitions. Requires root. 55 */ 56 @RunWith(DeviceJUnit4ClassRunner.class) 57 public class LibnativeloaderTest extends BaseHostJUnit4Test { 58 private static final String TAG = "LibnativeloaderTest"; 59 private static final String CLEANUP_PATHS_KEY = TAG + ":CLEANUP_PATHS"; 60 private static final String LOG_FILE_NAME = "TestActivity.log"; 61 62 @BeforeClassWithInfo beforeClassWithDevice(TestInformation testInfo)63 public static void beforeClassWithDevice(TestInformation testInfo) throws Exception { 64 DeviceContext ctx = new DeviceContext(testInfo); 65 66 // A soft reboot is slow, so do setup for all tests and reboot once. 67 68 File libContainerApk = ctx.mBuildHelper.getTestFile("library_container_app.apk"); 69 try (ZipFile libApk = new ZipFile(libContainerApk)) { 70 ctx.pushExtendedPublicSystemOemLibs(libApk); 71 ctx.pushExtendedPublicProductLibs(libApk); 72 ctx.pushPrivateLibs(libApk); 73 } 74 ctx.pushSharedLib( 75 "/system", "android.test.systemsharedlib", "libnativeloader_system_shared_lib.jar"); 76 ctx.pushSharedLib("/system_ext", "android.test.systemextsharedlib", 77 "libnativeloader_system_ext_shared_lib.jar"); 78 ctx.pushSharedLib("/product", "android.test.productsharedlib", 79 "libnativeloader_product_shared_lib.jar"); 80 ctx.pushSharedLib( 81 "/vendor", "android.test.vendorsharedlib", "libnativeloader_vendor_shared_lib.jar"); 82 83 // "Install" apps in various partitions through plain adb push followed by a soft reboot. We 84 // need them in these locations to test library loading restrictions, so for all except 85 // loadlibrarytest_data_app we cannot use ITestDevice.installPackage for it since it only 86 // installs in /data. 87 88 // For testSystemPrivApp 89 ctx.pushApk("loadlibrarytest_system_priv_app", "/system/priv-app"); 90 91 // For testSystemApp 92 ctx.pushApk("loadlibrarytest_system_app", "/system/app"); 93 94 // For testSystemExtApp 95 ctx.pushApk("loadlibrarytest_system_ext_app", "/system_ext/app"); 96 97 // For testProductApp 98 ctx.pushApk("loadlibrarytest_product_app", "/product/app"); 99 100 // For testVendorApp 101 ctx.pushApk("loadlibrarytest_vendor_app", "/vendor/app"); 102 103 ctx.softReboot(); 104 105 // For testDataApp. Install this the normal way after the system server restart. 106 ctx.installPackage("loadlibrarytest_data_app"); 107 108 testInfo.properties().put(CLEANUP_PATHS_KEY, ctx.mCleanup.getPathList()); 109 } 110 111 @AfterClassWithInfo afterClassWithDevice(TestInformation testInfo)112 public static void afterClassWithDevice(TestInformation testInfo) throws Exception { 113 DeviceContext ctx = new DeviceContext(testInfo); 114 115 // Uninstall loadlibrarytest_data_app. 116 ctx.mDevice.uninstallPackage("android.test.app.data"); 117 118 String cleanupPathList = testInfo.properties().get(CLEANUP_PATHS_KEY); 119 CleanupPaths cleanup = new CleanupPaths(ctx.mDevice, cleanupPathList); 120 cleanup.cleanup(); 121 } 122 123 @Test testSystemPrivApp()124 public void testSystemPrivApp() throws Exception { 125 // There's currently no difference in the tests between /system/priv-app and /system/app, so 126 // let's reuse the same one. 127 runTests("android.test.app.system_priv", "android.test.app.SystemAppTest"); 128 } 129 130 @Test testSystemApp()131 public void testSystemApp() throws Exception { 132 runTests("android.test.app.system", "android.test.app.SystemAppTest"); 133 } 134 135 @Test testSystemExtApp()136 public void testSystemExtApp() throws Exception { 137 // /system_ext should behave the same as /system, so run the same test class there. 138 runTests("android.test.app.system_ext", "android.test.app.SystemAppTest"); 139 } 140 141 @Test testProductApp()142 public void testProductApp() throws Exception { 143 runTests("android.test.app.product", "android.test.app.ProductAppTest"); 144 } 145 146 @Test testVendorApp()147 public void testVendorApp() throws Exception { 148 runTests("android.test.app.vendor", "android.test.app.VendorAppTest"); 149 } 150 151 @Test testDataApp()152 public void testDataApp() throws Exception { 153 runTests("android.test.app.data", "android.test.app.DataAppTest"); 154 } 155 runTests(String pkgName, String testClassName)156 private void runTests(String pkgName, String testClassName) throws Exception { 157 DeviceContext ctx = new DeviceContext(getTestInformation()); 158 var options = new DeviceTestRunOptions(pkgName) 159 .setTestClassName(testClassName) 160 .addInstrumentationArg("libDirName", ctx.libDirName()); 161 runDeviceTests(options); 162 } 163 164 // Utility class that keeps track of a set of paths the need to be deleted after testing. 165 private static class CleanupPaths { 166 private ITestDevice mDevice; 167 private List<String> mCleanupPaths; 168 CleanupPaths(ITestDevice device)169 CleanupPaths(ITestDevice device) { 170 mDevice = device; 171 mCleanupPaths = new ArrayList<String>(); 172 } 173 CleanupPaths(ITestDevice device, String pathList)174 CleanupPaths(ITestDevice device, String pathList) { 175 mDevice = device; 176 mCleanupPaths = Arrays.asList(pathList.split(":")); 177 } 178 getPathList()179 String getPathList() { return String.join(":", mCleanupPaths); } 180 181 // Adds the given path, or its topmost nonexisting parent directory, to the list of paths to 182 // clean up. addPath(String devicePath)183 void addPath(String devicePath) throws DeviceNotAvailableException { 184 File path = new File(devicePath); 185 while (true) { 186 File parentPath = path.getParentFile(); 187 if (parentPath == null || mDevice.doesFileExist(parentPath.toString())) { 188 break; 189 } 190 path = parentPath; 191 } 192 String nonExistingPath = path.toString(); 193 if (!mCleanupPaths.contains(nonExistingPath)) { 194 mCleanupPaths.add(nonExistingPath); 195 } 196 } 197 cleanup()198 void cleanup() throws DeviceNotAvailableException { 199 // Clean up in reverse order in case several pushed files were in the same nonexisting 200 // directory. 201 for (int i = mCleanupPaths.size() - 1; i >= 0; --i) { 202 mDevice.deleteFile(mCleanupPaths.get(i)); 203 } 204 } 205 } 206 207 // Class for code that needs an ITestDevice. It may be instantiated both in tests and in 208 // (Before|After)ClassWithInfo. 209 private static class DeviceContext implements AutoCloseable { 210 IInvocationContext mContext; 211 ITestDevice mDevice; 212 CompatibilityBuildHelper mBuildHelper; 213 CleanupPaths mCleanup; 214 private String mTestArch; 215 DeviceContext(TestInformation testInfo)216 DeviceContext(TestInformation testInfo) { 217 mContext = testInfo.getContext(); 218 mDevice = testInfo.getDevice(); 219 mBuildHelper = new CompatibilityBuildHelper(testInfo.getBuildInfo()); 220 mCleanup = new CleanupPaths(mDevice); 221 } 222 close()223 public void close() throws DeviceNotAvailableException { mCleanup.cleanup(); } 224 225 // Helper class to both push a library to device and record it in a public.libraries-xxx.txt 226 // file. 227 class PublicLibs { 228 private ZipFile mLibApk; 229 private List<String> mPublicLibs = new ArrayList<String>(); 230 PublicLibs(ZipFile libApk)231 PublicLibs(ZipFile libApk) { 232 mLibApk = libApk; 233 } 234 addLib(String libName, String destDir, String destName)235 void addLib(String libName, String destDir, String destName) throws Exception { 236 pushNativeTestLib(mLibApk, libName, destDir + "/" + destName); 237 mPublicLibs.add(destName); 238 } 239 pushPublicLibrariesFile(String path)240 void pushPublicLibrariesFile(String path) throws DeviceNotAvailableException { 241 pushString(mPublicLibs.stream().collect(Collectors.joining("\n")) + "\n", path); 242 } 243 } 244 pushExtendedPublicSystemOemLibs(ZipFile libApk)245 void pushExtendedPublicSystemOemLibs(ZipFile libApk) throws Exception { 246 var oem1Libs = new PublicLibs(libApk); 247 // Push libsystem_extpub<n>.oem1.so for each test. Since we cannot unload them, we need 248 // a fresh never-before-loaded library in each loadLibrary call. 249 for (int i = 1; i <= 3; ++i) { 250 oem1Libs.addLib("libsystem_testlib.so", "/system/${LIB}", 251 "libsystem_extpub" + i + ".oem1.so"); 252 } 253 oem1Libs.addLib("libsystem_testlib.so", "/system/${LIB}", "libsystem_extpub.oem1.so"); 254 oem1Libs.pushPublicLibrariesFile("/system/etc/public.libraries-oem1.txt"); 255 256 var oem2Libs = new PublicLibs(libApk); 257 oem2Libs.addLib("libsystem_testlib.so", "/system/${LIB}", "libsystem_extpub.oem2.so"); 258 // libextpub_nouses.oem2.so is a library that the test apps don't have 259 // <uses-native-library> dependencies for. 260 oem2Libs.addLib( 261 "libsystem_testlib.so", "/system/${LIB}", "libsystem_extpub_nouses.oem2.so"); 262 oem2Libs.pushPublicLibrariesFile("/system/etc/public.libraries-oem2.txt"); 263 } 264 pushExtendedPublicProductLibs(ZipFile libApk)265 void pushExtendedPublicProductLibs(ZipFile libApk) throws Exception { 266 var product1Libs = new PublicLibs(libApk); 267 // Push libproduct_extpub<n>.product1.so for each test. Since we cannot unload them, we 268 // need a fresh never-before-loaded library in each loadLibrary call. 269 for (int i = 1; i <= 3; ++i) { 270 product1Libs.addLib("libproduct_testlib.so", "/product/${LIB}", 271 "libproduct_extpub" + i + ".product1.so"); 272 } 273 product1Libs.addLib( 274 "libproduct_testlib.so", "/product/${LIB}", "libproduct_extpub.product1.so"); 275 product1Libs.pushPublicLibrariesFile("/product/etc/public.libraries-product1.txt"); 276 } 277 pushPrivateLibs(ZipFile libApk)278 void pushPrivateLibs(ZipFile libApk) throws Exception { 279 // Push the libraries once for each test. Since we cannot unload them, we need a fresh 280 // never-before-loaded library in each loadLibrary call. 281 for (int i = 1; i <= 6; ++i) { 282 pushNativeTestLib(libApk, "libsystem_testlib.so", 283 "/system/${LIB}/libsystem_private" + i + ".so"); 284 pushNativeTestLib(libApk, "libsystem_testlib.so", 285 "/system_ext/${LIB}/libsystemext_private" + i + ".so"); 286 pushNativeTestLib(libApk, "libproduct_testlib.so", 287 "/product/${LIB}/libproduct_private" + i + ".so"); 288 pushNativeTestLib(libApk, "libvendor_testlib.so", 289 "/vendor/${LIB}/libvendor_private" + i + ".so"); 290 } 291 } 292 pushSharedLib(String partitionDir, String packageName, String buildJarName)293 void pushSharedLib(String partitionDir, String packageName, String buildJarName) 294 throws Exception { 295 String path = partitionDir + "/framework/" + packageName + ".jar"; 296 pushFile(buildJarName, path); 297 // This permissions xml file is necessary to make it possible to depend on the shared 298 // library from the test app, even if it's in the same partition. It makes the library 299 // public to apps in other partitions as well, which is more than we need, but that 300 // being the case we test all shared libraries from all apps. 301 pushString("<permissions>\n" 302 + "<library name=\"" + packageName + "\" file=\"" + path + "\" />\n" 303 + "</permissions>\n", 304 partitionDir + "/etc/permissions/" + packageName + ".xml"); 305 } 306 softReboot()307 void softReboot() throws DeviceNotAvailableException { 308 assertCommandSucceeds("setprop dev.bootcomplete 0"); 309 assertCommandSucceeds("stop"); 310 assertCommandSucceeds("start"); 311 mDevice.waitForDeviceAvailable(); 312 } 313 getTestArch()314 String getTestArch() throws DeviceNotAvailableException { 315 if (mTestArch == null) { 316 IAbi abi = mContext.getConfigurationDescriptor().getAbi(); 317 mTestArch = abi != null ? abi.getName() 318 : assertCommandSucceeds("getprop ro.product.cpu.abi"); 319 } 320 return mTestArch; 321 } 322 libDirName()323 String libDirName() throws DeviceNotAvailableException { 324 return getTestArch().contains("64") ? "lib64" : "lib"; 325 } 326 327 // Pushes the given file contents to the device at the given destination path. destPath is 328 // assumed to have no risk of overlapping with existing files, and is deleted in tearDown(), 329 // along with any directory levels that had to be created. pushString(String fileContents, String destPath)330 void pushString(String fileContents, String destPath) throws DeviceNotAvailableException { 331 mCleanup.addPath(destPath); 332 assertThat(mDevice.pushString(fileContents, destPath)).isTrue(); 333 } 334 335 // Like pushString, but pushes a data file included in the host test. pushFile(String fileName, String destPath)336 void pushFile(String fileName, String destPath) throws Exception { 337 mCleanup.addPath(destPath); 338 assertThat(mDevice.pushFile(mBuildHelper.getTestFile(fileName), destPath)).isTrue(); 339 } 340 pushApk(String apkBaseName, String destPath)341 void pushApk(String apkBaseName, String destPath) throws Exception { 342 pushFile(apkBaseName + ".apk", 343 destPath + "/" + apkBaseName + "/" + apkBaseName + ".apk"); 344 } 345 346 // Like pushString, but extracts libnativeloader_testlib.so from the library_container_app 347 // APK and pushes it to destPath. "${LIB}" is replaced with "lib" or "lib64" as appropriate. pushNativeTestLib(ZipFile libApk, String libName, String destPath)348 void pushNativeTestLib(ZipFile libApk, String libName, String destPath) throws Exception { 349 String libApkPath = "lib/" + getTestArch() + "/" + libName; 350 ZipEntry entry = libApk.getEntry(libApkPath); 351 assertWithMessage("Failed to find " + libApkPath + " in library_container_app.apk") 352 .that(entry) 353 .isNotNull(); 354 355 File libraryTempFile; 356 try (InputStream inStream = libApk.getInputStream(entry)) { 357 libraryTempFile = writeStreamToTempFile(libName, inStream); 358 } 359 360 destPath = destPath.replace("${LIB}", libDirName()); 361 362 mCleanup.addPath(destPath); 363 assertThat(mDevice.pushFile(libraryTempFile, destPath)).isTrue(); 364 } 365 installPackage(String apkBaseName)366 void installPackage(String apkBaseName) throws Exception { 367 assertThat(mDevice.installPackage(mBuildHelper.getTestFile(apkBaseName + ".apk"), 368 false /* reinstall */)) 369 .isNull(); 370 } 371 assertCommandSucceeds(String command)372 String assertCommandSucceeds(String command) throws DeviceNotAvailableException { 373 CommandResult result = mDevice.executeShellV2Command(command); 374 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 375 // Remove trailing \n's. 376 return result.getStdout().trim(); 377 } 378 } 379 writeStreamToTempFile(String tempFileBaseName, InputStream inStream)380 static private File writeStreamToTempFile(String tempFileBaseName, InputStream inStream) 381 throws Exception { 382 File hostTempFile = File.createTempFile(tempFileBaseName, null); 383 try (FileOutputStream outStream = new FileOutputStream(hostTempFile)) { 384 ByteStreams.copy(inStream, outStream); 385 } 386 return hostTempFile; 387 } 388 } 389