| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.appmanifest.cts; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertThat; |
| |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; |
| import com.android.tradefed.targetprep.TargetSetupError; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| |
| import com.android.tradefed.device.IFileEntry; |
| import com.android.tradefed.util.CommandResult; |
| import com.android.tradefed.util.RunUtil; |
| import com.android.tradefed.util.FileUtil; |
| import com.android.tradefed.util.ZipUtil; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import static org.junit.Assert.assertEquals; |
| |
| import java.io.File; |
| import java.io.BufferedWriter; |
| import java.io.BufferedReader; |
| import java.io.FileWriter; |
| import java.io.InputStreamReader; |
| import java.io.IOException; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.Arrays; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| import java.util.zip.ZipFile; |
| |
| /** |
| * Tests about uses-native-library tags that was introduced in Android S. |
| * |
| * The test reads the list of partner-defined public native shared libraries |
| * (see <a href="https://source.android.com/devices/tech/config/namespaces_libraries#adding-additional-native-libraries)"> |
| * Adding additional native libraries</a>) and make sure that those are available to the apps |
| * only when they are explicitly listed on the app manifest using the new tag. The libs not listed |
| * are not available even though they are declared as public. |
| * |
| * This test also make sure that the new behavior is only for the new apps targeting Android S or |
| * higher. Apps targeting Android 11 or lower still has access to all partner-defined public libs |
| * regardless of the use of the tag. |
| */ |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| public class UsesNativeLibraryTestCase extends BaseHostJUnit4Test { |
| // The list of partner-defined public native shared libraries |
| private final Set<String> mPublicLibraries = new HashSet<>(); |
| |
| // The list of public libs that we will make the test app to depend on |
| private final Set<String> mSomePublicLibraries = new HashSet<>(); |
| |
| // Remaining public libraries that shouldn't be available to new apps |
| private final Set<String> mRemainingPublicLibraries = new HashSet<>(); |
| |
| private File mWorkDir; |
| |
| // Name of a fake library that doesn't exist on the device |
| private String mNonExistingLib; |
| |
| // Name of a library that actually exists on the device, but is not part of the public libraries |
| private String mPrivateLib; |
| |
| // Values to enable/disable/reset the compat change gating |
| private enum CompatChangeState { |
| RESET, |
| ENABLE, |
| DISABLE |
| } |
| |
| // The package name of apk from the buildTestApp |
| private static final String TEST_APP_PACKAGE_NAME = "com.android.test.usesnativesharedlibrary"; |
| |
| // The compat change id of the enforce native shared library dependencies |
| private static final long ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES = 142191088; |
| |
| @Before |
| public void setUp() throws Exception { |
| // extract "foo.so" from lines of foo.so -> (so) foo.so |
| Pattern pattern = Pattern.compile("(\\S+)\\s*->\\s*\\((\\S+)\\)\\s*(\\S+)"); |
| Arrays.stream(executeShellCommand("dumpsys package libraries").split("\n")). |
| skip(1) /* for "Libraries:" header */ . |
| map(line -> pattern.matcher(line.trim())). |
| filter(matcher -> matcher.matches() && matcher.group(2).equals("so")). |
| map(matcher -> matcher.group(1)). |
| forEach(mPublicLibraries::add); |
| |
| // Pick first half of the public libraries |
| mPublicLibraries.stream(). |
| limit(mPublicLibraries.size() / 2). |
| forEach(mSomePublicLibraries::add); |
| |
| // ... and remainders |
| mPublicLibraries.stream(). |
| filter(lib -> !mSomePublicLibraries.contains(lib)). |
| forEach(mRemainingPublicLibraries::add); |
| |
| mNonExistingLib = "libnamethatneverexist.so"; |
| assertFalse(mPublicLibraries.contains(mNonExistingLib)); // unlikely! |
| |
| mPrivateLib = "libui.so"; // randomly chosen private lib |
| assertTrue(getDevice().getFileEntry("/system/lib/" + mPrivateLib) != null || |
| getDevice().getFileEntry("/system/lib64/" + mPrivateLib) != null); |
| assertFalse(mPublicLibraries.contains(mPrivateLib)); |
| |
| // The zip file contains all the tools and files for building a test app on demand. Extract |
| // it to the work directory. |
| try (ZipFile packageZip = new ZipFile(getTestInformation().getDependencyFile( |
| "CtsUesNativeLibraryBuildPackage.zip", false))) { |
| mWorkDir = FileUtil.createTempDir("work"); |
| ZipUtil.extractZip(packageZip, mWorkDir); |
| |
| // Make sure executables are executable |
| FileUtil.chmod(getFile("aapt2"), "u+x"); |
| FileUtil.chmod(getFile("merge_zips"), "u+x"); |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @After |
| public void cleanUp() throws Exception { |
| FileUtil.recursiveDelete(mWorkDir); |
| uninstallPackage(TEST_APP_PACKAGE_NAME); |
| setCompatChange(ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES, TEST_APP_PACKAGE_NAME, |
| CompatChangeState.RESET); |
| |
| } |
| |
| private File getFile(String path) { |
| return new File(mWorkDir, path); |
| } |
| |
| private String executeShellCommand(String command) { |
| try { |
| return getDevice().executeShellCommand(command); |
| } catch (DeviceNotAvailableException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private Stream<IFileEntry> getFileEntriesUnder(String path) { |
| try { |
| return getDevice().getFileEntry(path).getChildren(true).stream(); |
| } catch (DeviceNotAvailableException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private Stream<String> findPublicLibraryFilesUnder(String partition) { |
| return getFileEntriesUnder(partition + "/etc"). |
| filter(fe -> { |
| // For vendor partition we only allow public.libraries.txt file. |
| // For other partitions, partner-added libs can be listed in |
| // public.libraries-<companyname>.txt files. |
| if (partition.equals("/vendor")) { |
| return fe.getName().equals("public.libraries.txt"); |
| } else { |
| return fe.getName().startsWith("public.libraries-") && |
| fe.getName().endsWith(".txt"); |
| } |
| }). |
| map(fe -> fe.getFullPath()); |
| } |
| |
| /** |
| * Tests if the native shared library list reported by the package manager is the same as |
| * the public.libraries*.txt files in the partitions. |
| */ |
| @Test |
| public void testPublicLibrariesAreAllRegistered() throws DeviceNotAvailableException { |
| Set<String> libraryNamesFromTxt = |
| Stream.of("/system", "/system_ext", "/product", "/vendor"). |
| flatMap(dir -> findPublicLibraryFilesUnder(dir)). |
| map(file -> executeShellCommand("cat " + file)). |
| flatMap(lines -> Arrays.stream(lines.split("\n"))). |
| filter(line -> { |
| // filter-out empty lines or comment lines that start with # |
| String strip = line.trim(); |
| return !strip.isEmpty() && !strip.startsWith("#"); |
| }). |
| // line format is "name [bitness]". Extract the name part. |
| map(line -> line.trim().split("\\s+")[0]). |
| collect(Collectors.toSet()); |
| |
| assertEquals(mPublicLibraries, libraryNamesFromTxt); |
| } |
| |
| /** |
| * Creates an AndroidManifest.xml file from the template with the given api level and the list |
| * of mandatory and optional native shared libraries |
| */ |
| private File createManifestFileWithUsesNativeLibraryTags(File dir, int apiLevel, |
| String[] requiredLibraries, String[] optionalLibraries) { |
| try (BufferedReader reader = new BufferedReader(new InputStreamReader( |
| getClass().getClassLoader(). |
| getResourceAsStream("AndroidManifest_template.xml")))) { |
| StringBuffer sb = new StringBuffer(); |
| String line = null; |
| while( (line = reader.readLine()) != null) { |
| sb.append(line); |
| } |
| String template = sb.toString(); |
| |
| sb = new StringBuffer(); |
| for(String lib : requiredLibraries) { |
| sb.append(String.format( |
| "<uses-native-library android:name=\"%s\"/>", lib)); |
| } |
| for(String lib : optionalLibraries) { |
| sb.append(String.format( |
| "<uses-native-library android:name=\"%s\" android:required=\"false\"/>", |
| lib)); |
| } |
| |
| String newContent = template.replace("%USES_LIBRARY%", sb.toString()); |
| newContent = newContent.replace("%TARGET_SDK_VERSION%", Integer.toString(apiLevel)); |
| |
| File output = new File(dir, "AndroidManifest.xml"); |
| FileUtil.writeToFile(newContent, output); |
| return output; |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void runCommand(String cmd) { |
| CommandResult result = RunUtil.getDefault().runTimedCmd(100000, cmd.split(" ")); |
| if (result.getExitCode() != 0) { |
| throw new RuntimeException(result.getStderr()); |
| } |
| } |
| |
| private File buildTestApp(int apiLevel, |
| String[] requiredLibraries, |
| String[] optionalLibraries, |
| String[] availableLibraries, |
| String[] unavailableLibraries) throws IOException { |
| File buildRoot = FileUtil.createTempDir("appbuild", mWorkDir); |
| |
| // Create available.txt and unavailable.txt files. They contain the list of native libs |
| // that must be loadable and non-loadable. The Test app will fail if any of the lib in |
| // available.txt is non-loadable, or if any of the lib in unavailable.txt is loadable. |
| File assetDir = new File(buildRoot, "asset"); |
| assetDir.mkdir(); |
| File availableTxtFile = new File(assetDir, "available.txt"); |
| File unavailableTxtFile = new File(assetDir, "unavailable.txt"); |
| FileUtil.writeToFile(String.join("\n", availableLibraries), availableTxtFile, false); |
| FileUtil.writeToFile(String.join("\n", unavailableLibraries), unavailableTxtFile, false); |
| |
| File manifestFile = createManifestFileWithUsesNativeLibraryTags(buildRoot, apiLevel, |
| requiredLibraries, optionalLibraries); |
| |
| File resFile = new File(buildRoot, "package-res.apk"); |
| runCommand(String.format("%s link --manifest %s -I %s -A %s -o %s", |
| getFile("aapt2"), |
| manifestFile, |
| getFile("android.jar"), |
| assetDir, |
| resFile)); |
| |
| // Append the app code to the apk |
| File unsignedApkFile = new File(buildRoot, "unsigned.apk"); |
| runCommand(String.format("%s %s %s %s", |
| getFile("merge_zips"), |
| unsignedApkFile, |
| resFile, |
| getFile("CtsUsesNativeLibraryTestApp.jar"))); |
| |
| File signedApkFile = new File(buildRoot, "signed.apk"); |
| runCommand(String.format("java -Djava.library.path=%s -jar %s %s %s %s %s", |
| mWorkDir, |
| getFile("signapk.jar"), |
| getFile("testkey.x509.pem"), |
| getFile("testkey.pk8"), |
| unsignedApkFile, |
| signedApkFile)); |
| |
| return signedApkFile; |
| } |
| |
| private boolean installTestApp(File testApp) throws Exception { |
| // Explicit uninstallation is required because we might downgrade the target API level |
| // from 31 to 30 |
| uninstallPackage(TEST_APP_PACKAGE_NAME); |
| try { |
| installPackage(testApp.toString()); |
| return true; |
| } catch (TargetSetupError e) { |
| System.out.println(e.getMessage()); |
| return false; |
| } |
| } |
| |
| private void runInstalledTestApp() throws Exception { |
| runDeviceTests(TEST_APP_PACKAGE_NAME, |
| "com.android.test.usesnativesharedlibrary.LoadTest"); |
| } |
| |
| private void setCompatChange(long changeId, String packageName, CompatChangeState state) |
| throws DeviceNotAvailableException { |
| final StringBuilder cmd = new StringBuilder("am compat "); |
| switch (state) { |
| case RESET: |
| cmd.append("reset "); |
| break; |
| |
| case ENABLE: |
| cmd.append("enable "); |
| break; |
| |
| case DISABLE: |
| cmd.append("disable "); |
| break; |
| |
| default: |
| throw new IllegalArgumentException("Invalid compat change state:" + state); |
| } |
| cmd.append(changeId).append(" "); |
| cmd.append(packageName); |
| getDevice().executeShellCommand(cmd.toString()); |
| } |
| |
| private static String[] add(Set<String> s, String...extra) { |
| List<String> ret = new ArrayList<>(); |
| ret.addAll(s); |
| ret.addAll(Arrays.asList(extra)); |
| return ret.toArray(new String[0]); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////// |
| // Tests for when apps depend on non-existing lib |
| /////////////////////////////////////////////////////////////////////////// |
| |
| @Test |
| public void testOldAppDependsOnNonExistingLib() throws Exception { |
| String[] requiredLibs = {mNonExistingLib}; |
| String[] optionalLibs = {}; |
| String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs |
| String[] unavailableLibs = {mNonExistingLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(30, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppDependsOnNonExistingLib() throws Exception { |
| String[] requiredLibs = {mNonExistingLib}; |
| String[] optionalLibs = {}; |
| String[] availableLibs = {}; // new app doesn't have access to unlisted public libs |
| String[] unavailableLibs = add(mPublicLibraries, mNonExistingLib, mPrivateLib); |
| |
| assertFalse(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| |
| // install failed, so can't run the on-device test |
| } |
| |
| @Test |
| public void testNewAppDependsOnNonExistingLib_withCompatEnabled_installFail() |
| throws Exception { |
| setCompatChange(ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES, TEST_APP_PACKAGE_NAME, |
| CompatChangeState.ENABLE); |
| String[] requiredLibs = {mNonExistingLib}; |
| String[] optionalLibs = {}; |
| String[] availableLibs = {}; // new app doesn't have access to unlisted public libs |
| String[] unavailableLibs = add(mPublicLibraries, mNonExistingLib, mPrivateLib); |
| |
| assertFalse(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| } |
| |
| @Test |
| public void testNewAppDependsOnNonExistingLib_withCompatDisabled_installSucceed() |
| throws Exception { |
| setCompatChange(ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES, TEST_APP_PACKAGE_NAME, |
| CompatChangeState.DISABLE); |
| String[] requiredLibs = {mNonExistingLib}; |
| String[] optionalLibs = {}; |
| String[] availableLibs = {}; // new app doesn't have access to unlisted public libs |
| String[] unavailableLibs = add(mPublicLibraries, mNonExistingLib, mPrivateLib); |
| |
| assertTrue(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| } |
| |
| @Test |
| public void testOldAppOptionallyDependsOnNonExistingLib() throws Exception { |
| String[] requiredLibs = {}; |
| String[] optionalLibs = {mNonExistingLib}; |
| String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs |
| String[] unavailableLibs = {mNonExistingLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(30, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppOptionallyDependsOnNonExistingLib() throws Exception { |
| String[] requiredLibs = {}; |
| String[] optionalLibs = {mNonExistingLib}; |
| String[] availableLibs = {}; // new app doesn't have access to unlisted public libs |
| String[] unavailableLibs = {mNonExistingLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////// |
| // Tests for when apps depend on private lib |
| /////////////////////////////////////////////////////////////////////////// |
| |
| @Test |
| public void testOldAppDependsOnPrivateLib() throws Exception { |
| String[] requiredLibs = {mPrivateLib}; |
| String[] optionalLibs = {}; |
| String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs |
| String[] unavailableLibs = {mPrivateLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(30, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppDependsOnPrivateLib() throws Exception { |
| String[] requiredLibs = {mPrivateLib}; |
| String[] optionalLibs = {}; |
| String[] availableLibs = {}; // new app doesn't have access to unlisted public libs |
| String[] unavailableLibs = add(mPublicLibraries, mPrivateLib, mPrivateLib); |
| |
| assertFalse(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| |
| // install failed, so can't run the on-device test |
| } |
| |
| @Test |
| public void testOldAppOptionallyDependsOnPrivateLib() throws Exception { |
| String[] requiredLibs = {}; |
| String[] optionalLibs = {mPrivateLib}; |
| String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs |
| String[] unavailableLibs = {mPrivateLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(30, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppOptionallyDependsOnPrivateLib() throws Exception { |
| String[] requiredLibs = {}; |
| String[] optionalLibs = {mPrivateLib}; |
| String[] availableLibs = {}; // new app doesn't have access to unlisted public libs |
| String[] unavailableLibs = {mPrivateLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////// |
| // Tests for when apps depend on all public libraries |
| /////////////////////////////////////////////////////////////////////////// |
| |
| @Test |
| public void testOldAppDependsOnAllPublicLibraries() throws Exception { |
| String[] requiredLibs = add(mPublicLibraries); |
| String[] optionalLibs = {}; |
| String[] availableLibs = add(mPublicLibraries); // old app still has access to all libs |
| String[] unavailableLibs = {mNonExistingLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(30, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppDependsOnAllPublicLibraries() throws Exception { |
| String[] requiredLibs = add(mPublicLibraries); |
| String[] optionalLibs = {}; |
| String[] availableLibs = add(mPublicLibraries); // new app now has access to all libs |
| String[] unavailableLibs = {mNonExistingLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////// |
| // Tests for when apps depend on some public libraries |
| /////////////////////////////////////////////////////////////////////////// |
| |
| @Test |
| public void testOldAppDependsOnSomePublicLibraries() throws Exception { |
| // select the first half of the public lib |
| String[] requiredLibs = add(mSomePublicLibraries); |
| String[] optionalLibs = {}; |
| String[] availableLibs = add(mPublicLibraries); // old app still has access to all libs |
| String[] unavailableLibs = {mNonExistingLib, mPrivateLib}; |
| |
| assertTrue(installTestApp(buildTestApp(30, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppDependsOnSomePublicLibraries() throws Exception { |
| String[] requiredLibs = add(mSomePublicLibraries); |
| String[] optionalLibs = {}; |
| // new app has access to the listed libs only |
| String[] availableLibs = add(mSomePublicLibraries); |
| // And doesn't have access to the remaining public libs and of course non-existing |
| // and private libs. |
| String[] unavailableLibs = add(mRemainingPublicLibraries, mNonExistingLib, mPrivateLib); |
| |
| assertTrue(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| |
| @Test |
| public void testNewAppOptionallyDependsOnSomePublicLibraries() throws Exception { |
| // select the first half of the public lib |
| String[] requiredLibs = {}; |
| String[] optionalLibs = add(mSomePublicLibraries); |
| // new app has access to the listed libs only |
| String[] availableLibs = add(mSomePublicLibraries); |
| // And doesn't have access to the remaining public libs and of course non-existing |
| // and private libs. |
| String[] unavailableLibs = add(mRemainingPublicLibraries, mNonExistingLib, mPrivateLib); |
| |
| assertTrue(installTestApp(buildTestApp(31, |
| requiredLibs, optionalLibs, availableLibs, unavailableLibs))); |
| runInstalledTestApp(); |
| } |
| } |
| |