Check for try catch ranges when verifying CF frames
Bug: b/227982754
Bug: b/228141844
Change-Id: Ic871e255a04a05c32e9025010a316d84b442fe74
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerificationHelper.java b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerificationHelper.java
index 0d466f0..c81abe4 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerificationHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerificationHelper.java
@@ -174,16 +174,6 @@
for (CfTryCatch tryCatchRange : tryCatchRanges) {
if (tryCatchRange.start == label) {
currentCatchRanges.add(tryCatchRange);
- // We can have fall-through into this range requiring the current frame being
- // assignable to the handler frame. This is handled for locals when we see a throwing
- // instruction, but we can validate here that the stack will be a single element stack
- // [throwable].
- CfFrame destinationFrame = stateMap.get(tryCatchRange.start);
- if (destinationFrame == null) {
- throw CfCodeStackMapValidatingException.error("No frame for target catch range target");
- }
- checkStackIsAssignable(
- destinationFrame.getStack(), throwStack, factory, isJavaAssignable);
}
}
currentCatchRanges.removeIf(currentRange -> currentRange.end == label);
@@ -191,6 +181,35 @@
return this;
}
+ public void checkTryCatchRange(CfTryCatch tryCatchRange) {
+ // According to the spec:
+ // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.1
+ // saying ` and the handler's target (the initial instruction of the handler code) is type
+ // safe assuming an incoming type state T. The type state T is derived from ExcStackFrame
+ // by replacing the operand stack with a stack whose sole element is the handler's
+ // exception class.
+ tryCatchRange.targets.forEach(
+ target -> {
+ CfFrame destinationFrame = stateMap.get(target);
+ if (destinationFrame == null) {
+ throw CfCodeStackMapValidatingException.error("No frame for target catch range target");
+ }
+ // From the spec: the handler's exception class is assignable to the class Throwable.
+ tryCatchRange.guards.forEach(
+ guard -> {
+ if (!isJavaAssignable.test(guard, factory.throwableType)) {
+ throw CfCodeStackMapValidatingException.error(
+ "Could not assign '" + guard.toSourceString() + "' to throwable.");
+ }
+ checkStackIsAssignable(
+ ImmutableDeque.of(FrameType.initialized(guard)),
+ destinationFrame.getStack(),
+ factory,
+ isJavaAssignable);
+ });
+ });
+ }
+
private void checkFrameIsSet() {
if (currentFrame == NO_FRAME) {
throw CfCodeStackMapValidatingException.error("Unexpected state change");
@@ -218,9 +237,6 @@
if (destinationFrame == null) {
throw CfCodeStackMapValidatingException.error("No frame for target catch range target");
}
- // We have to check all current handler targets have assignable locals and a 1-element
- // stack assignable to throwable. It is not required that the the thrown error is
- // handled.
checkLocalsIsAssignable(
currentFrame.getLocals(), destinationFrame.getLocals(), factory, isJavaAssignable);
}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
index 9e3b1ee..3df1eba 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
@@ -100,6 +100,6 @@
DexMethod context,
AppView<?> appView,
DexItemFactory dexItemFactory) {
- // This is a no-op.
+ frameBuilder.seenLabel(this);
}
}
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 910d5f5..4974a5d 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -963,6 +963,16 @@
isAssignablePredicate(appView),
appView.dexItemFactory(),
maxStack);
+ for (CfTryCatch tryCatchRange : tryCatchRanges) {
+ try {
+ builder.checkTryCatchRange(tryCatchRange);
+ } catch (CfCodeStackMapValidatingException ex) {
+ return reportStackMapError(
+ CfCodeStackMapValidatingException.invalidTryCatchRange(
+ method, tryCatchRange, ex.getMessage(), appView),
+ appView);
+ }
+ }
if (stateMap.containsKey(null)) {
assert !shouldComputeInitialFrame();
builder.checkFrameAndSet(stateMap.get(null));
@@ -989,7 +999,7 @@
instruction.evaluate(builder, previousMethodSignature, appView, appView.dexItemFactory());
} catch (CfCodeStackMapValidatingException ex) {
return reportStackMapError(
- CfCodeStackMapValidatingException.toDiagnostics(
+ CfCodeStackMapValidatingException.invalidStackMapForInstruction(
method, i, instruction, ex.getMessage(), appView),
appView);
}
diff --git a/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java b/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java
index 1a25ce6..4b417bf 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java
@@ -5,6 +5,8 @@
package com.android.tools.r8.graph;
import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.utils.StringUtils;
public class CfCodeStackMapValidatingException extends RuntimeException {
@@ -52,7 +54,24 @@
sb.toString());
}
- public static CfCodeDiagnostics toDiagnostics(
+ public static CfCodeDiagnostics invalidTryCatchRange(
+ ProgramMethod method, CfTryCatch tryCatch, String detailMessage, AppView<?> appView) {
+ StringBuilder sb =
+ new StringBuilder("Invalid try catch range for ")
+ .append(StringUtils.join(", ", tryCatch.guards, DexType::getTypeName))
+ .append(": ")
+ .append(detailMessage)
+ .append(".");
+ if (appView.enableWholeProgramOptimizations()) {
+ sb.append(" In later version of R8, the method may be assumed not reachable.");
+ }
+ return new CfCodeDiagnostics(
+ method.getOrigin(),
+ appView.graphLens().getOriginalMethodSignature(method.getReference()),
+ sb.toString());
+ }
+
+ public static CfCodeDiagnostics invalidStackMapForInstruction(
ProgramMethod method,
int instructionIndex,
CfInstruction instruction,
diff --git a/src/test/java/com/android/tools/r8/cf/stackmap/SharedExceptionTypeTest.java b/src/test/java/com/android/tools/r8/cf/stackmap/SharedExceptionTypeTest.java
new file mode 100644
index 0000000..1f30f40
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/stackmap/SharedExceptionTypeTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2022, 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.cf.stackmap;
+
+import static org.junit.Assume.assumeTrue;
+
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SharedExceptionTypeTest extends TestBase {
+
+ private final TestParameters parameters;
+ private final String EXPECTED = "Hello World!";
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().build();
+ }
+
+ public SharedExceptionTypeTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testJvm() throws Exception {
+ assumeTrue(parameters.isCfRuntime());
+ testForJvm()
+ .addInnerClasses(SharedExceptionTypeTest.class)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines(EXPECTED);
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8(parameters.getBackend())
+ .addInnerClasses(SharedExceptionTypeTest.class)
+ .setMinApi(AndroidApiLevel.B)
+ .addOptionsModification(options -> options.testing.readInputStackMaps = true)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines(EXPECTED);
+ }
+
+ public static class ExceptionSuper extends RuntimeException {
+
+ public void printError() {
+ System.out.println("Hello World!");
+ }
+ }
+
+ public static class ExceptionA extends ExceptionSuper {}
+
+ public static class ExceptionB extends ExceptionSuper {}
+
+ public static class Main {
+
+ public static void foo(String[] args) {
+ if (args.length == 0) {
+ throw new ExceptionA();
+ } else if (args.length == 1) {
+ throw new ExceptionB();
+ } else {
+ throw new RuntimeException("FOO BAR");
+ }
+ }
+
+ public static void main(String[] args) {
+ try {
+ foo(args);
+ } catch (ExceptionA | ExceptionB e) {
+ e.printError();
+ }
+ }
+ }
+}