• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         if (cleanupPathList != null) {
120             CleanupPaths cleanup = new CleanupPaths(ctx.mDevice, cleanupPathList);
121             cleanup.cleanup();
122         }
123     }
124 
125     @Test
testSystemPrivApp()126     public void testSystemPrivApp() throws Exception {
127         // There's currently no difference in the tests between /system/priv-app and /system/app, so
128         // let's reuse the same one.
129         runTests("android.test.app.system_priv", "android.test.app.SystemAppTest");
130     }
131 
132     @Test
testSystemApp()133     public void testSystemApp() throws Exception {
134         runTests("android.test.app.system", "android.test.app.SystemAppTest");
135     }
136 
137     @Test
testSystemExtApp()138     public void testSystemExtApp() throws Exception {
139         // /system_ext should behave the same as /system, so run the same test class there.
140         runTests("android.test.app.system_ext", "android.test.app.SystemAppTest");
141     }
142 
143     @Test
testProductApp()144     public void testProductApp() throws Exception {
145         runTests("android.test.app.product", "android.test.app.ProductAppTest");
146     }
147 
148     @Test
testVendorApp()149     public void testVendorApp() throws Exception {
150         runTests("android.test.app.vendor", "android.test.app.VendorAppTest");
151     }
152 
153     @Test
testDataApp()154     public void testDataApp() throws Exception {
155         runTests("android.test.app.data", "android.test.app.DataAppTest");
156     }
157 
runTests(String pkgName, String testClassName)158     private void runTests(String pkgName, String testClassName) throws Exception {
159         DeviceContext ctx = new DeviceContext(getTestInformation());
160         var options = new DeviceTestRunOptions(pkgName)
161                               .setTestClassName(testClassName)
162                               .addInstrumentationArg("libDirName", ctx.libDirName());
163         runDeviceTests(options);
164     }
165 
166     // Utility class that keeps track of a set of paths the need to be deleted after testing.
167     private static class CleanupPaths {
168         private ITestDevice mDevice;
169         private List<String> mCleanupPaths;
170 
CleanupPaths(ITestDevice device)171         CleanupPaths(ITestDevice device) {
172             mDevice = device;
173             mCleanupPaths = new ArrayList<String>();
174         }
175 
CleanupPaths(ITestDevice device, String pathList)176         CleanupPaths(ITestDevice device, String pathList) {
177             mDevice = device;
178             mCleanupPaths = Arrays.asList(pathList.split(":"));
179         }
180 
getPathList()181         String getPathList() { return String.join(":", mCleanupPaths); }
182 
183         // Adds the given path, or its topmost nonexisting parent directory, to the list of paths to
184         // clean up.
addPath(String devicePath)185         void addPath(String devicePath) throws DeviceNotAvailableException {
186             File path = new File(devicePath);
187             while (true) {
188                 File parentPath = path.getParentFile();
189                 if (parentPath == null || mDevice.doesFileExist(parentPath.toString())) {
190                     break;
191                 }
192                 path = parentPath;
193             }
194             String nonExistingPath = path.toString();
195             if (!mCleanupPaths.contains(nonExistingPath)) {
196                 mCleanupPaths.add(nonExistingPath);
197             }
198         }
199 
cleanup()200         void cleanup() throws DeviceNotAvailableException {
201             // Clean up in reverse order in case several pushed files were in the same nonexisting
202             // directory.
203             for (int i = mCleanupPaths.size() - 1; i >= 0; --i) {
204                 mDevice.deleteFile(mCleanupPaths.get(i));
205             }
206         }
207     }
208 
209     // Class for code that needs an ITestDevice. It may be instantiated both in tests and in
210     // (Before|After)ClassWithInfo.
211     private static class DeviceContext implements AutoCloseable {
212         IInvocationContext mContext;
213         ITestDevice mDevice;
214         CompatibilityBuildHelper mBuildHelper;
215         CleanupPaths mCleanup;
216         private String mTestArch;
217 
DeviceContext(TestInformation testInfo)218         DeviceContext(TestInformation testInfo) {
219             mContext = testInfo.getContext();
220             mDevice = testInfo.getDevice();
221             mBuildHelper = new CompatibilityBuildHelper(testInfo.getBuildInfo());
222             mCleanup = new CleanupPaths(mDevice);
223         }
224 
close()225         public void close() throws DeviceNotAvailableException { mCleanup.cleanup(); }
226 
227         // Helper class to both push a library to device and record it in a public.libraries-xxx.txt
228         // file.
229         class PublicLibs {
230             private ZipFile mLibApk;
231             private List<String> mPublicLibs = new ArrayList<String>();
232 
PublicLibs(ZipFile libApk)233             PublicLibs(ZipFile libApk) {
234                 mLibApk = libApk;
235             }
236 
addLib(String libName, String destDir, String destName)237             void addLib(String libName, String destDir, String destName) throws Exception {
238                 pushNativeTestLib(mLibApk, libName, destDir + "/" + destName);
239                 mPublicLibs.add(destName);
240             }
241 
pushPublicLibrariesFile(String path)242             void pushPublicLibrariesFile(String path) throws DeviceNotAvailableException {
243                 pushString(mPublicLibs.stream().collect(Collectors.joining("\n")) + "\n", path);
244             }
245         }
246 
pushExtendedPublicSystemOemLibs(ZipFile libApk)247         void pushExtendedPublicSystemOemLibs(ZipFile libApk) throws Exception {
248             var oem1Libs = new PublicLibs(libApk);
249             // Push libsystem_extpub<n>.oem1.so for each test. Since we cannot unload them, we need
250             // a fresh never-before-loaded library in each loadLibrary call.
251             for (int i = 1; i <= 3; ++i) {
252                 oem1Libs.addLib("libsystem_testlib.so", "/system/${LIB}",
253                         "libsystem_extpub" + i + ".oem1.so");
254             }
255             oem1Libs.addLib("libsystem_testlib.so", "/system/${LIB}", "libsystem_extpub.oem1.so");
256             oem1Libs.pushPublicLibrariesFile("/system/etc/public.libraries-oem1.txt");
257 
258             var oem2Libs = new PublicLibs(libApk);
259             oem2Libs.addLib("libsystem_testlib.so", "/system/${LIB}", "libsystem_extpub.oem2.so");
260             // libextpub_nouses.oem2.so is a library that the test apps don't have
261             // <uses-native-library> dependencies for.
262             oem2Libs.addLib(
263                     "libsystem_testlib.so", "/system/${LIB}", "libsystem_extpub_nouses.oem2.so");
264             oem2Libs.pushPublicLibrariesFile("/system/etc/public.libraries-oem2.txt");
265         }
266 
pushExtendedPublicProductLibs(ZipFile libApk)267         void pushExtendedPublicProductLibs(ZipFile libApk) throws Exception {
268             var product1Libs = new PublicLibs(libApk);
269             // Push libproduct_extpub<n>.product1.so for each test. Since we cannot unload them, we
270             // need a fresh never-before-loaded library in each loadLibrary call.
271             for (int i = 1; i <= 3; ++i) {
272                 product1Libs.addLib("libproduct_testlib.so", "/product/${LIB}",
273                         "libproduct_extpub" + i + ".product1.so");
274             }
275             product1Libs.addLib(
276                     "libproduct_testlib.so", "/product/${LIB}", "libproduct_extpub.product1.so");
277             product1Libs.pushPublicLibrariesFile("/product/etc/public.libraries-product1.txt");
278         }
279 
pushPrivateLibs(ZipFile libApk)280         void pushPrivateLibs(ZipFile libApk) throws Exception {
281             // Push the libraries once for each test. Since we cannot unload them, we need a fresh
282             // never-before-loaded library in each loadLibrary call.
283             //
284             // Remember to update testPrivateLibsExist in TestUtils.java when
285             // the number of libraries changes.
286             for (int i = 1; i <= 10; ++i) {
287                 pushNativeTestLib(libApk, "libsystem_testlib.so",
288                         "/system/${LIB}/libsystem_private" + i + ".so");
289                 pushNativeTestLib(libApk, "libsystem_testlib.so",
290                         "/system_ext/${LIB}/libsystemext_private" + i + ".so");
291                 pushNativeTestLib(libApk, "libproduct_testlib.so",
292                         "/product/${LIB}/libproduct_private" + i + ".so");
293                 pushNativeTestLib(libApk, "libvendor_testlib.so",
294                         "/vendor/${LIB}/libvendor_private" + i + ".so");
295             }
296         }
297 
pushSharedLib(String partitionDir, String packageName, String buildJarName)298         void pushSharedLib(String partitionDir, String packageName, String buildJarName)
299                 throws Exception {
300             String path = partitionDir + "/framework/" + packageName + ".jar";
301             pushFile(buildJarName, path);
302             // This permissions xml file is necessary to make it possible to depend on the shared
303             // library from the test app, even if it's in the same partition. It makes the library
304             // public to apps in other partitions as well, which is more than we need, but that
305             // being the case we test all shared libraries from all apps.
306             pushString("<permissions>\n"
307                             + "<library name=\"" + packageName + "\" file=\"" + path + "\" />\n"
308                             + "</permissions>\n",
309                     partitionDir + "/etc/permissions/" + packageName + ".xml");
310         }
311 
softReboot()312         void softReboot() throws DeviceNotAvailableException {
313             assertCommandSucceeds("setprop dev.bootcomplete 0");
314             assertCommandSucceeds("stop");
315             assertCommandSucceeds("start");
316             mDevice.waitForDeviceAvailable();
317         }
318 
getTestArch()319         String getTestArch() throws DeviceNotAvailableException {
320             if (mTestArch == null) {
321                 IAbi abi = mContext.getConfigurationDescriptor().getAbi();
322                 mTestArch = abi != null ? abi.getName()
323                                         : assertCommandSucceeds("getprop ro.product.cpu.abi");
324             }
325             return mTestArch;
326         }
327 
libDirName()328         String libDirName() throws DeviceNotAvailableException {
329             return getTestArch().contains("64") ? "lib64" : "lib";
330         }
331 
332         // Pushes the given file contents to the device at the given destination path. destPath is
333         // assumed to have no risk of overlapping with existing files, and is deleted in tearDown(),
334         // along with any directory levels that had to be created.
pushString(String fileContents, String destPath)335         void pushString(String fileContents, String destPath) throws DeviceNotAvailableException {
336             mCleanup.addPath(destPath);
337             assertThat(mDevice.pushString(fileContents, destPath)).isTrue();
338         }
339 
340         // Like pushString, but pushes a data file included in the host test.
pushFile(String fileName, String destPath)341         void pushFile(String fileName, String destPath) throws Exception {
342             mCleanup.addPath(destPath);
343             assertThat(mDevice.pushFile(mBuildHelper.getTestFile(fileName), destPath)).isTrue();
344         }
345 
pushApk(String apkBaseName, String destPath)346         void pushApk(String apkBaseName, String destPath) throws Exception {
347             pushFile(apkBaseName + ".apk",
348                     destPath + "/" + apkBaseName + "/" + apkBaseName + ".apk");
349         }
350 
351         // Like pushString, but extracts libnativeloader_testlib.so from the library_container_app
352         // APK and pushes it to destPath. "${LIB}" is replaced with "lib" or "lib64" as appropriate.
pushNativeTestLib(ZipFile libApk, String libName, String destPath)353         void pushNativeTestLib(ZipFile libApk, String libName, String destPath) throws Exception {
354             String libApkPath = "lib/" + getTestArch() + "/" + libName;
355             ZipEntry entry = libApk.getEntry(libApkPath);
356             assertWithMessage("Failed to find " + libApkPath + " in library_container_app.apk")
357                     .that(entry)
358                     .isNotNull();
359 
360             File libraryTempFile;
361             try (InputStream inStream = libApk.getInputStream(entry)) {
362                 libraryTempFile = writeStreamToTempFile(libName, inStream);
363             }
364             libraryTempFile.setReadOnly();
365 
366             destPath = destPath.replace("${LIB}", libDirName());
367 
368             mCleanup.addPath(destPath);
369             assertThat(mDevice.pushFile(libraryTempFile, destPath)).isTrue();
370         }
371 
installPackage(String apkBaseName)372         void installPackage(String apkBaseName) throws Exception {
373             assertThat(mDevice.installPackage(mBuildHelper.getTestFile(apkBaseName + ".apk"),
374                                false /* reinstall */))
375                     .isNull();
376         }
377 
assertCommandSucceeds(String command)378         String assertCommandSucceeds(String command) throws DeviceNotAvailableException {
379             CommandResult result = mDevice.executeShellV2Command(command);
380             assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0);
381             // Remove trailing \n's.
382             return result.getStdout().trim();
383         }
384     }
385 
writeStreamToTempFile(String tempFileBaseName, InputStream inStream)386     static private File writeStreamToTempFile(String tempFileBaseName, InputStream inStream)
387             throws Exception {
388         File hostTempFile = File.createTempFile(tempFileBaseName, null);
389         try (FileOutputStream outStream = new FileOutputStream(hostTempFile)) {
390             ByteStreams.copy(inStream, outStream);
391         }
392         return hostTempFile;
393     }
394 }
395