[ApiModel] Insert CheckCast for return values causing verification error

Bug: b/272725341
Change-Id: I2479d669217d90f057fa248fcea0f0f27faf91a6
diff --git a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
index 7562429..51394a9 100644
--- a/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
+++ b/src/main/java/com/android/tools/r8/androidapi/ComputedApiLevel.java
@@ -79,6 +79,8 @@
 
   OptionalBool isLessThanOrEqualTo(ComputedApiLevel other);
 
+  OptionalBool isGreaterThan(AndroidApiLevel other);
+
   class NotSetApiLevel implements ComputedApiLevel {
 
     private static final NotSetApiLevel INSTANCE = new NotSetApiLevel();
@@ -98,6 +100,12 @@
     }
 
     @Override
+    public OptionalBool isGreaterThan(AndroidApiLevel other) {
+      assert false : "Cannot compute relationship for not set";
+      return OptionalBool.unknown();
+    }
+
+    @Override
     public boolean isNotSetApiLevel() {
       return true;
     }
@@ -130,6 +138,11 @@
     }
 
     @Override
+    public OptionalBool isGreaterThan(AndroidApiLevel other) {
+      return OptionalBool.unknown();
+    }
+
+    @Override
     public boolean isUnknownApiLevel() {
       return true;
     }
@@ -192,6 +205,11 @@
     }
 
     @Override
