Don't rebind above api unreachable classes on dalvik

On dalvik we can trip up the verifier if we rebind an invoke above a
(at runtime) unknown class

Bug: b/302826300
Bug: b/350946989
Change-Id: Iae17a77904e520bbd80386f3fa95d7b860a3228c
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
index 60b1176..14f21b3e 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingHelper.java
@@ -13,12 +13,14 @@
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.AndroidApiLevelUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Iterables;
@@ -47,8 +49,10 @@
     if (invokeType.isDirect()) {
       return original;
     }
-
-    if (invokeType.isSuper() && options.canHaveSuperInvokeBug()) {
+    if ((invokeType.isSuper() && options.canHaveSuperInvokeBug())
+        || (invokeType.isVirtual()
+            && options.canHaveDalvikVerifyErrorOnVirtualInvokeWithMissingClasses()
+            && canBreakDalvikWithRebinding(resolutionResult))) {
       // To preserve semantics we should find the first library method on the boundary.
       DexType firstLibraryTarget =
           firstLibraryClassOrFirstInterfaceTarget(
@@ -113,6 +117,36 @@
     return newHolder != null ? original.withHolder(newHolder, appView.dexItemFactory()) : original;
   }
 
+  // On dalvik we can have hard verification errors if we rebind to known library classes that
+  // are super classes unknown library classes.
+  private boolean canBreakDalvikWithRebinding(SingleResolutionResult<?> resolutionResult) {
+    assert !resolutionResult.getResolvedHolder().isProgramClass();
+
+    DexClass current = resolutionResult.getInitialResolutionHolder();
+    while (current != null && !current.isLibraryClass()) {
+      current = appView.definitionFor(current.getSuperType());
+    }
+    if (current == null) {
+      assert resolutionResult.getResolvedHolder().isInterface();
+      return false;
+    }
+
+    DexLibraryClass currentLibraryClass = current.asLibraryClass();
+    while (currentLibraryClass != null) {
+      if (!AndroidApiLevelUtils.isApiSafeForReference(currentLibraryClass, appView)) {
+        return true;
+      }
+      if (!currentLibraryClass.hasSuperType()) {
+        // Object
+        break;
+      }
+      currentLibraryClass =
+          DexLibraryClass.asLibraryClassOrNull(
+              appView.definitionFor(currentLibraryClass.getSuperType()));
+    }
+    return false;
+  }
+
   private boolean canRebindDirectlyToLibraryMethod(
       DexClassAndMethod resolvedMethod,
       SingleResolutionResult<?> resolutionResult,
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 074df65..8cee41d 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -3288,4 +3288,11 @@
   public boolean canInitNewInstanceUsingSuperclassConstructor() {
     return isGeneratingDex() && minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.L);
   }
+
+  // b/302826300 Dalvik can vms gives hard verification errors when we rebind program types to
+  // library types that are superclasses of unavailable classes on the specific api level.
+  // Instead, we should always rebind to just the first library class.
+  public boolean canHaveDalvikVerifyErrorOnVirtualInvokeWithMissingClasses() {
+    return canHaveBugPresentUntilExclusive(AndroidApiLevel.L);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java b/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java
index 90d4305..53740e7 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/LibraryMemberRebindingInterfaceTest.java
@@ -136,6 +136,11 @@
       // If we are compiling to an old runtime with a new android.jar, we should rebind to LibraryB.
       return LibraryB.class;
     }
+    // On pre L we don't rebind above classes that we can't find in the api database
+    if (!parameters.isDexRuntime() || parameters.getApiLevel().isLessThan(AndroidApiLevel.L)) {
+      return LibraryB.class;
+    }
+
     // Otherwise, we are compiling to an old android.jar, in which case we should rebind to
     // LibraryI.
     return LibraryI.class;
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingWithApiDatabaseLookupTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingWithApiDatabaseLookupTest.java
new file mode 100644
index 0000000..c9bec48
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingWithApiDatabaseLookupTest.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.memberrebinding;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithHolderAndName;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoMethodStaticizing;
+import com.android.tools.r8.NoParameterTypeStrengthening;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MemberRebindingWithApiDatabaseLookupTest extends TestBase {
+
+  static final String EXPECTED = "foobar";
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public MemberRebindingWithApiDatabaseLookupTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(Foo.class, Bar.class, ProgramClassExtendsLibrary.class)
+        .addLibraryClasses(LibraryClass.class, LibraryBaseClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoMethodStaticizingAnnotations()
+        .enableConstantArgumentAnnotations()
+        .enableNoParameterTypeStrengtheningAnnotations()
+        .addKeepMainRule(Foo.class)
+        .addKeepClassRules(ProgramClassExtendsLibrary.class)
+        .apply(setMockApiLevelForClass(LibraryBaseClass.class, AndroidApiLevel.B))
+        .apply(setMockApiLevelForClass(LibraryClass.class, AndroidApiLevel.L))
+        .apply(
+            setMockApiLevelForMethod(
+                LibraryBaseClass.class.getDeclaredMethod("z"), AndroidApiLevel.B))
+        .compile()
+        .addRunClasspathClasses(LibraryBaseClass.class)
+        .run(parameters.getRuntime(), Foo.class)
+        .assertSuccessWithOutputLines(EXPECTED)
+        .inspect(
+            codeInspector -> {
+              // We should not have rewritten the invoke virtual in X to the base most library class
+              // if we are not on dalvik, otherwise the first library class.
+              Class expectedInvokeClass =
+                  parameters.getApiLevel().isLessThan(AndroidApiLevel.L)
+                      ? LibraryClass.class
+                      : LibraryBaseClass.class;
+              MethodSubject xMethod =
+                  codeInspector.clazz(Bar.class).uniqueMethodWithOriginalName("x");
+              assertThat(
+                  xMethod, invokesMethodWithHolderAndName(expectedInvokeClass.getTypeName(), "z"));
+            });
+  }
+
+  public static class Foo {
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        new Bar().x(null);
+      } else {
+        new Bar().y("foobar");
+      }
+    }
+  }
+
+  @NeverClassInline
+  public static class Bar {
+    @NeverInline
+    @NoMethodStaticizing
+    @KeepConstantArguments
+    @NoParameterTypeStrengthening
+    public void x(ProgramClassExtendsLibrary value) {
+      // We need to ensure that we don't rebind this to LibraryBaseClass as that will make dalvik
+      // (incorrectly) hard fail verification even when we don't call this method.
+      // Since the superclass of value is not available at runtime dalvik will lower value to
+      // type object, which we can't use for the invoke virtual on the known library class
+      // LibraryBaseClass.
+      value.z();
+    }
+
+    @NeverInline
+    public void y(String s) {
+      System.out.println(s);
+    }
+  }
+
+  public static class ProgramClassExtendsLibrary extends LibraryClass {}
+
+  public static class LibraryClass extends LibraryBaseClass {}
+
+  public static class LibraryBaseClass {
+    public void z() {
+      System.out.println("z()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/Regress302826300.java b/src/test/java/com/android/tools/r8/regress/Regress302826300.java
index f87eb90..d8cda04 100644
--- a/src/test/java/com/android/tools/r8/regress/Regress302826300.java
+++ b/src/test/java/com/android/tools/r8/regress/Regress302826300.java
@@ -31,6 +31,9 @@
 
   @Test
   public void testD8() throws Exception {
+    // This is illustrating the problem from b/302826300 using just D8 (in which case we don't
+    // introduce the error, the error is in the build setup). The actual bug is introduced by
+    // R8, see MemberRebindingWithApiDatabaseLookupTest.
     D8TestRunResult run =
         testForD8(parameters.getBackend())
             // We simply pass LibraryBaseClass as program, but could also compile it separately and