blob: 2492b48a164df85e41be476f3f469c8031b0fc6f [file] [log] [blame]
/*
* Copyright (C) 2016 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.jni.cts;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import androidx.test.InstrumentationRegistry;
import dalvik.system.PathClassLoader;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class LinkerNamespacesHelper {
private final static String PUBLIC_CONFIG_DIR = "/system/etc/";
private final static String PRODUCT_CONFIG_DIR = "/product/etc/";
private final static String SYSTEM_CONFIG_FILE = PUBLIC_CONFIG_DIR + "public.libraries.txt";
private final static Pattern EXTENSION_CONFIG_FILE_PATTERN = Pattern.compile(
"public\\.libraries-([A-Za-z0-9\\-_.]+)\\.txt");
private final static String VENDOR_CONFIG_FILE = "/vendor/etc/public.libraries.txt";
private final static String[] PUBLIC_SYSTEM_LIBRARIES = {
"libaaudio.so",
"libandroid.so",
"libc.so",
"libcamera2ndk.so",
"libdl.so",
"libEGL.so",
"libGLESv1_CM.so",
"libGLESv2.so",
"libGLESv3.so",
"libicui18n.so",
"libicuuc.so",
"libjnigraphics.so",
"liblog.so",
"libmediandk.so",
"libm.so",
"libnativewindow.so",
"libneuralnetworks.so",
"libOpenMAXAL.so",
"libOpenSLES.so",
"libRS.so",
"libstdc++.so",
"libsync.so",
"libvulkan.so",
"libz.so"
};
// The grey-list.
private final static String[] PRIVATE_SYSTEM_LIBRARIES = {
"libandroid_runtime.so",
"libbinder.so",
"libcrypto.so",
"libcutils.so",
"libexpat.so",
"libgui.so",
"libmedia.so",
"libnativehelper.so",
"libskia.so",
"libssl.so",
"libstagefright.so",
"libsqlite.so",
"libui.so",
"libutils.so",
"libvorbisidec.so",
};
private final static String WEBVIEW_PLAT_SUPPORT_LIB = "libwebviewchromium_plat_support.so";
private static List<String> readPublicLibrariesFile(File file) throws IOException {
List<String> libs = new ArrayList<>();
if (file.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
libs.add(line);
}
}
}
return libs;
}
private static String readExtensionConfigFiles(String configDir, List<String> libs) throws IOException {
File[] configFiles = new File(configDir).listFiles(
new FilenameFilter() {
public boolean accept(File dir, String name) {
return EXTENSION_CONFIG_FILE_PATTERN.matcher(name).matches();
}
});
if (configFiles == null) return null;
for (File configFile: configFiles) {
String fileName = configFile.toPath().getFileName().toString();
Matcher configMatcher = EXTENSION_CONFIG_FILE_PATTERN.matcher(fileName);
if (configMatcher.matches()) {
String companyName = configMatcher.group(1);
// a lib in public.libraries-acme.txt should be
// libFoo.acme.so
List<String> libNames = readPublicLibrariesFile(configFile);
for (String lib : libNames) {
int space = lib.lastIndexOf(' ');
if (space != -1) {
// Drop 64 or 32 from 'libFoo.so 64'
lib = lib.substring(0, space);
}
if (lib.endsWith("." + companyName + ".so")) {
libs.add(lib);
} else {
return "Library \"" + lib + "\" in " + configFile.toString()
+ " must have company name " + companyName + " as suffix.";
}
}
}
}
return null;
}
public static String runAccessibilityTest() throws IOException {
List<String> systemLibs = new ArrayList<>();
Collections.addAll(systemLibs, PUBLIC_SYSTEM_LIBRARIES);
if (InstrumentationRegistry.getContext().getPackageManager().
hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
systemLibs.add(WEBVIEW_PLAT_SUPPORT_LIB);
}
// Check if public.libraries.txt contains libs other than the
// public system libs (NDK libs).
List<String> oemLibs = new ArrayList<>();
String oemLibsError = readExtensionConfigFiles(PUBLIC_CONFIG_DIR, oemLibs);
if (oemLibsError != null) return oemLibsError;
// OEM libs that passed above tests are available to Android app via JNI
systemLibs.addAll(oemLibs);
// PRODUCT libs that passed are also available
List<String> productLibs = new ArrayList<>();
String productLibsError = readExtensionConfigFiles(PRODUCT_CONFIG_DIR, productLibs);
if (productLibsError != null) return productLibsError;
List<String> vendorLibs = readPublicLibrariesFile(new File(VENDOR_CONFIG_FILE));
// Make sure that the libs in grey-list are not exposed to apps. In fact, it
// would be better for us to run this check against all system libraries which
// are not NDK libs, but grey-list libs are enough for now since they have been
// the most popular violators.
Set<String> greyListLibs = new HashSet<>();
Collections.addAll(greyListLibs, PRIVATE_SYSTEM_LIBRARIES);
// Note: check for systemLibs isn't needed since we already checked
// /system/etc/public.libraries.txt against NDK and
// /system/etc/public.libraries-<company>.txt against lib<name>.<company>.so.
for (String lib : vendorLibs) {
if (greyListLibs.contains(lib)) {
return "Internal library \"" + lib + "\" must not be available to apps.";
}
}
return runAccessibilityTestImpl(systemLibs.toArray(new String[systemLibs.size()]),
vendorLibs.toArray(new String[vendorLibs.size()]),
productLibs.toArray(new String[productLibs.size()]));
}
private static native String runAccessibilityTestImpl(String[] publicSystemLibs,
String[] publicVendorLibs,
String[] publicProductLibs);
private static void invokeIncrementGlobal(Class<?> clazz) throws Exception {
clazz.getMethod("incrementGlobal").invoke(null);
}
private static int invokeGetGlobal(Class<?> clazz) throws Exception {
return (Integer)clazz.getMethod("getGlobal").invoke(null);
}
private static ApplicationInfo getApplicationInfo(String packageName) {
PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
try {
return pm.getApplicationInfo(packageName, 0);
} catch (NameNotFoundException nnfe) {
throw new RuntimeException(nnfe);
}
}
private static String getSourcePath(String packageName) {
String sourcePath = getApplicationInfo(packageName).sourceDir;
if (sourcePath == null) {
throw new IllegalStateException("No source path path found for " + packageName);
}
return sourcePath;
}
private static String getNativePath(String packageName) {
String nativePath = getApplicationInfo(packageName).nativeLibraryDir;
if (nativePath == null) {
throw new IllegalStateException("No native path path found for " + packageName);
}
return nativePath;
}
// Verify the behaviour of native library loading in class loaders.
// In this test:
// - libjninamespacea1, libjninamespacea2 and libjninamespaceb depend on libjnicommon
// - loaderA will load ClassNamespaceA1 (loading libjninamespacea1)
// - loaderA will load ClassNamespaceA2 (loading libjninamespacea2)
// - loaderB will load ClassNamespaceB (loading libjninamespaceb)
// - incrementGlobal/getGlobal operate on a static global from libjnicommon
// and each class should get its own view on it.
//
// This is a test case for 2 different scenarios:
// - loading native libraries in different class loaders
// - loading native libraries in the same class loader
// Ideally we would have 2 different tests but JNI doesn't allow loading the same library in
// different class loaders. So to keep the number of native libraries manageable we just
// re-use the same class loaders for the two tests.
public static String runClassLoaderNamespaces() throws Exception {
// Test for different class loaders.
// Verify that common dependencies get a separate copy in each class loader.
// libjnicommon should be loaded twice:
// in the namespace for loaderA and the one for loaderB.
String apkPath = getSourcePath("android.jni.cts");
String nativePath = getNativePath("android.jni.cts");
PathClassLoader loaderA = new PathClassLoader(
apkPath, nativePath, ClassLoader.getSystemClassLoader());
Class<?> testA1Class = loaderA.loadClass("android.jni.cts.ClassNamespaceA1");
PathClassLoader loaderB = new PathClassLoader(
apkPath, nativePath, ClassLoader.getSystemClassLoader());
Class<?> testBClass = loaderB.loadClass("android.jni.cts.ClassNamespaceB");
int globalA1 = invokeGetGlobal(testA1Class);
int globalB = invokeGetGlobal(testBClass);
if (globalA1 != 0 || globalB != 0) {
return "Expected globals to be 0/0: globalA1=" + globalA1 + " globalB=" + globalB;
}
invokeIncrementGlobal(testA1Class);
globalA1 = invokeGetGlobal(testA1Class);
globalB = invokeGetGlobal(testBClass);
if (globalA1 != 1 || globalB != 0) {
return "Expected globals to be 1/0: globalA1=" + globalA1 + " globalB=" + globalB;
}
invokeIncrementGlobal(testBClass);
globalA1 = invokeGetGlobal(testA1Class);
globalB = invokeGetGlobal(testBClass);
if (globalA1 != 1 || globalB != 1) {
return "Expected globals to be 1/1: globalA1=" + globalA1 + " globalB=" + globalB;
}
// Test for the same class loaders.
// Verify that if we load ClassNamespaceA2 into loaderA we get the same view on the
// globals.
Class<?> testA2Class = loaderA.loadClass("android.jni.cts.ClassNamespaceA2");
int globalA2 = invokeGetGlobal(testA2Class);
if (globalA1 != 1 || globalA2 !=1) {
return "Expected globals to be 1/1: globalA1=" + globalA1 + " globalA2=" + globalA2;
}
invokeIncrementGlobal(testA1Class);
globalA1 = invokeGetGlobal(testA1Class);
globalA2 = invokeGetGlobal(testA2Class);
if (globalA1 != 2 || globalA2 != 2) {
return "Expected globals to be 2/2: globalA1=" + globalA1 + " globalA2=" + globalA2;
}
invokeIncrementGlobal(testA2Class);
globalA1 = invokeGetGlobal(testA1Class);
globalA2 = invokeGetGlobal(testA2Class);
if (globalA1 != 3 || globalA2 != 3) {
return "Expected globals to be 2/2: globalA1=" + globalA1 + " globalA2=" + globalA2;
}
// On success we return null.
return null;
}
}
class ClassNamespaceA1 {
static {
System.loadLibrary("jninamespacea1");
}
public static native void incrementGlobal();
public static native int getGlobal();
}
class ClassNamespaceA2 {
static {
System.loadLibrary("jninamespacea2");
}
public static native void incrementGlobal();
public static native int getGlobal();
}
class ClassNamespaceB {
static {
System.loadLibrary("jninamespaceb");
}
public static native void incrementGlobal();
public static native int getGlobal();
}