Test for hidden api enforcement killswitch
Currently, simply verifies that we can access VMRuntime.THE_ONE, which
doesn't prove anything as there is no blacklisting enabled yet.
Refactor JNI parts of hidden API test into cts-signature-common to it can
be used in this test.
(cherry picked from commit d635f8936bb3baad00c4bf5658e3bc7f420a3421 in
master)
Bug: 64382372
Test: $ cts/tests/signature/runSignatureTests.sh
Change-Id: I8c566136dea2a6b5d41d81981d0f1ef88a0a1a5f
diff --git a/tests/signature/api-check/Android.mk b/tests/signature/api-check/Android.mk
index 4462393..94b59ba 100644
--- a/tests/signature/api-check/Android.mk
+++ b/tests/signature/api-check/Android.mk
@@ -26,7 +26,7 @@
LOCAL_MODULE := cts-api-signature-test
-LOCAL_SDK_VERSION := current
+LOCAL_SDK_VERSION := test_current
LOCAL_STATIC_JAVA_LIBRARIES := \
cts-signature-common \
diff --git a/tests/signature/api-check/hidden-api-killswitch/Android.mk b/tests/signature/api-check/hidden-api-killswitch/Android.mk
new file mode 100644
index 0000000..2e1ee05
--- /dev/null
+++ b/tests/signature/api-check/hidden-api-killswitch/Android.mk
@@ -0,0 +1,30 @@
+# Copyright (C) 2018 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_PACKAGE_NAME := CtsHiddenApiKillswitchTestCases
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_MULTILIB := both
+LOCAL_JNI_SHARED_LIBRARIES := libcts_dexchecker
+LOCAL_NDK_STL_VARIANT := c++_static
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts vts general-tests
+LOCAL_SDK_VERSION := current
+LOCAL_STATIC_JAVA_LIBRARIES := cts-api-signature-test
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/signature/api-check/hidden-api-killswitch/AndroidManifest.xml b/tests/signature/api-check/hidden-api-killswitch/AndroidManifest.xml
new file mode 100644
index 0000000..8699d18
--- /dev/null
+++ b/tests/signature/api-check/hidden-api-killswitch/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2018 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.signature.cts.api.killswitch">
+
+ <application/>
+
+ <instrumentation android:name="repackaged.android.test.InstrumentationTestRunner"
+ android:targetPackage="android.signature.cts.api.killswitch"
+ android:label="Hidden API Killswitch Test"/>
+</manifest>
diff --git a/tests/signature/api-check/hidden-api-killswitch/AndroidTest.xml b/tests/signature/api-check/hidden-api-killswitch/AndroidTest.xml
new file mode 100644
index 0000000..2d99a73
--- /dev/null
+++ b/tests/signature/api-check/hidden-api-killswitch/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<configuration description="Config for CTS Hidden API Signature test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="systems" />
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <!-- Enable the killswitch before running the test, then disable it afterwards. The test
+ is intended to verify the behaviour when the killswitch is enabled. -->
+ <option name="run-command" value="settings put global hidden_api_blacklist_exemptions \*" />
+ <option name="teardown-command" value="settings delete global hidden_api_blacklist_exemptions" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsHiddenApiKillswitchTestCases.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.signature.cts.api.killswitch" />
+ <option name="runner" value="repackaged.android.test.InstrumentationTestRunner" />
+ <option name="class" value="android.signature.cts.api.KillswitchTest" />
+ <option name="runtime-hint" value="30s" />
+ </test>
+</configuration>
diff --git a/tests/signature/api-check/hidden-api/Android.mk b/tests/signature/api-check/hidden-api/Android.mk
index a5bc737..f039032 100644
--- a/tests/signature/api-check/hidden-api/Android.mk
+++ b/tests/signature/api-check/hidden-api/Android.mk
@@ -24,18 +24,8 @@
$(eval $(call copy-one-file,$(INTERNAL_PLATFORM_HIDDENAPI_BLACKLIST),$(LOCAL_BUILT_MODULE)))
include $(CLEAR_VARS)
-LOCAL_MODULE := libcts_hiddenapi
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := hidden-api.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
-LOCAL_SDK_VERSION := current
-LOCAL_NDK_STL_VARIANT := c++_static
-include $(BUILD_SHARED_LIBRARY)
-
-include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := CtsHiddenApiDiscoveryTestCases
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
LOCAL_SIGNATURE_API_FILES := blacklist.api
-LOCAL_JNI_SHARED_LIBRARIES := libcts_hiddenapi
+LOCAL_JNI_SHARED_LIBRARIES := libcts_dexchecker
include $(LOCAL_PATH)/../build_signature_apk.mk
diff --git a/tests/signature/api-check/hidden-api/hidden-api.cpp b/tests/signature/api-check/hidden-api/hidden-api.cpp
deleted file mode 100644
index fd2a94d..0000000
--- a/tests/signature/api-check/hidden-api/hidden-api.cpp
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "jni.h"
-
-class ScopedUtfChars {
- public:
- ScopedUtfChars(JNIEnv* env, jstring s) : env_(env), string_(s) {
- if (s == NULL) {
- utf_chars_ = NULL;
- } else {
- utf_chars_ = env->GetStringUTFChars(s, NULL);
- }
- }
-
- ~ScopedUtfChars() {
- if (utf_chars_) {
- env_->ReleaseStringUTFChars(string_, utf_chars_);
- }
- }
-
- const char* c_str() const {
- return utf_chars_;
- }
-
- private:
- JNIEnv* env_;
- jstring string_;
- const char* utf_chars_;
-};
-
-extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getField_1JNI(
- JNIEnv* env, jclass, jclass klass, jstring name, jstring type) {
- ScopedUtfChars utf_name(env, name);
- ScopedUtfChars utf_type(env, type);
- // Attempt to access the given instance field. It will succeed if it exists,
- // and throw NoSuchFieldError if not.
- env->GetFieldID(klass, utf_name.c_str(), utf_type.c_str());
-}
-
-extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getStaticField_1JNI(
- JNIEnv* env, jclass, jclass klass, jstring name, jstring type) {
- ScopedUtfChars utf_name(env, name);
- ScopedUtfChars utf_type(env, type);
- // Attempt to access the given static field. It will succeed if it exists,
- // and throw NoSuchFieldError if not.
- env->GetStaticFieldID(klass, utf_name.c_str(), utf_type.c_str());
-}
-
-extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getMethod_1JNI(
- JNIEnv* env, jclass, jclass klass, jstring name, jstring signature) {
- ScopedUtfChars utf_name(env, name);
- ScopedUtfChars utf_signature(env, signature);
- // Attempt to access the given instance method. It will succeed if it exists,
- // and throw NoSuchMethodError if not.
- env->GetMethodID(klass, utf_name.c_str(), utf_signature.c_str());
-}
-
-extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getStaticMethod_1JNI(
- JNIEnv* env, jclass, jclass klass, jstring name, jstring signature) {
- ScopedUtfChars utf_name(env, name);
- ScopedUtfChars utf_signature(env, signature);
- // Attempt to access the given static method. It will succeed if it exists,
- // and throw NoSuchMethodError if not.
- env->GetStaticMethodID(klass, utf_name.c_str(), utf_signature.c_str());
-}
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java
index e1da5ac..9673fd2 100644
--- a/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java
@@ -21,13 +21,17 @@
import android.signature.cts.DexApiDocumentParser.DexField;
import android.signature.cts.DexApiDocumentParser.DexMember;
import android.signature.cts.DexApiDocumentParser.DexMethod;
+import android.signature.cts.DexMemberChecker;
import android.signature.cts.FailureType;
+import android.signature.cts.ResultObserver;
+
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.List;
+import java.util.Set;
import java.util.stream.Stream;
import java.text.ParseException;
@@ -52,136 +56,59 @@
* Will check the entire API, and then report the complete list of failures
*/
public void testSignature() {
- System.loadLibrary("cts_hiddenapi");
+ DexMemberChecker.init();
runWithTestResultObserver(resultObserver -> {
+ DexMemberChecker.Observer observer = new DexMemberChecker.Observer() {
+ @Override
+ public void classAccessible(boolean accessible, DexMember member) {
+ }
+
+ @Override
+ public void fieldAccessibleViaReflection(boolean accessible, DexField field) {
+ if (accessible) {
+ resultObserver.notifyFailure(
+ FailureType.EXTRA_FIELD,
+ field.toString(),
+ "Hidden field accessible through reflection");
+ }
+ }
+
+ @Override
+ public void fieldAccessibleViaJni(boolean accessible, DexField field) {
+ if (accessible) {
+ resultObserver.notifyFailure(
+ FailureType.EXTRA_FIELD,
+ field.toString(),
+ "Hidden field accessible through JNI");
+ }
+ }
+
+ @Override
+ public void methodAccessibleViaReflection(boolean accessible, DexMethod method) {
+ if (accessible) {
+ resultObserver.notifyFailure(
+ FailureType.EXTRA_METHOD,
+ method.toString(),
+ "Hidden method accessible through reflection");
+ }
+ }
+
+ @Override
+ public void methodAccessibleViaJni(boolean accessible, DexMethod method) {
+ if (accessible) {
+ resultObserver.notifyFailure(
+ FailureType.EXTRA_METHOD,
+ method.toString(),
+ "Hidden method accessible through JNI");
+ }
+ }
+ };
parseDexApiFilesAsStream(hiddenApiFiles).forEach(dexMember -> {
- checkSingleMember(dexMember, resultObserver);
+ DexMemberChecker.checkSingleMember(dexMember, observer);
});
});
}
- /**
- * Check that a DexMember cannot be discovered with reflection or JNI, and
- * record results in the result
- */
- private void checkSingleMember(DexMember dexMember, TestResultObserver resultObserver) {
- Class<?> klass = findClass(dexMember);
- if (klass == null) {
- // Class not found. Therefore its members are not visible.
- return;
- }
-
- if (dexMember instanceof DexField) {
- if (hasMatchingField_Reflection(klass, (DexField) dexMember)) {
- resultObserver.notifyFailure(
- FailureType.EXTRA_FIELD,
- dexMember.toString(),
- "Hidden field accessible through reflection");
- }
- if (hasMatchingField_JNI(klass, (DexField) dexMember)) {
- resultObserver.notifyFailure(
- FailureType.EXTRA_FIELD,
- dexMember.toString(),
- "Hidden field accessible through JNI");
- }
- } else if (dexMember instanceof DexMethod) {
- if (hasMatchingMethod_Reflection(klass, (DexMethod) dexMember)) {
- resultObserver.notifyFailure(
- FailureType.EXTRA_METHOD,
- dexMember.toString(),
- "Hidden method accessible through reflection");
- }
- if (hasMatchingMethod_JNI(klass, (DexMethod) dexMember)) {
- resultObserver.notifyFailure(
- FailureType.EXTRA_METHOD,
- dexMember.toString(),
- "Hidden method accessible through JNI");
- }
- } else {
- throw new IllegalStateException("Unexpected type of dex member");
- }
- }
-
- private boolean typesMatch(Class<?>[] classes, List<String> typeNames) {
- if (classes.length != typeNames.size()) {
- return false;
- }
- for (int i = 0; i < classes.length; ++i) {
- if (!classes[i].getTypeName().equals(typeNames.get(i))) {
- return false;
- }
- }
- return true;
- }
-
- private Class<?> findClass(DexMember dexMember) {
- Class<?> klass = null;
- try {
- return Class.forName(dexMember.getJavaClassName());
- } catch (ClassNotFoundException ex) {
- return null;
- }
- }
-
- private static boolean hasMatchingField_Reflection(Class<?> klass, DexField dexField) {
- try {
- klass.getDeclaredField(dexField.getName());
- return true;
- } catch (NoSuchFieldException ex) {
- return false;
- }
- }
-
- private static boolean hasMatchingField_JNI(Class<?> klass, DexField dexField) {
- try {
- getField_JNI(klass, dexField.getName(), dexField.getDexType());
- return true;
- } catch (NoSuchFieldError ex) {
- }
- try {
- getStaticField_JNI(klass, dexField.getName(), dexField.getDexType());
- return true;
- } catch (NoSuchFieldError ex) {
- }
- return false;
- }
-
- private boolean hasMatchingMethod_Reflection(Class<?> klass, DexMethod dexMethod) {
- List<String> methodParams = dexMethod.getJavaParameterTypes();
-
- if (dexMethod.isConstructor()) {
- for (Constructor constructor : klass.getDeclaredConstructors()) {
- if (typesMatch(constructor.getParameterTypes(), methodParams)) {
- return true;
- }
- }
- } else {
- for (Method method : klass.getDeclaredMethods()) {
- if (method.getName().equals(dexMethod.getName())
- && typesMatch(method.getParameterTypes(), methodParams)) {
- return true;
- }
- }
- }
- return false;
- }
-
- private static boolean hasMatchingMethod_JNI(Class<?> klass, DexMethod dexMethod) {
- try {
- getMethod_JNI(klass, dexMethod.getName(), dexMethod.getDexSignature());
- return true;
- } catch (NoSuchMethodError ex) {
- }
- if (!dexMethod.isConstructor()) {
- try {
- getStaticMethod_JNI(klass, dexMethod.getName(), dexMethod.getDexSignature());
- return true;
- } catch (NoSuchMethodError ex) {
- }
- }
- return false;
- }
-
private static Stream<DexMember> parseDexApiFilesAsStream(String[] apiFiles) {
DexApiDocumentParser dexApiDocumentParser = new DexApiDocumentParser();
return Stream.of(apiFiles)
@@ -190,9 +117,4 @@
.flatMap(stream -> dexApiDocumentParser.parseAsStream(stream));
}
- private static native boolean getField_JNI(Class<?> klass, String name, String type);
- private static native boolean getStaticField_JNI(Class<?> klass, String name, String type);
- private static native boolean getMethod_JNI(Class<?> klass, String name, String signature);
- private static native boolean getStaticMethod_JNI(Class<?> klass, String name,
- String signature);
}
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/KillswitchTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/KillswitchTest.java
new file mode 100644
index 0000000..37ff746
--- /dev/null
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/KillswitchTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 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.signature.cts.api;
+
+import android.provider.Settings;
+import android.signature.cts.DexApiDocumentParser;
+import android.signature.cts.DexMemberChecker;
+import android.signature.cts.FailureType;
+
+public class KillswitchTest extends AbstractApiTest {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ DexMemberChecker.init();
+ // precondition check: make sure global setting has been configured properly.
+ // This should be done via an adb command, configured in AndroidTest.xml.
+ assertEquals("Global setting " + Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS,
+ "*",
+ Settings.Global.getString(
+ getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS));
+ }
+
+ private static final String ERROR_MESSAGE_APPENDIX =
+ " when global setting hidden_api_blacklist_exemptions is set to *";
+
+ public void testKillswitch() {
+ // for now, just verify that we can access something *not* on the light grey or white list
+ DexApiDocumentParser.DexMember member = new DexApiDocumentParser.DexField(
+ "Ldalvik/system/VMRuntime;", "THE_ONE", "Ldalvik/system/VMRuntime;");
+ runWithTestResultObserver(resultObserver ->
+ DexMemberChecker.checkSingleMember(member, new DexMemberChecker.Observer() {
+ @Override
+ public void classAccessible(boolean accessible,
+ DexApiDocumentParser.DexMember member) {
+ if (!accessible) {
+ resultObserver.notifyFailure(
+ FailureType.MISSING_CLASS,
+ member.toString(),
+ "Class from boot classpath is not accessible"
+ + ERROR_MESSAGE_APPENDIX);
+ }
+ }
+
+ @Override
+ public void fieldAccessibleViaReflection(boolean accessible,
+ DexApiDocumentParser.DexField field) {
+ if (!accessible) {
+ resultObserver.notifyFailure(
+ FailureType.MISSING_FIELD,
+ field.toString(),
+ "Field from boot classpath is not accessible via reflection"
+ + ERROR_MESSAGE_APPENDIX);
+ }
+ }
+
+ @Override
+ public void fieldAccessibleViaJni(boolean accessible,
+ DexApiDocumentParser.DexField field) {
+ if (!accessible) {
+ resultObserver.notifyFailure(
+ FailureType.MISSING_FIELD,
+ field.toString(),
+ "Field from boot classpath is not accessible via JNI"
+ + ERROR_MESSAGE_APPENDIX);
+ }
+ }
+
+ @Override
+ public void methodAccessibleViaReflection(boolean accessible,
+ DexApiDocumentParser.DexMethod method) {
+ if (!accessible) {
+ resultObserver.notifyFailure(
+ FailureType.MISSING_METHOD,
+ method.toString(),
+ "Method from boot classpath is not accessible via reflection"
+ + ERROR_MESSAGE_APPENDIX);
+ }
+ }
+
+ @Override
+ public void methodAccessibleViaJni(boolean accessible,
+ DexApiDocumentParser.DexMethod method) {
+ if (!accessible) {
+ resultObserver.notifyFailure(
+ FailureType.MISSING_METHOD,
+ method.toString(),
+ "Method from boot classpath is not accessible via JNI"
+ + ERROR_MESSAGE_APPENDIX);
+ }
+ }
+
+ }));
+ }
+}