+    public OptionalBool isGreaterThan(AndroidApiLevel other) {
+      return OptionalBool.of(apiLevel.isGreaterThan(other));
+    }
+
+    @Override
     public String toString() {
       return apiLevel.toString();
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Return.java b/src/main/java/com/android/tools/r8/ir/code/Return.java
index 935e9df..971e059 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Return.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Return.java
@@ -141,9 +141,16 @@
 
   public static class Builder extends BuilderBase<Builder, Return> {
 
+    private Value returnValue = null;
+
+    public Builder setReturnValue(Value returnValue) {
+      this.returnValue = returnValue;
+      return self();
+    }
+
     @Override
     public Return build() {
-      return amend(new Return());
+      return amend(returnValue == null ? new Return() : new Return(returnValue));
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index afa6aa1..265cccb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -65,6 +65,7 @@
 import com.android.tools.r8.ir.optimize.NaturalIntLoopRemover;
 import com.android.tools.r8.ir.optimize.RedundantFieldLoadAndStoreElimination;
 import com.android.tools.r8.ir.optimize.ReflectionOptimizer;
+import com.android.tools.r8.ir.optimize.RemoveVerificationErrorForUnknownReturnedValues;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
@@ -142,7 +143,9 @@
   private final TypeChecker typeChecker;
   private final ServiceLoaderRewriter serviceLoaderRewriter;
   private final EnumValueOptimizer enumValueOptimizer;
-  private final EnumUnboxer enumUnboxer;
+  protected final EnumUnboxer enumUnboxer;
+  protected final RemoveVerificationErrorForUnknownReturnedValues
+      removeVerificationErrorForUnknownReturnedValues;
 
   public final AssumeInserter assumeInserter;
   private final DynamicTypeOptimization dynamicTypeOptimization;
@@ -227,6 +230,7 @@
       this.enumValueOptimizer = null;
       this.enumUnboxer = EnumUnboxer.empty();
       this.assumeInserter = null;
+      this.removeVerificationErrorForUnknownReturnedValues = null;
       return;
     }
     this.instructionDesugaring =
@@ -237,6 +241,11 @@
         options.processCovariantReturnTypeAnnotations
             ? new CovariantReturnTypeAnnotationTransformer(this, appView.dexItemFactory())
             : null;
+    removeVerificationErrorForUnknownReturnedValues =
+        (appView.options().apiModelingOptions().enableApiCallerIdentification
+                && appView.options().canHaveVerifyErrorForUnknownUnusedReturnValue())
+            ? new RemoveVerificationErrorForUnknownReturnedValues(appView)
+            : null;
     if (appView.enableWholeProgramOptimizations()) {
       assert appView.appInfo().hasLiveness();
       assert appView.rootSet() != null;
@@ -1324,7 +1333,7 @@
     timing.begin("Split range invokes");
     codeRewriter.splitRangeInvokeConstants(code);
     timing.end();
-    timing.begin("Propogate sparse conditionals");
+    timing.begin("Propagate sparse conditionals");
     new SparseConditionalConstantPropagation(appView, code).run();
     timing.end();
     timing.begin("Rewrite always throwing instructions");
@@ -1451,6 +1460,10 @@
       previous = printMethod(code, "IR after shorten live ranges (SSA)", previous);
     }
 
+    if (removeVerificationErrorForUnknownReturnedValues != null) {
+      removeVerificationErrorForUnknownReturnedValues.run(context, code, timing);
+    }
+
     timing.begin("Canonicalize idempotent calls");
     idempotentFunctionCallCanonicalizer.canonicalize(code);
     timing.end();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
index 3050bdc..c71debf 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.desugar.apimodel;
 
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.isApiLevelLessThanOrEqualToG;
 import static org.objectweb.asm.Opcodes.INVOKESTATIC;
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
@@ -34,7 +35,6 @@
 import com.android.tools.r8.ir.synthetic.FieldAccessorBuilder;
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
@@ -114,7 +114,7 @@
     ComputedApiLevel referenceApiLevel =
         apiLevelCompute.computeApiLevelForLibraryReference(reference, ComputedApiLevel.unknown());
     if (appView.computedMinApiLevel().isGreaterThanOrEqualTo(referenceApiLevel)
-        || isApiLevelLessThanOrEqualTo9(referenceApiLevel)
+        || isApiLevelLessThanOrEqualToG(referenceApiLevel)
         || referenceApiLevel.isUnknownApiLevel()) {
       return appView.computedMinApiLevel();
     }
@@ -155,11 +155,6 @@
     return traversalResult.isBreak() ? traversalResult.asBreak().getValue() : null;
   }
 
-  private boolean isApiLevelLessThanOrEqualTo9(ComputedApiLevel apiLevel) {
-    return apiLevel.isKnownApiLevel()
-        && apiLevel.asKnownApiLevel().getApiLevel().isLessThanOrEqualTo(AndroidApiLevel.G);
-  }
-
   private Collection<CfInstruction> desugarLibraryCall(
       UniqueContext uniqueContext,
       CfInstruction instruction,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RemoveVerificationErrorForUnknownReturnedValues.java b/src/main/java/com/android/tools/r8/ir/optimize/RemoveVerificationErrorForUnknownReturnedValues.java
new file mode 100644
index 0000000..dd8f5bd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RemoveVerificationErrorForUnknownReturnedValues.java
@@ -0,0 +1,216 @@
+// Copyright (c) 2023, 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.ir.optimize;
+
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.isApiLevelLessThanOrEqualToG;
+
+import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
+import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.Set;
+
+/***
+ * Some Dalvik and ART runtimes have problems with verification when it comes to computing subtype
+ * relationship. Take the following code:
+ * <pre>
+ *     public LibraryClass callLibraryWithDirectReturn() {
+ *       if (AndroidBuildVersion.SDK_INT < 31) {
+ *         return null;
+ *       } else {
+ *         LibrarySub sub = Outline.create();
+ *         return sub;
+ *       }
+ *     }
+ * </pre>
+ *
+ * This can cause verification failures if LibraryClass is known but LibrarySub is unknown. It seems
+ * like the problem is that the verifier assumes that it can compute the relationship. If we add
+ * an instruction that causes soft-verification we remove the hard verification error:
+ * <pre>
+ *     public LibraryClass callLibraryWithDirectReturn() {
+ *       if (AndroidBuildVersion.SDK_INT < 31) {
+ *         return null;
+ *       } else {
+ *         LibrarySub sub = Outline.create();
+ *         sub.foo();
+ *         return sub;
+ *       }
+ *     }
+ * </pre>
+ *
+ * The assumption here is that the verifier will figure out that it needs to run this by
+ * interpreting and bails out.
+ *
+ * We fix the issue here, both for our outlines and for manual outlines, by putting in a check-cast
+ * on the return value if a potential unknown library subtype flows to the return value.
+ * <pre>
+ *     public LibraryClass callLibraryWithDirectReturn() {
+ *       if (AndroidBuildVersion.SDK_INT < 31) {
+ *         return null;
+ *       } else {
+ *         LibrarySub sub = Outline.create();
+ *         return (LibraryClass)sub;
+ *       }
+ *     }
+ * </pre>
+ *
+ * See b/272725341 for more information.
+ */
+public class RemoveVerificationErrorForUnknownReturnedValues {
+
+  private final AppView<?> appView;
+  private final AndroidApiLevelCompute apiLevelCompute;
+  private final SyntheticItems syntheticItems;
+
+  public RemoveVerificationErrorForUnknownReturnedValues(AppView<?> appView) {
+    this.appView = appView;
+    this.apiLevelCompute = appView.apiLevelCompute();
+    this.syntheticItems = appView.getSyntheticItems();
+  }
+
+  private AppInfoWithClassHierarchy getAppInfoWithClassHierarchy() {
+    return appView.appInfoForDesugaring();
+  }
+
+  public void run(ProgramMethod context, IRCode code, Timing timing) {
+    timing.begin("Compute and insert checkcast on return values");
+    AppInfoWithClassHierarchy appInfoWithClassHierarchy = getAppInfoWithClassHierarchy();
+    Set<Return> returnValuesNeedingCheckCast =
+        getReturnsPotentiallyNeedingCheckCast(appInfoWithClassHierarchy, context, code);
+    insertCheckCastForReturnValues(context, code, returnValuesNeedingCheckCast);
+    timing.end();
+  }
+
+  private Set<Return> getReturnsPotentiallyNeedingCheckCast(
+      AppInfoWithClassHierarchy appInfo, ProgramMethod context, IRCode code) {
+    if (syntheticItems.isSyntheticOfKind(
+        context.getHolderType(), kinds -> kinds.API_MODEL_OUTLINE)) {
+      return Collections.emptySet();
+    }
+    DexType returnType = context.getReturnType();
+    if (!returnType.isClassType()) {
+      return Collections.emptySet();
+    }
+    // Everything is assignable to object type and the verifier do not throw an error here.
+    if (returnType == appView.dexItemFactory().objectType) {
+      return Collections.emptySet();
+    }
+    DexClass returnTypeClass = appInfo.definitionFor(returnType);
+    if (returnTypeClass == null || !returnTypeClass.isLibraryClass()) {
+      return Collections.emptySet();
+    }
+    ComputedApiLevel computedReturnApiLevel =
+        apiLevelCompute.computeApiLevelForLibraryReference(returnType, ComputedApiLevel.unknown());
+    if (computedReturnApiLevel.isUnknownApiLevel()) {
+      return Collections.emptySet();
+    }
+    Set<Value> seenSet = Sets.newIdentityHashSet();
+    Set<Return> returnsOfInterest = Sets.newIdentityHashSet();
+    code.computeNormalExitBlocks()
+        .forEach(
+            basicBlock -> {
+              Return exit = basicBlock.exit().asReturn();
+              Value aliasedReturnValue = exit.returnValue().getAliasedValue();
+              if (shouldInsertCheckCastForValue(appInfo, returnType, aliasedReturnValue, seenSet)) {
+                returnsOfInterest.add(exit);
+              }
+            });
+    return returnsOfInterest;
+  }
+
+  private boolean shouldInsertCheckCastForValue(
+      AppInfoWithClassHierarchy appInfo, DexType returnType, Value value, Set<Value> seenSet) {
+    WorkList<Value> workList = WorkList.newIdentityWorkList(value, seenSet);
+    while (workList.hasNext()) {
+      Value next = workList.next();
+      if (next.isPhi()) {
+        workList.addIfNotSeen(next.asPhi().getOperands());
+      }
+      TypeElement type = next.getType();
+      if (!type.isClassType()) {
+        assert type.isNullType() || type.isArrayType();
+        continue;
+      }
+      DexType returnValueType = type.asClassType().getClassType();
+      DexClass returnValueClass = appInfo.definitionFor(returnValueType);
+      if (returnValueClass == null || !returnValueClass.isLibraryClass()) {
+        continue;
+      }
+      if (!appInfo.isStrictSubtypeOf(returnValueType, returnType)) {
+        continue;
+      }
+      ComputedApiLevel computedValueApiLevel =
+          apiLevelCompute.computeApiLevelForLibraryReference(
+              returnValueType, ComputedApiLevel.unknown());
+      // We could in principle also bail out if the computedValueApiLevel == computedReturnApiLevel,
+      // however, if we stub the return type class we will introduce the error again. We do not know
+      // at this point if we stub the returnTypeClass.
+      ComputedApiLevel minApiLevel = appView.computedMinApiLevel();
+      if (!computedValueApiLevel.isUnknownApiLevel()
+          && !isApiLevelLessThanOrEqualToG(computedValueApiLevel)
+          && computedValueApiLevel.isGreaterThan(minApiLevel)
+          && isDalvikOrSubTypeIntroducedLaterThanAndroidR(minApiLevel, computedValueApiLevel)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // Dalvik and some new ART versions have a stricter verifier that do not allow type-checking
+  // unknown return value types against a known return type.
+  private boolean isDalvikOrSubTypeIntroducedLaterThanAndroidR(
+      ComputedApiLevel minApiLevel, ComputedApiLevel subTypeApiLevel) {
+    if (minApiLevel.isLessThanOrEqualTo(AndroidApiLevel.K_WATCH).isPossiblyTrue()) {
+      return true;
+    }
+    return subTypeApiLevel.isGreaterThan(AndroidApiLevel.R).isPossiblyTrue();
+  }
+
+  private void insertCheckCastForReturnValues(
+      ProgramMethod context, IRCode code, Set<Return> returnsNeedingCast) {
+    if (returnsNeedingCast.isEmpty()) {
+      return;
+    }
+    InstructionListIterator iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Return returnInstruction = iterator.next().asReturn();
+      if (returnInstruction == null) {
+        continue;
+      }
+      DexType returnType = context.getReturnType();
+      Value returnValue = returnInstruction.returnValue();
+      CheckCast checkCast =
+          CheckCast.builder()
+              .setObject(returnValue)
+              .setFreshOutValue(
+                  code, returnType.toTypeElement(appView, returnValue.getType().nullability()))
+              .setCastType(returnType)
+              .setPosition(returnInstruction.getPosition())
+              .build();
+      iterator.replaceCurrentInstruction(checkCast);
+      iterator.add(
+          Return.builder()
+              .setPosition(returnInstruction.getPosition())
+              .setReturnValue(checkCast.outValue())
+              .build());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 2c3b88b..4e45bb5 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -22,6 +23,9 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.Set;
 
 public class AndroidApiLevelUtils {
 
@@ -179,4 +183,147 @@
     return oldBaseLibraryClass != null
         && isApiSafeForReference(newBaseLibraryClass, oldBaseLibraryClass, appView);
   }
+
+  public static Pair<DexClass, ComputedApiLevel> findAndComputeApiLevelForLibraryDefinition(
+      AppView<?> appView,
+      AppInfoWithClassHierarchy appInfo,
+      DexClass holder,
+      DexMember<?, ?> reference) {
+    AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
+    if (holder.isLibraryClass()) {
+      return Pair.create(
+          holder,
+          apiLevelCompute.computeApiLevelForLibraryReference(
+              reference, ComputedApiLevel.unknown()));
+    }
+    // The API database do not allow for resolving into it (since that is not stable), and it is
+    // therefore designed in a way where all members of classes can be queried on any sub-type with
+    // the api level for where it is reachable. It is therefore sufficient for us, to figure out if
+    // an instruction is a library call, to either find a program definition or to find the library
+    // frontier.
+    // Scan through the type hierarchy to find the first library class or program definition.
+    DexClass firstClassWithReferenceOrLibraryClass =
+        firstLibraryClassOrProgramClassWithDefinition(appInfo, holder, reference);
+    if (firstClassWithReferenceOrLibraryClass == null) {
+      return Pair.create(null, ComputedApiLevel.unknown());
+    }
+    if (!firstClassWithReferenceOrLibraryClass.isLibraryClass()) {
+      return Pair.create(firstClassWithReferenceOrLibraryClass, appView.computedMinApiLevel());
+    }
+    ComputedApiLevel apiLevel =
+        apiLevelCompute.computeApiLevelForLibraryReference(
+            reference.withHolder(
+                firstClassWithReferenceOrLibraryClass.getType(), appView.dexItemFactory()),
+            ComputedApiLevel.unknown());
+    if (apiLevel.isKnownApiLevel()) {
+      return Pair.create(firstClassWithReferenceOrLibraryClass, apiLevel);
+    }
+    // We were unable to find a definition in the class hierarchy, check all interfaces for a
+    // definition or the library interfaces for the first interface definition.
+    Set<DexClass> firstLibraryInterfaces =
+        findAllFirstLibraryInterfacesOrProgramClassWithDefinition(appInfo, holder, reference);
+    if (firstLibraryInterfaces.size() == 1) {
+      DexClass firstClass = firstLibraryInterfaces.iterator().next();
+      if (!firstClass.isLibraryClass()) {
+        return Pair.create(firstClass, appView.computedMinApiLevel());
+      }
+    }
+    DexClass foundClass = null;
+    ComputedApiLevel minApiLevel = ComputedApiLevel.unknown();
+    for (DexClass libraryInterface : firstLibraryInterfaces) {
+      assert libraryInterface.isLibraryClass();
+      ComputedApiLevel libraryIfaceApiLevel =
+          apiLevelCompute.computeApiLevelForLibraryReference(
+              reference.withHolder(
+                  firstClassWithReferenceOrLibraryClass.getType(), appView.dexItemFactory()),
+              ComputedApiLevel.unknown());
+      if (minApiLevel.isGreaterThan(libraryIfaceApiLevel)) {
+        minApiLevel = libraryIfaceApiLevel;
+        foundClass = libraryInterface;
+      }
+    }
+    return Pair.create(foundClass, minApiLevel);
+  }
+
+  private static DexClass firstLibraryClassOrProgramClassWithDefinition(
+      AppInfoWithClassHierarchy appInfo, DexClass originalClass, DexMember<?, ?> reference) {
+    if (originalClass.isLibraryClass()) {
+      return originalClass;
+    }
+    WorkList<DexClass> workList = WorkList.newIdentityWorkList(originalClass);
+    while (workList.hasNext()) {
+      DexClass clazz = workList.next();
+      if (clazz.isLibraryClass()) {
+        return clazz;
+      } else if (clazz.lookupMember(reference) != null) {
+        return clazz;
+      } else if (clazz.getSuperType() != null) {
+        appInfo
+            .contextIndependentDefinitionForWithResolutionResult(clazz.getSuperType())
+            .forEachClassResolutionResult(workList::addIfNotSeen);
+      }
+    }
+    return null;
+  }
+
+  private static Set<DexClass> findAllFirstLibraryInterfacesOrProgramClassWithDefinition(
+      AppInfoWithClassHierarchy appInfo, DexClass originalClass, DexMember<?, ?> reference) {
+    Set<DexClass> interfaces = Sets.newLinkedHashSet();
+    WorkList<DexClass> workList = WorkList.newIdentityWorkList(originalClass);
+    while (workList.hasNext()) {
+      DexClass clazz = workList.next();
+      if (clazz.isLibraryClass()) {
+        if (clazz.isInterface()) {
+          interfaces.add(clazz);
+        }
+      } else if (clazz.lookupMember(reference) != null) {
+        return Collections.singleton(clazz);
+      } else {
+        clazz.forEachImmediateSupertype(
+            superType ->
+                appInfo
+                    .contextIndependentDefinitionForWithResolutionResult(superType)
+                    .forEachClassResolutionResult(workList::addIfNotSeen));
+      }
+    }
+    return interfaces;
+  }
+
+  /**
+   * A lot of functionality has already been outlined in androidx. The ordinary pattern for manual
+   * outlining is to create a class with the name ApiXXImpl where XX is the api level. This method
+   * will check the context to see if it matches this pattern in androidx and extract the api level
+   * for comparison with the computed api level.
+   */
+  public static boolean isOutlinedAtSameOrLowerLevel(
+      DexProgramClass clazz, ComputedApiLevel apiLevel) {
+    assert apiLevel.isKnownApiLevel();
+    if (!clazz.getType().getDescriptor().startsWith("Landroidx/")) {
+      return false;
+    }
+    String simpleName = clazz.getSimpleName();
+    int apiIndex = simpleName.indexOf("Api");
+    if (apiIndex < 0) {
+      return false;
+    }
+    int endApiIndex = apiIndex += 3;
+    int implIndex = simpleName.indexOf("Impl");
+    if (implIndex < 0 || implIndex < endApiIndex || (implIndex - endApiIndex) != 2) {
+      return false;
+    }
+    String apiLevelAsString = simpleName.substring(endApiIndex, implIndex);
+    if (!StringUtils.onlyContainsDigits(apiLevelAsString)) {
+      return false;
+    }
+    int apiLevelAsInt = Integer.parseInt(apiLevelAsString);
+    if (apiLevelAsInt < 10 || apiLevelAsInt > AndroidApiLevel.LATEST.getLevel()) {
+      return false;
+    }
+    return apiLevel.asKnownApiLevel().getApiLevel().getLevel() <= apiLevelAsInt;
+  }
+
+  public static boolean isApiLevelLessThanOrEqualToG(ComputedApiLevel apiLevel) {
+    return apiLevel.isKnownApiLevel()
+        && apiLevel.asKnownApiLevel().getApiLevel().isLessThanOrEqualTo(AndroidApiLevel.G);
+  }
 }
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 048a18e..6c80076 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2118,7 +2118,7 @@
    * Predicate to guard against the possible presence of a VM bug.
    *
    * <p>Note that if the compilation is not desugaring to a min-api or targeting DEX at a min-api,
-   * then the bug is assumed to be present as the CF output could be futher compiled to any target.
+   * then the bug is assumed to be present as the CF output could be further compiled to any target.
    */
   private boolean canHaveBugPresentUntil(AndroidApiLevel level) {
     if (desugarState.isOn() || isGeneratingDex()) {
@@ -2757,4 +2757,10 @@
   public boolean canHaveIssueWithInlinedMonitors() {
     return canHaveBugPresentUntil(AndroidApiLevel.N);
   }
+
+  // b/272725341. ART 11 and 12 re-introduced hard verification errors when unable to compute
+  // subtype relationship when no other verification issues exists in code.
+  public boolean canHaveVerifyErrorForUnknownUnusedReturnValue() {
+    return isGeneratingDex() && canHaveBugPresentUntil(AndroidApiLevel.T);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelManualOutlineWithUnknownReturnTypeTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelManualOutlineWithUnknownReturnTypeTest.java
index 8f921d3..e7be12b 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelManualOutlineWithUnknownReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelManualOutlineWithUnknownReturnTypeTest.java
@@ -4,10 +4,10 @@
 
 package com.android.tools.r8.apimodel;
 
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assume.assumeTrue;
 
-import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.TestParameters;
@@ -68,7 +68,7 @@
   }
 
   @Test
-  public void testD8() throws Exception {
+  public void testD8WithModeling() throws Exception {
     assumeTrue(parameters.isDexRuntime());
     testForD8(parameters.getBackend())
         .addProgramClasses(Main.class, ProgramClass.class, ManualOutline.class)
@@ -76,19 +76,30 @@
         .addLibraryClasses(LibraryClass.class, LibrarySub.class)
         .setMinApi(parameters.getApiLevel())
         .addOptionsModification(options -> options.apiModelingOptions().disableMissingApiModeling())
+        .apply(setMockApiLevelForClass(LibraryClass.class, AndroidApiLevel.B))
+        .apply(setMockApiLevelForClass(LibrarySub.class, getMockApiLevel()))
+        .compile()
+        .apply(this::setupRuntime)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("ProgramClass::print");
+  }
+
+  @Test
+  public void testD8NoModeling() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    boolean willHaveVerifyError =
+        (parameters.getDexRuntimeVersion().isDalvik()
+                || parameters.isDexRuntimeVersion(Version.V12_0_0))
+            && !addedToLibraryHere;
+    testForD8(parameters.getBackend())
+        .addProgramClasses(Main.class, ProgramClass.class, ManualOutline.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addLibraryClasses(LibraryClass.class, LibrarySub.class)
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(options -> options.apiModelingOptions().disableMissingApiModeling())
         .compile()
         .apply(this::setupRuntime)
         .run(parameters.getRuntime(), Main.class)
-        .apply(this::checkOutput);
-  }
-
-  private void checkOutput(SingleTestRunResult<?> runResult) {
-    // TODO(b/272725341): Potentially we can remove the verification error
-    boolean willHaveVerifyError =
-        (parameters.getDexRuntimeVersion().isDalvik()
-                || parameters.isDexRuntimeVersion(Version.V12_0_0))
-            && !addedToLibraryHere;
-    runResult
         .assertSuccessWithOutputLinesIf(!willHaveVerifyError, "ProgramClass::print")
         .assertFailureWithErrorThatThrowsIf(willHaveVerifyError, VerifyError.class);
   }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineWithUnknownReturnTypeTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineWithUnknownReturnTypeTest.java
index 72f2815..928e5da 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineWithUnknownReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineWithUnknownReturnTypeTest.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -124,14 +123,7 @@
   }
 
   private void checkOutput(SingleTestRunResult<?> runResult) {
-    // TODO(b/272725341): We should not cause verify error.
-    boolean willHaveVerifyError =
-        (parameters.getDexRuntimeVersion().isDalvik()
-                || parameters.isDexRuntimeVersion(Version.V12_0_0))
-            && !addedToLibraryHere;
-    runResult
-        .assertSuccessWithOutputLinesIf(!willHaveVerifyError, "ProgramClass::print")
-        .assertFailureWithErrorThatThrowsIf(willHaveVerifyError, VerifyError.class);
+    runResult.assertSuccessWithOutputLines("ProgramClass::print");
   }
 
   public static class LibraryClass {