blob: 90fa26520860cd446b6467998d96a96f534d92e3 [file] [log] [blame]
/*
* 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();
}
}