| /* |
| * Copyright (C) 2017 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. |
| */ |
| |
| import dalvik.system.VMRuntime; |
| import java.lang.invoke.MethodHandles; |
| import java.lang.invoke.MethodType; |
| import java.util.function.Consumer; |
| |
| public class ChildClass { |
| enum PrimitiveType { |
| TInteger('I', Integer.TYPE, Integer.valueOf(0)), |
| TLong('J', Long.TYPE, Long.valueOf(0)), |
| TFloat('F', Float.TYPE, Float.valueOf(0)), |
| TDouble('D', Double.TYPE, Double.valueOf(0)), |
| TBoolean('Z', Boolean.TYPE, Boolean.valueOf(false)), |
| TByte('B', Byte.TYPE, Byte.valueOf((byte) 0)), |
| TShort('S', Short.TYPE, Short.valueOf((short) 0)), |
| TCharacter('C', Character.TYPE, Character.valueOf('0')); |
| |
| PrimitiveType(char shorty, Class klass, Object value) { |
| mShorty = shorty; |
| mClass = klass; |
| mDefaultValue = value; |
| } |
| |
| public char mShorty; |
| public Class mClass; |
| public Object mDefaultValue; |
| } |
| |
| enum Hiddenness { |
| Sdk(PrimitiveType.TShort), |
| Unsupported(PrimitiveType.TBoolean), |
| ConditionallyBlocked(PrimitiveType.TByte), |
| Blocklist(PrimitiveType.TCharacter), |
| BlocklistAndCorePlatformApi(PrimitiveType.TInteger); |
| |
| Hiddenness(PrimitiveType type) { mAssociatedType = type; } |
| public PrimitiveType mAssociatedType; |
| } |
| |
| enum Visibility { |
| Public(PrimitiveType.TInteger), |
| Package(PrimitiveType.TFloat), |
| Protected(PrimitiveType.TLong), |
| Private(PrimitiveType.TDouble); |
| |
| Visibility(PrimitiveType type) { mAssociatedType = type; } |
| public PrimitiveType mAssociatedType; |
| } |
| |
| enum Behaviour { |
| Granted, |
| Warning, |
| Denied, |
| } |
| |
| // This needs to be kept in sync with DexDomain in Main. |
| enum DexDomain { |
| CorePlatform, |
| Platform, |
| Application |
| } |
| |
| private static final boolean booleanValues[] = new boolean[] { false, true }; |
| |
| public static void runTest(String libFileName, int parentDomainOrdinal, |
| int childDomainOrdinal, boolean everythingSdked) throws Exception { |
| System.load(libFileName); |
| |
| parentDomain = DexDomain.values()[parentDomainOrdinal]; |
| childDomain = DexDomain.values()[childDomainOrdinal]; |
| |
| configMessage = "parentDomain=" + parentDomain.name() + ", childDomain=" + childDomain.name() |
| + ", everythingSdked=" + everythingSdked; |
| |
| // Check expectations about loading into boot class path. |
| boolean isParentInBoot = (ParentClass.class.getClassLoader().getParent() == null); |
| boolean expectedParentInBoot = (parentDomain != DexDomain.Application); |
| if (isParentInBoot != expectedParentInBoot) { |
| throw new RuntimeException("Expected ParentClass " + |
| (expectedParentInBoot ? "" : "not ") + "in boot class path"); |
| } |
| boolean isChildInBoot = (ChildClass.class.getClassLoader().getParent() == null); |
| boolean expectedChildInBoot = (childDomain != DexDomain.Application); |
| if (isChildInBoot != expectedChildInBoot) { |
| throw new RuntimeException("Expected ChildClass " + (expectedChildInBoot ? "" : "not ") + |
| "in boot class path"); |
| } |
| ChildClass.everythingSdked = everythingSdked; |
| |
| boolean isSameBoot = (isParentInBoot == isChildInBoot); |
| |
| // For compat reasons, meta-reflection should still be usable by apps if hidden api check |
| // hardening is disabled (i.e. target SDK is Q or earlier). The only configuration where this |
| // workaround used to work is for ChildClass in the Application domain and ParentClass in the |
| // Platform domain, so only test that configuration with hidden api check hardening disabled. |
| boolean testHiddenApiCheckHardeningDisabled = |
| (childDomain == DexDomain.Application) && (parentDomain == DexDomain.Platform); |
| |
| // Run meaningful combinations of access flags. |
| for (Hiddenness hiddenness : Hiddenness.values()) { |
| final Behaviour expected; |
| final boolean invokesMemberCallback; |
| // Warnings are now disabled whenever access is granted, even for |
| // greylisted APIs. This is the behaviour for release builds. |
| if (everythingSdked || hiddenness == Hiddenness.Sdk) { |
| expected = Behaviour.Granted; |
| invokesMemberCallback = false; |
| } else if (parentDomain == DexDomain.CorePlatform && childDomain == DexDomain.Platform) { |
| expected = (hiddenness == Hiddenness.BlocklistAndCorePlatformApi) |
| ? Behaviour.Granted : Behaviour.Denied; |
| invokesMemberCallback = false; |
| } else if (isSameBoot) { |
| expected = Behaviour.Granted; |
| invokesMemberCallback = false; |
| } else if (hiddenness == Hiddenness.Blocklist || |
| hiddenness == Hiddenness.BlocklistAndCorePlatformApi) { |
| expected = Behaviour.Denied; |
| invokesMemberCallback = true; |
| } else { |
| expected = Behaviour.Warning; |
| invokesMemberCallback = true; |
| } |
| |
| for (boolean isStatic : booleanValues) { |
| String suffix = (isStatic ? "Static" : "") + hiddenness.name(); |
| |
| for (Visibility visibility : Visibility.values()) { |
| // Test reflection and JNI on methods and fields |
| for (Class klass : new Class<?>[] { ParentClass.class, ParentInterface.class }) { |
| String baseName = visibility.name() + suffix; |
| checkField(klass, "field" + baseName, isStatic, visibility, expected, |
| invokesMemberCallback, testHiddenApiCheckHardeningDisabled); |
| checkMethod(klass, "method" + baseName, isStatic, visibility, expected, |
| invokesMemberCallback, testHiddenApiCheckHardeningDisabled); |
| } |
| |
| // Check whether one can use a class constructor. |
| checkConstructor(ParentClass.class, visibility, hiddenness, expected, |
| testHiddenApiCheckHardeningDisabled); |
| |
| // Check whether one can use an interface default method. |
| String name = "method" + visibility.name() + "Default" + hiddenness.name(); |
| checkMethod(ParentInterface.class, name, /*isStatic*/ false, visibility, expected, |
| invokesMemberCallback, testHiddenApiCheckHardeningDisabled); |
| } |
| |
| // Test whether static linking succeeds. |
| checkLinking("LinkFieldGet" + suffix, /*takesParameter*/ false, expected); |
| checkLinking("LinkFieldSet" + suffix, /*takesParameter*/ true, expected); |
| checkLinking("LinkMethod" + suffix, /*takesParameter*/ false, expected); |
| checkLinking("LinkMethodInterface" + suffix, /*takesParameter*/ false, expected); |
| } |
| |
| // Check whether Class.newInstance succeeds. |
| checkNullaryConstructor(Class.forName("NullaryConstructor" + hiddenness.name()), expected); |
| } |
| } |
| |
| static final class RecordingConsumer implements Consumer<String> { |
| public String recordedValue = null; |
| |
| @Override |
| public void accept(String value) { |
| recordedValue = value; |
| } |
| } |
| |
| private static void checkMemberCallback(Class<?> klass, String name, |
| boolean isPublic, boolean isField, boolean expectedCallback) { |
| try { |
| RecordingConsumer consumer = new RecordingConsumer(); |
| VMRuntime.setNonSdkApiUsageConsumer(consumer); |
| try { |
| if (isPublic) { |
| if (isField) { |
| klass.getField(name); |
| } else { |
| klass.getMethod(name); |
| } |
| } else { |
| if (isField) { |
| klass.getDeclaredField(name); |
| } else { |
| klass.getDeclaredMethod(name); |
| } |
| } |
| } catch (NoSuchFieldException|NoSuchMethodException ignored) { |
| // We're not concerned whether an exception is thrown or not - we're |
| // only interested in whether the callback is invoked. |
| } |
| |
| boolean actualCallback = consumer.recordedValue != null && |
| consumer.recordedValue.contains(name); |
| if (expectedCallback != actualCallback) { |
| if (expectedCallback) { |
| throw new RuntimeException("Expected callback for member: " + name); |
| } else { |
| throw new RuntimeException("Did not expect callback for member: " + name); |
| } |
| } |
| } finally { |
| VMRuntime.setNonSdkApiUsageConsumer(null); |
| } |
| } |
| |
| private static void checkField(Class<?> klass, String name, boolean isStatic, |
| Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback, |
| boolean testHiddenApiCheckHardeningDisabled) throws Exception { |
| |
| boolean isPublic = (visibility == Visibility.Public); |
| boolean canDiscover = (behaviour != Behaviour.Denied); |
| |
| if (klass.isInterface() && (!isStatic || !isPublic)) { |
| // Interfaces only have public static fields. |
| return; |
| } |
| |
| // Test discovery with reflection. |
| |
| if (Reflection.canDiscoverWithGetDeclaredField(klass, name) != canDiscover) { |
| throwDiscoveryException(klass, name, true, "getDeclaredField()", canDiscover); |
| } |
| |
| if (Reflection.canDiscoverWithGetDeclaredFields(klass, name) != canDiscover) { |
| throwDiscoveryException(klass, name, true, "getDeclaredFields()", canDiscover); |
| } |
| |
| if (Reflection.canDiscoverWithGetField(klass, name) != (canDiscover && isPublic)) { |
| throwDiscoveryException(klass, name, true, "getField()", (canDiscover && isPublic)); |
| } |
| |
| if (Reflection.canDiscoverWithGetFields(klass, name) != (canDiscover && isPublic)) { |
| throwDiscoveryException(klass, name, true, "getFields()", (canDiscover && isPublic)); |
| } |
| |
| // Test discovery with JNI. |
| |
| if (JNI.canDiscoverField(klass, name, isStatic) != canDiscover) { |
| throwDiscoveryException(klass, name, true, "JNI", canDiscover); |
| } |
| |
| // Test discovery with MethodHandles.lookup() which is caller |
| // context sensitive. |
| |
| final MethodHandles.Lookup lookup = MethodHandles.lookup(); |
| if (JLI.canDiscoverWithLookupFindGetter(lookup, klass, name, int.class) |
| != canDiscover) { |
| throwDiscoveryException(klass, name, true, "MethodHandles.lookup().findGetter()", |
| canDiscover); |
| } |
| if (JLI.canDiscoverWithLookupFindStaticGetter(lookup, klass, name, int.class) |
| != canDiscover) { |
| throwDiscoveryException(klass, name, true, "MethodHandles.lookup().findStaticGetter()", |
| canDiscover); |
| } |
| |
| // Test discovery with MethodHandles.publicLookup() which can only |
| // see public fields. Looking up setters here and fields in |
| // interfaces are implicitly final. |
| |
| final MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); |
| if (JLI.canDiscoverWithLookupFindSetter(publicLookup, klass, name, int.class) |
| != canDiscover) { |
| throwDiscoveryException(klass, name, true, "MethodHandles.publicLookup().findSetter()", |
| canDiscover); |
| } |
| if (JLI.canDiscoverWithLookupFindStaticSetter(publicLookup, klass, name, int.class) |
| != canDiscover) { |
| throwDiscoveryException(klass, name, true, "MethodHandles.publicLookup().findStaticSetter()", |
| canDiscover); |
| } |
| |
| // Check for meta reflection. |
| |
| // With hidden api check hardening enabled, only white and light greylisted fields should be |
| // discoverable. |
| if (Reflection.canDiscoverFieldWithMetaReflection(klass, name, true) != canDiscover) { |
| throwDiscoveryException(klass, name, false, |
| "Meta reflection with hidden api hardening enabled", canDiscover); |
| } |
| |
| if (testHiddenApiCheckHardeningDisabled) { |
| // With hidden api check hardening disabled, all fields should be discoverable. |
| if (Reflection.canDiscoverFieldWithMetaReflection(klass, name, false) != true) { |
| throwDiscoveryException(klass, name, false, |
| "Meta reflection with hidden api hardening enabled", canDiscover); |
| } |
| } |
| |
| if (canDiscover) { |
| // Test that modifiers are unaffected. |
| |
| if (Reflection.canObserveFieldHiddenAccessFlags(klass, name)) { |
| throwModifiersException(klass, name, true); |
| } |
| |
| // Test getters and setters when meaningful. |
| |
| if (!Reflection.canGetField(klass, name)) { |
| throwAccessException(klass, name, true, "Field.getInt()"); |
| } |
| if (!Reflection.canSetField(klass, name)) { |
| throwAccessException(klass, name, true, "Field.setInt()"); |
| } |
| if (!JNI.canGetField(klass, name, isStatic)) { |
| throwAccessException(klass, name, true, "getIntField"); |
| } |
| if (!JNI.canSetField(klass, name, isStatic)) { |
| throwAccessException(klass, name, true, "setIntField"); |
| } |
| } |
| |
| // Test that callbacks are invoked correctly. |
| checkMemberCallback(klass, name, isPublic, true /* isField */, invokesMemberCallback); |
| } |
| |
| private static void checkMethod(Class<?> klass, String name, boolean isStatic, |
| Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback, |
| boolean testHiddenApiCheckHardeningDisabled) throws Exception { |
| |
| boolean isPublic = (visibility == Visibility.Public); |
| if (klass.isInterface() && !isPublic) { |
| // All interface members are public. |
| return; |
| } |
| |
| boolean canDiscover = (behaviour != Behaviour.Denied); |
| |
| // Test discovery with reflection. |
| |
| if (Reflection.canDiscoverWithGetDeclaredMethod(klass, name) != canDiscover) { |
| throwDiscoveryException(klass, name, false, "getDeclaredMethod()", canDiscover); |
| } |
| |
| if (Reflection.canDiscoverWithGetDeclaredMethods(klass, name) != canDiscover) { |
| throwDiscoveryException(klass, name, false, "getDeclaredMethods()", canDiscover); |
| } |
| |
| if (Reflection.canDiscoverWithGetMethod(klass, name) != (canDiscover && isPublic)) { |
| throwDiscoveryException(klass, name, false, "getMethod()", (canDiscover && isPublic)); |
| } |
| |
| if (Reflection.canDiscoverWithGetMethods(klass, name) != (canDiscover && isPublic)) { |
| throwDiscoveryException(klass, name, false, "getMethods()", (canDiscover && isPublic)); |
| } |
| |
| // Test discovery with JNI. |
| |
| if (JNI.canDiscoverMethod(klass, name, isStatic) != canDiscover) { |
| throwDiscoveryException(klass, name, false, "JNI", canDiscover); |
| } |
| |
| // Test discovery with MethodHandles.lookup(). |
| |
| final MethodHandles.Lookup lookup = MethodHandles.lookup(); |
| final MethodType methodType = MethodType.methodType(int.class); |
| if (JLI.canDiscoverWithLookupFindVirtual(lookup, klass, name, methodType) != canDiscover) { |
| throwDiscoveryException(klass, name, false, "MethodHandles.lookup().findVirtual()", |
| canDiscover); |
| } |
| |
| if (JLI.canDiscoverWithLookupFindStatic(lookup, klass, name, methodType) != canDiscover) { |
| throwDiscoveryException(klass, name, false, "MethodHandles.lookup().findStatic()", |
| canDiscover); |
| } |
| |
| // Check for meta reflection. |
| |
| // With hidden api check hardening enabled, only white and light greylisted methods should be |
| // discoverable. |
| if (Reflection.canDiscoverMethodWithMetaReflection(klass, name, true) != canDiscover) { |
| throwDiscoveryException(klass, name, false, |
| "Meta reflection with hidden api hardening enabled", canDiscover); |
| } |
| |
| if (testHiddenApiCheckHardeningDisabled) { |
| // With hidden api check hardening disabled, all methods should be discoverable. |
| if (Reflection.canDiscoverMethodWithMetaReflection(klass, name, false) != true) { |
| throwDiscoveryException(klass, name, false, |
| "Meta reflection with hidden api hardening enabled", canDiscover); |
| } |
| } |
| |
| // Finish here if we could not discover the method. |
| |
| if (canDiscover) { |
| // Test that modifiers are unaffected. |
| |
| if (Reflection.canObserveMethodHiddenAccessFlags(klass, name)) { |
| throwModifiersException(klass, name, false); |
| } |
| |
| // Test whether we can invoke the method. This skips non-static interface methods. |
| if (!klass.isInterface() || isStatic) { |
| if (!Reflection.canInvokeMethod(klass, name)) { |
| throwAccessException(klass, name, false, "invoke()"); |
| } |
| if (!JNI.canInvokeMethodA(klass, name, isStatic)) { |
| throwAccessException(klass, name, false, "CallMethodA"); |
| } |
| if (!JNI.canInvokeMethodV(klass, name, isStatic)) { |
| throwAccessException(klass, name, false, "CallMethodV"); |
| } |
| } |
| } |
| |
| // Test that callbacks are invoked correctly. |
| checkMemberCallback(klass, name, isPublic, false /* isField */, invokesMemberCallback); |
| } |
| |
| private static void checkConstructor(Class<?> klass, Visibility visibility, Hiddenness hiddenness, |
| Behaviour behaviour, boolean testHiddenApiCheckHardeningDisabled) throws Exception { |
| |
| boolean isPublic = (visibility == Visibility.Public); |
| String signature = "(" + visibility.mAssociatedType.mShorty + |
| hiddenness.mAssociatedType.mShorty + ")V"; |
| String fullName = "<init>" + signature; |
| Class<?> args[] = new Class[] { visibility.mAssociatedType.mClass, |
| hiddenness.mAssociatedType.mClass }; |
| Object initargs[] = new Object[] { visibility.mAssociatedType.mDefaultValue, |
| hiddenness.mAssociatedType.mDefaultValue }; |
| MethodType methodType = MethodType.methodType(void.class, args); |
| |
| boolean canDiscover = (behaviour != Behaviour.Denied); |
| |
| // Test discovery with reflection. |
| |
| if (Reflection.canDiscoverWithGetDeclaredConstructor(klass, args) != canDiscover) { |
| throwDiscoveryException(klass, fullName, false, "getDeclaredConstructor()", canDiscover); |
| } |
| |
| if (Reflection.canDiscoverWithGetDeclaredConstructors(klass, args) != canDiscover) { |
| throwDiscoveryException(klass, fullName, false, "getDeclaredConstructors()", canDiscover); |
| } |
| |
| if (Reflection.canDiscoverWithGetConstructor(klass, args) != (canDiscover && isPublic)) { |
| throwDiscoveryException( |
| klass, fullName, false, "getConstructor()", (canDiscover && isPublic)); |
| } |
| |
| if (Reflection.canDiscoverWithGetConstructors(klass, args) != (canDiscover && isPublic)) { |
| throwDiscoveryException( |
| klass, fullName, false, "getConstructors()", (canDiscover && isPublic)); |
| } |
| |
| // Test discovery with JNI. |
| |
| if (JNI.canDiscoverConstructor(klass, signature) != canDiscover) { |
| throwDiscoveryException(klass, fullName, false, "JNI", canDiscover); |
| } |
| |
| // Test discovery with MethodHandles.lookup() |
| |
| final MethodHandles.Lookup lookup = MethodHandles.lookup(); |
| if (JLI.canDiscoverWithLookupFindConstructor(lookup, klass, methodType) != canDiscover) { |
| throwDiscoveryException(klass, fullName, false, "MethodHandles.lookup().findConstructor", |
| canDiscover); |
| } |
| |
| final MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); |
| if (JLI.canDiscoverWithLookupFindConstructor(publicLookup, klass, methodType) != canDiscover) { |
| throwDiscoveryException(klass, fullName, false, |
| "MethodHandles.publicLookup().findConstructor", |
| canDiscover); |
| } |
| |
| // Check for meta reflection. |
| |
| // With hidden api check hardening enabled, only white and light greylisted constructors should |
| // be discoverable. |
| if (Reflection.canDiscoverConstructorWithMetaReflection(klass, args, true) != canDiscover) { |
| throwDiscoveryException(klass, fullName, false, |
| "Meta reflection with hidden api hardening enabled", canDiscover); |
| } |
| |
| if (testHiddenApiCheckHardeningDisabled) { |
| // With hidden api check hardening disabled, all constructors should be discoverable. |
| if (Reflection.canDiscoverConstructorWithMetaReflection(klass, args, false) != true) { |
| throwDiscoveryException(klass, fullName, false, |
| "Meta reflection with hidden api hardening enabled", canDiscover); |
| } |
| } |
| |
| if (canDiscover) { |
| // Test whether we can invoke the constructor. |
| |
| if (!Reflection.canInvokeConstructor(klass, args, initargs)) { |
| throwAccessException(klass, fullName, false, "invoke()"); |
| } |
| if (!JNI.canInvokeConstructorA(klass, signature)) { |
| throwAccessException(klass, fullName, false, "NewObjectA"); |
| } |
| if (!JNI.canInvokeConstructorV(klass, signature)) { |
| throwAccessException(klass, fullName, false, "NewObjectV"); |
| } |
| } |
| } |
| |
| private static void checkNullaryConstructor(Class<?> klass, Behaviour behaviour) |
| throws Exception { |
| boolean canAccess = (behaviour != Behaviour.Denied); |
| |
| if (Reflection.canUseNewInstance(klass) != canAccess) { |
| throw new RuntimeException("Expected to " + (canAccess ? "" : "not ") + |
| "be able to construct " + klass.getName() + ". " + configMessage); |
| } |
| } |
| |
| private static void checkLinking(String className, boolean takesParameter, Behaviour behaviour) |
| throws Exception { |
| boolean canAccess = (behaviour != Behaviour.Denied); |
| |
| if (Linking.canAccess(className, takesParameter) != canAccess) { |
| throw new RuntimeException("Expected to " + (canAccess ? "" : "not ") + |
| "be able to verify " + className + "." + configMessage); |
| } |
| } |
| |
| private static void throwDiscoveryException(Class<?> klass, String name, boolean isField, |
| String fn, boolean canAccess) { |
| throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() + |
| "." + name + " to " + (canAccess ? "" : "not ") + "be discoverable with " + fn + ". " + |
| configMessage); |
| } |
| |
| private static void throwAccessException(Class<?> klass, String name, boolean isField, |
| String fn) { |
| throw new RuntimeException("Expected to be able to access " + (isField ? "field " : "method ") + |
| klass.getName() + "." + name + " using " + fn + ". " + configMessage); |
| } |
| |
| private static void throwModifiersException(Class<?> klass, String name, boolean isField) { |
| throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() + |
| "." + name + " to not expose hidden modifiers"); |
| } |
| |
| private static DexDomain parentDomain; |
| private static DexDomain childDomain; |
| private static boolean everythingSdked; |
| |
| private static String configMessage; |
| } |