diff options
author | Philip P. Moltmann <moltmann@google.com> | 2018-05-21 19:15:56 +0000 |
---|---|---|
committer | Philip P. Moltmann <moltmann@google.com> | 2018-05-21 13:09:05 -0700 |
commit | 314cb2efb7b1d8d9b584a6e0bd82727168cfd181 (patch) | |
tree | 3b848189d1bc1a2238caf1d1285953232c35806a | |
parent | 8738f6764b9f61eb11ad678cad6d9434b4133731 (diff) | |
download | dexmaker-pie-qpr1-s1-release.tar.gz |
DO NOT MERGE: Revert "DO NOT MERGE: Revert "Allow mocks to call blackisted APIs""android-9.0.0_r35android-9.0.0_r34android-9.0.0_r33android-9.0.0_r32android-9.0.0_r31android-9.0.0_r30android-9.0.0_r22android-9.0.0_r21android-9.0.0_r20android-9.0.0_r19android-9.0.0_r16android-9.0.0_r12android-9.0.0_r11pie-qpr2-releasepie-qpr1-s3-releasepie-qpr1-s2-releasepie-qpr1-s1-releasepie-qpr1-releasepie-dr1-releasepie-dr1-devpie-dev
This reverts commit 8738f6764b9f61eb11ad678cad6d9434b4133731.
Reason for revert: This was only a temporary revert. Now that the real issue is fixed, we can submit this again
Bug: 80041014
Test: vts-tradefed run host --class com.android.tradefed.device.metric.VtsCoverageCollectorTest
atest CtsActivityManagerDeviceTestCases
atest CtsInputMethodTestCases
Change-Id: I8d0ac45e3fe73c1870cf08f989b6d39f09923183
6 files changed, 297 insertions, 0 deletions
diff --git a/README.version b/README.version index 2d91670..92d3d64 100644 --- a/README.version +++ b/README.version @@ -10,3 +10,5 @@ It includes a stock code generator for class proxies. If you just want to do AOP Local Modifications: Allow to share classloader via dexmaker.share_classloader system property (I8c2490c3ec8e8582dc41c486f8f7a406bd635ebb) + Allow 'Q' until we can replace the version check with a number based check + Mark mocks as trusted (needs upstreaming) diff --git a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java index 319ff0d..abcf4f5 100644 --- a/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java +++ b/dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java @@ -26,6 +26,7 @@ import com.android.dx.stock.ProxyBuilder.MethodSetEntry; import org.mockito.Mockito; import org.mockito.exceptions.base.MockitoException; import org.mockito.internal.creation.instance.Instantiator; +import org.mockito.internal.util.reflection.LenientCopyTool; import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; import org.mockito.plugins.InstantiatorProvider; @@ -36,6 +37,7 @@ import java.io.InputStream; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; @@ -106,6 +108,40 @@ public final class InlineDexmakerMockMaker implements MockMaker { + "Potentially, the current VM does not support the jvmti API correctly", ioe); } + + // Blacklisted APIs were introduced in Android P: + // + // https://android-developers.googleblog.com/2018/02/ + // improving-stability-by-reducing-usage.html + // + // This feature prevents access to blacklisted fields and calling of blacklisted APIs + // if the calling class is not trusted. + Method allowHiddenApiReflectionFrom; + try { + Class vmDebug = Class.forName("dalvik.system.VMDebug"); + allowHiddenApiReflectionFrom = vmDebug.getDeclaredMethod( + "allowHiddenApiReflectionFrom", Class.class); + } catch (ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalStateException("Cannot find " + + "VMDebug#allowHiddenApiReflectionFrom."); + } + + // The LenientCopyTool copies the fields to a spy when creating the copy from an + // existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool + // as trusted allows the tool to copy all fields, including the blacklisted ones. + try { + allowHiddenApiReflectionFrom.invoke(null, LenientCopyTool.class); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + + // The MockMethodAdvice is used by methods of spies to call the real methods. As the + // real methods might be blacklisted, this class needs to be marked as trusted. + try { + allowHiddenApiReflectionFrom.invoke(null, MockMethodAdvice.class); + } catch (InvocationTargetException e) { + throw e.getCause(); + } } catch (Throwable throwable) { agent = null; initializationError = throwable; diff --git a/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/BlacklistedApis.java b/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/BlacklistedApis.java new file mode 100644 index 0000000..ffe55fb --- /dev/null +++ b/dexmaker-mockito-tests/src/androidTest/java/com/android/dx/mockito/tests/BlacklistedApis.java @@ -0,0 +1,161 @@ +/* + * 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 com.android.dx.mockito.tests; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.view.View; +import android.widget.FrameLayout; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Simplified versions of bugs seen in the past + */ +@RunWith(AndroidJUnit4.class) +public class BlacklistedApis { + /** + * Check if the application is marked as {@code android:debuggable} in the manifest + * + * @return {@code true} iff it is marked as such + */ + private boolean isDebuggable() throws PackageManager.NameNotFoundException { + Context context = InstrumentationRegistry.getTargetContext(); + PackageInfo packageInfo = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0); + + return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + + @Test + public void callBlacklistedPublicMethodRealMethod() throws Exception { + Context targetContext = InstrumentationRegistry.getTargetContext(); + + FrameLayout child = new FrameLayout(targetContext); + FrameLayout parent = spy(new FrameLayout(targetContext)); + + if (isDebuggable()) { + // This calls a blacklisted public method. + // Since Android P these methods are not callable from outside of the Android framework + // anymore: + // + // https://android-developers.googleblog.com/2018/02/ + // improving-stability-by-reducing-usage.html + // + // Hence if we use a subclass mock this will fail. Inline mocking does not have this + // problem as the mock class is the same as the mocked class. + parent.addView(child); + } else { + try { + parent.addView(child); + fail(); + } catch (NoSuchMethodError expected) { + // expected + } + } + } + + @Test + public void copyBlacklistedFields() throws Exception { + assumeTrue(isDebuggable()); + + Context targetContext = InstrumentationRegistry.getTargetContext(); + + FrameLayout child = new FrameLayout(targetContext); + FrameLayout parent = spy(new FrameLayout(targetContext)); + + parent.addView(child); + + // During cloning of the parent spy, all fields are copied. This accesses a blacklisted + // fields. Since Android P these fields are not visible from outside of the Android + // framework anymore: + // + // https://android-developers.googleblog.com/2018/02/ + // improving-stability-by-reducing-usage.html + // + // As 'measure' requires the fields to be initialized, this fails if the fields are not + // copied. + parent.measure(100, 100); + } + + @Test + public void cannotCallBlackListedAfterSpying() { + // Spying and mocking might change the View class's byte code + spy(new View(InstrumentationRegistry.getTargetContext(), null)); + mock(View.class); + + // View#setNotifyAutofillManagerOnClick is a blacklisted method. Resolving it should fail + try { + View.class.getDeclaredMethod("setNotifyAutofillManagerOnClick", Boolean.TYPE); + fail(); + } catch (NoSuchMethodException expected) { + // expected + } + } + + public class CallBlackListedMethod { + public boolean callingBlacklistedMethodCausesException() { + // Settings.Global#isValidZenMode is a blacklisted method. Resolving it should fail + try { + Settings.Global.class.getDeclaredMethod("isValidZenMode", Integer.TYPE); + return false; + } catch (NoSuchMethodException expected) { + return true; + } + } + } + + @Test + public void spiesCannotBeUsedToCallHiddenMethods() { + CallBlackListedMethod t = spy(new CallBlackListedMethod()); + assertTrue(t.callingBlacklistedMethodCausesException()); + } + + public abstract class CallBlacklistedMethodAbstract { + public boolean callingBlacklistedMethodCausesException() { + // Settings.Global#isValidZenMode is a blacklisted method. Resolving it should fail + try { + Settings.Global.class.getDeclaredMethod("isValidZenMode", Integer.TYPE); + return false; + } catch (NoSuchMethodException expected) { + return true; + } + } + + public abstract void unused(); + } + + @Test + public void mocksOfAbstractClassesCannotBeUsedToCallHiddenMethods() { + CallBlacklistedMethodAbstract t = mock(CallBlacklistedMethodAbstract.class); + doCallRealMethod().when(t).callingBlacklistedMethodCausesException(); + assertTrue(t.callingBlacklistedMethodCausesException()); + } +} diff --git a/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java b/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java index b19f7bd..19f371e 100644 --- a/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java +++ b/dexmaker-mockito/src/main/java/com/android/dx/mockito/DexmakerMockMaker.java @@ -16,15 +16,21 @@ package com.android.dx.mockito; +import android.os.Build; +import android.util.Log; + import com.android.dx.stock.ProxyBuilder; import org.mockito.exceptions.base.MockitoException; import org.mockito.exceptions.stacktrace.StackTraceCleaner; +import org.mockito.internal.util.reflection.LenientCopyTool; import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; import org.mockito.plugins.MockMaker; import org.mockito.plugins.StackTraceCleanerProvider; import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.Set; @@ -33,8 +39,42 @@ import java.util.Set; * Generates mock instances on Android's runtime. */ public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProvider { + private static final String LOG_TAG = DexmakerMockMaker.class.getSimpleName(); + private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); + public DexmakerMockMaker() { + if (Build.VERSION.SDK_INT >= 28) { + // Blacklisted APIs were introduced in Android P: + // + // https://android-developers.googleblog.com/2018/02/ + // improving-stability-by-reducing-usage.html + // + // This feature prevents access to blacklisted fields and calling of blacklisted APIs + // if the calling class is not trusted. + Method allowHiddenApiReflectionFromMethod; + try { + Class vmDebug = Class.forName("dalvik.system.VMDebug"); + allowHiddenApiReflectionFromMethod = vmDebug.getDeclaredMethod( + "allowHiddenApiReflectionFrom", Class.class); + } catch (ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalStateException( + "Cannot find VMDebug#allowHiddenApiReflectionFrom. Method is needed to " + + "allow spies to copy blacklisted fields."); + } + + // The LenientCopyTool copies the fields to a spy when creating the copy from an + // existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool + // as trusted allows the tool to copy all fields, including the blacklisted ones. + try { + allowHiddenApiReflectionFromMethod.invoke(null, LenientCopyTool.class); + } catch (InvocationTargetException | IllegalAccessException e) { + Log.w(LOG_TAG, "Cannot allow LenientCopyTool to copy spies of blacklisted fields. " + + "This might break spying on system classes."); + } + } + } + @Override public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { Class<T> typeToMock = settings.getTypeToMock(); diff --git a/dexmaker/src/main/java/com/android/dx/DexMaker.java b/dexmaker/src/main/java/com/android/dx/DexMaker.java index 02baa9b..755c9fa 100644 --- a/dexmaker/src/main/java/com/android/dx/DexMaker.java +++ b/dexmaker/src/main/java/com/android/dx/DexMaker.java @@ -49,6 +49,8 @@ import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR; import static java.lang.reflect.Modifier.PRIVATE; import static java.lang.reflect.Modifier.STATIC; +import android.util.Log; + /** * Generates a <strong>D</strong>alvik <strong>EX</strong>ecutable (dex) * file for execution on Android. Dex files define classes and interfaces, @@ -196,9 +198,12 @@ import static java.lang.reflect.Modifier.STATIC; * }</pre> */ public final class DexMaker { + private static final String LOG_TAG = DexMaker.class.getSimpleName(); + private final Map<TypeId<?>, TypeDeclaration> types = new LinkedHashMap<>(); private ClassLoader sharedClassLoader; private DexFile outputDex; + private boolean markAsTrusted; /** * Creates a new {@code DexMaker} instance, which can be used to create a @@ -359,12 +364,51 @@ public final class DexMaker { return "Generated_" + checksum +".jar"; } + /** + * Set shared class loader to use. + * + * <p>If a class wants to call package private methods of another class they need to share a + * class loader. One common case for this requirement is a mock class wanting to mock package + * private methods of the original class. + * + * @param classLoader the class loader the new class should be loaded by + */ public void setSharedClassLoader(ClassLoader classLoader) { this.sharedClassLoader = classLoader; } + public void markAsTrusted() { + this.markAsTrusted = true; + } + private ClassLoader generateClassLoader(File result, File dexCache, ClassLoader parent) { try { + // Try to load the class so that it can call hidden APIs. This is required for spying + // on system classes as real-methods of these classes might call blacklisted APIs + if (markAsTrusted) { + try { + if (sharedClassLoader != null) { + ClassLoader loader = parent != null ? parent : sharedClassLoader; + loader.getClass().getMethod("addDexPath", String.class, + Boolean.TYPE).invoke(loader, result.getPath(), true); + return loader; + } else { + return (ClassLoader) Class.forName("dalvik.system.BaseDexClassLoader") + .getConstructor(String.class, File.class, String.class, + ClassLoader.class, Boolean.TYPE) + .newInstance(result.getPath(), dexCache.getAbsoluteFile(), null, + parent, true); + } + } catch (InvocationTargetException e) { + if (e.getCause() instanceof SecurityException) { + Log.i(LOG_TAG, "Cannot allow to call blacklisted super methods. This might " + + "break spying on system classes.", e.getCause()); + } else { + throw e; + } + } + } + if (sharedClassLoader != null) { ClassLoader loader = parent != null ? parent : sharedClassLoader; loader.getClass().getMethod("addDexPath", String.class).invoke(loader, diff --git a/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java b/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java index 1363894..053fb16 100644 --- a/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java +++ b/dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java @@ -47,6 +47,8 @@ import static java.lang.reflect.Modifier.PRIVATE; import static java.lang.reflect.Modifier.PUBLIC; import static java.lang.reflect.Modifier.STATIC; +import android.os.Build; + /** * Creates dynamic proxies of concrete classes. * <p> @@ -301,6 +303,18 @@ public final class ProxyBuilder<T> { if (sharedClassLoader) { dexMaker.setSharedClassLoader(baseClass.getClassLoader()); } + if (Build.VERSION.SDK_INT >= 28) { + // The proxied class might have blacklisted methods. Blacklisting methods (and fields) + // is a new feature of Android P: + // + // https://android-developers.googleblog.com/2018/02/ + // improving-stability-by-reducing-usage.html + // + // The newly generated class might not be allowed to call methods of the proxied class + // if it is not trusted. As it is not clear which classes have blacklisted methods, mark + // all generated classes as trusted. + dexMaker.markAsTrusted(); + } ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache); try { proxyClass = loadClass(classLoader, generatedName); |