Update synthetic markers to include a versioning hash.

This renames the synthetic marker too such that it is not visible to
any previous version of the compiler. Going forward any marker with a
non-matching versioning hash is likwise ignored.

Bug: b/227317456
Change-Id: I87e63aba272c3965d259fffdaa23b01039e04a4c
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index 5a2bffc..9f223c7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -390,9 +390,13 @@
 
   public static DexAnnotation createAnnotationSynthesizedClass(
       SyntheticKind kind, DexItemFactory dexItemFactory) {
+    DexString versionHash =
+        dexItemFactory.createString(dexItemFactory.getSyntheticNaming().getVersionHash());
     DexAnnotationElement kindElement =
         new DexAnnotationElement(dexItemFactory.kindString, DexValueInt.create(kind.getId()));
-    DexAnnotationElement[] elements = new DexAnnotationElement[] {kindElement};
+    DexAnnotationElement versionHashElement =
+        new DexAnnotationElement(dexItemFactory.versionHashString, new DexValueString(versionHash));
+    DexAnnotationElement[] elements = new DexAnnotationElement[] {kindElement, versionHashElement};
     return new DexAnnotation(
         VISIBILITY_BUILD,
         new DexEncodedAnnotation(dexItemFactory.annotationSynthesizedClass, elements));
@@ -413,17 +417,28 @@
       return null;
     }
     int length = annotation.annotation.elements.length;
-    if (length != 1) {
+    if (length != 2) {
       return null;
     }
-    assert factory.kindString.isLessThan(factory.valueString);
+    assert factory.kindString.isLessThan(factory.versionHashString);
     DexAnnotationElement kindElement = annotation.annotation.elements[0];
+    DexAnnotationElement versionHashElement = annotation.annotation.elements[1];
     if (kindElement.name != factory.kindString) {
       return null;
     }
     if (!kindElement.value.isDexValueInt()) {
       return null;
     }
+    if (versionHashElement.name != factory.versionHashString) {
+      return null;
+    }
+    if (!versionHashElement.value.isDexValueString()) {
+      return null;
+    }
+    String currentVersionHash = synthetics.getNaming().getVersionHash();
+    if (!currentVersionHash.equals(versionHashElement.value.asDexValueString().toString())) {
+      return null;
+    }
     SyntheticKind kind =
         synthetics.getNaming().fromId(kindElement.value.asDexValueInt().getValue());
     return kind;
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 3e5150c..98dac6d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -338,6 +338,7 @@
 
   public final DexString valueString = createString("value");
   public final DexString kindString = createString("kind");
+  public final DexString versionHashString = createString("versionHash");
 
   // Prefix for runtime affecting yet potential class-retained annotations.
   public final DexString dalvikAnnotationOptimizationPrefix =
@@ -643,7 +644,7 @@
       createStaticallyKnownType("Ldalvik/annotation/SourceDebugExtension;");
   public final DexType annotationThrows = createStaticallyKnownType("Ldalvik/annotation/Throws;");
   public final DexType annotationSynthesizedClass =
-      createStaticallyKnownType("Lcom/android/tools/r8/annotations/SynthesizedClass;");
+      createStaticallyKnownType("Lcom/android/tools/r8/annotations/SynthesizedClassV2;");
   public final DexType annotationCovariantReturnType =
       createStaticallyKnownType("Ldalvik/annotation/codegen/CovariantReturnType;");
   public final DexType annotationCovariantReturnTypes =
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
index ceb9a37..67dbe6a 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import java.nio.charset.StandardCharsets;
 import org.objectweb.asm.Attribute;
 import org.objectweb.asm.ByteVector;
 import org.objectweb.asm.ClassReader;
@@ -23,33 +24,51 @@
 public class SyntheticMarker {
 
   private static final String SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME =
-      "com.android.tools.r8.SynthesizedClass";
+      "com.android.tools.r8.SynthesizedClassV2";
 
   public static Attribute getMarkerAttributePrototype(SyntheticNaming syntheticNaming) {
-    return new MarkerAttribute(null, syntheticNaming);
+    return new MarkerAttribute(null, null, syntheticNaming);
   }
 
   public static void writeMarkerAttribute(
       ClassWriter writer, SyntheticKind kind, SyntheticItems syntheticItems) {
-    writer.visitAttribute(new MarkerAttribute(kind, syntheticItems.getNaming()));
+    SyntheticNaming naming = syntheticItems.getNaming();
+    writer.visitAttribute(new MarkerAttribute(kind, naming.getVersionHash(), naming));
   }
 
   public static SyntheticMarker readMarkerAttribute(Attribute attribute) {
     if (attribute instanceof MarkerAttribute) {
       MarkerAttribute marker = (MarkerAttribute) attribute;
-      return new SyntheticMarker(marker.kind, null);
+      if (marker.versionHash.equals(marker.syntheticNaming.getVersionHash())) {
+        return new SyntheticMarker(marker.kind, null);
+      }
     }
     return null;
   }
 
+  /**
+   * CF attribute for marking synthetic classes.
+   *
+   * <p>The attribute name is defined by {@code SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME}. The format of
+   * the attribute payload is
+   *
+   * <pre>
+   *   u2 syntheticKindId
+   *   u2 versionHashLength
+   *   u1[versionHashLength] versionHashBytes
+   * </pre>
+   */
   private static class MarkerAttribute extends Attribute {
 
-    private SyntheticKind kind;
+    private final SyntheticKind kind;
+    private final String versionHash;
     private final SyntheticNaming syntheticNaming;
 
-    public MarkerAttribute(SyntheticKind kind, SyntheticNaming syntheticNaming) {
+    public MarkerAttribute(
+        SyntheticKind kind, String versionHash, SyntheticNaming syntheticNaming) {
       super(SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME);
       this.kind = kind;
+      this.versionHash = versionHash;
       this.syntheticNaming = syntheticNaming;
     }
 
@@ -61,18 +80,29 @@
         char[] charBuffer,
         int codeAttributeOffset,
         Label[] labels) {
-      short id = classReader.readShort(offset);
-      assert id >= 0;
-      SyntheticKind kind = syntheticNaming.fromId(id);
-      return new MarkerAttribute(kind, syntheticNaming);
+      short syntheticKindId = classReader.readShort(offset);
+      offset += 2;
+      short versionHashLength = classReader.readShort(offset);
+      offset += 2;
+      byte[] versionHashBytes = new byte[versionHashLength];
+      for (int i = 0; i < versionHashLength; i++) {
+        versionHashBytes[i] = (byte) classReader.readByte(offset++);
+      }
+      assert syntheticKindId >= 0;
+      SyntheticKind kind = syntheticNaming.fromId(syntheticKindId);
+      String versionHash = new String(versionHashBytes, StandardCharsets.UTF_8);
+      return new MarkerAttribute(kind, versionHash, syntheticNaming);
     }
 
     @Override
     protected ByteVector write(
         ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
-      ByteVector byteVector = new ByteVector();
       assert 0 <= kind.getId() && kind.getId() <= Short.MAX_VALUE;
+      ByteVector byteVector = new ByteVector();
       byteVector.putShort(kind.getId());
+      byte[] versionHashBytes = versionHash.getBytes(StandardCharsets.UTF_8);
+      byteVector.putShort(versionHashBytes.length);
+      byteVector.putByteArray(versionHashBytes, 0, versionHashBytes.length);
       return byteVector;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index edc591e..e263786 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.synthesis;
 
+import com.android.tools.r8.Version;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -12,16 +13,17 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.structural.Equatable;
 import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 import it.unimi.dsi.fastutil.ints.IntArraySet;
 import it.unimi.dsi.fastutil.ints.IntSet;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
 public class SyntheticNaming {
 
-  public SyntheticNaming() {}
-
   private KindGenerator generator = new KindGenerator();
 
   // Global synthetics.
@@ -87,7 +89,19 @@
   public final SyntheticKind ARRAY_CONVERSION = generator.forSingleMethod(37, "$ArrayConversion");
   public final SyntheticKind API_MODEL_OUTLINE = generator.forSingleMethod(32, "ApiModelOutline");
 
-  private final List<SyntheticKind> ALL_KINDS = generator.getAllKinds();
+  private final String versionHash;
+  private final List<SyntheticKind> ALL_KINDS;
+
+  public SyntheticNaming() {
+    generator.hasher.putString(Version.getVersionString(), StandardCharsets.UTF_8);
+    versionHash = generator.hasher.hash().toString();
+    ALL_KINDS = generator.getAllKinds();
+    generator = null;
+  }
+
+  public String getVersionHash() {
+    return versionHash;
+  }
 
   public Collection<SyntheticKind> kinds() {
     return ALL_KINDS;
@@ -105,11 +119,13 @@
   private static class KindGenerator {
     private List<SyntheticKind> kinds = new ArrayList<>();
     private IntSet usedIds = new IntArraySet();
+    private Hasher hasher = Hashing.sha256().newHasher();
 
     private SyntheticKind register(SyntheticKind kind) {
       if (!usedIds.add(kind.getId())) {
         throw new Unreachable("Invalid reuse of synthetic kind id: " + kind.getId());
       }
+      kind.hash(hasher);
       kinds.add(kind);
       return kind;
     }
@@ -198,6 +214,13 @@
 
     public abstract boolean isMayOverridesNonProgramType();
 
+    public final void hash(Hasher hasher) {
+      hasher.putInt(getId());
+      hasher.putString(getDescriptor(), StandardCharsets.UTF_8);
+      internalHash(hasher);
+    }
+
+    public abstract void internalHash(Hasher hasher);
   }
 
   private static class SyntheticMethodKind extends SyntheticKind {
@@ -232,6 +255,10 @@
       return false;
     }
 
+    @Override
+    public void internalHash(Hasher hasher) {
+      hasher.putString("method", StandardCharsets.UTF_8);
+    }
   }
 
   private static class SyntheticClassKind extends SyntheticKind {
@@ -269,6 +296,11 @@
       return false;
     }
 
+    @Override
+    public void internalHash(Hasher hasher) {
+      hasher.putString("class", StandardCharsets.UTF_8);
+      hasher.putBoolean(sharable);
+    }
   }
 
   private static class SyntheticFixedClassKind extends SyntheticClassKind {
@@ -299,6 +331,11 @@
       return mayOverridesNonProgramType;
     }
 
+    @Override
+    public void internalHash(Hasher hasher) {
+      hasher.putString(isGlobal() ? "global" : "fixed", StandardCharsets.UTF_8);
+      hasher.putBoolean(mayOverridesNonProgramType);
+    }
   }
 
   private static final String SYNTHETIC_CLASS_SEPARATOR = "$$";
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
new file mode 100644
index 0000000..706ace9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
@@ -0,0 +1,261 @@
+// 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.synthesis;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ByteVector;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+
+@RunWith(Parameterized.class)
+public class SyntheticMarkerCfTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDefaultCfRuntime()
+        .withApiLevel(AndroidApiLevel.B)
+        .enableApiLevelsForCf()
+        .build();
+  }
+
+  public SyntheticMarkerCfTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  /**
+   * Mirror of the initial D8 synthetic marker format.
+   *
+   * <p>The legacy marker just had the synthetic kind id as payload.
+   */
+  private static class SyntheticMarkerV1 extends Attribute {
+    static final SyntheticMarkerV1 PROTO = new SyntheticMarkerV1((short) 0);
+
+    final short kindId;
+
+    public SyntheticMarkerV1(short kindId) {
+      super("com.android.tools.r8.SynthesizedClass");
+      this.kindId = kindId;
+    }
+
+    @Override
+    protected Attribute read(
+        ClassReader classReader,
+        int offset,
+        int length,
+        char[] charBuffer,
+        int codeAttributeOffset,
+        Label[] labels) {
+      short id = classReader.readShort(offset);
+      return new SyntheticMarkerV1(id);
+    }
+
+    @Override
+    protected ByteVector write(
+        ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+      ByteVector byteVector = new ByteVector();
+      byteVector.putShort(kindId);
+      return byteVector;
+    }
+  }
+
+  /**
+   * Format of the current synthetic marker.
+   *
+   * <p>The marker is distinguished by a new attribute type name.
+   *
+   * <p>The payload is the kind id, version hash length and then version hash bytes.
+   */
+  private static class SyntheticMarkerV2 extends Attribute {
+    static final SyntheticMarkerV2 PROTO = new SyntheticMarkerV2((short) 0, null);
+
+    final short kindId;
+    final byte[] versionHash;
+
+    public SyntheticMarkerV2(short kindId, byte[] versionHash) {
+      super("com.android.tools.r8.SynthesizedClassV2");
+      this.versionHash = versionHash;
+      this.kindId = kindId;
+    }
+
+    @Override
+    protected Attribute read(
+        ClassReader classReader,
+        int offset,
+        int length,
+        char[] charBuffer,
+        int codeAttributeOffset,
+        Label[] labels) {
+      short kindId = classReader.readShort(offset);
+      offset += 2;
+      short versionLength = classReader.readShort(offset);
+      offset += 2;
+      byte[] versionBytes = new byte[versionLength];
+      for (int i = 0; i < versionLength; i++) {
+        versionBytes[i] = (byte) classReader.readByte(offset++);
+      }
+      return new SyntheticMarkerV2(kindId, versionBytes);
+    }
+
+    @Override
+    protected ByteVector write(
+        ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+      ByteVector byteVector = new ByteVector();
+      byteVector.putShort(kindId);
+      byteVector.putShort(versionHash.length);
+      byteVector.putByteArray(versionHash, 0, versionHash.length);
+      return byteVector;
+    }
+  }
+
+  private static List<Attribute> readAttributes(byte[] bytes) {
+    List<Attribute> attributes = new ArrayList<>();
+    ClassReader reader = new ClassReader(bytes);
+    reader.accept(
+        new ClassVisitor(InternalOptions.ASM_VERSION) {
+          @Override
+          public void visit(
+              int version,
+              int access,
+              String name,
+              String signature,
+              String superName,
+              String[] interfaces) {
+            super.visit(version, access, name, signature, superName, interfaces);
+          }
+
+          @Override
+          public void visitAttribute(Attribute attribute) {
+            attributes.add(attribute);
+          }
+        },
+        new Attribute[] {SyntheticMarkerV1.PROTO, SyntheticMarkerV2.PROTO},
+        ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
+    return attributes;
+  }
+
+  private byte[] getTestClassWithMarker(Attribute marker) throws IOException {
+    return transformer(TestClass.class)
+        .addClassTransformer(
+            new ClassTransformer() {
+              @Override
+              public void visit(
+                  int version,
+                  int access,
+                  String name,
+                  String signature,
+                  String superName,
+                  String[] interfaces) {
+                super.visit(version, access, name, signature, superName, interfaces);
+                super.visitAttribute(marker);
+              }
+            })
+        .transform();
+  }
+
+  /** Test that reads the correct marker from a compilation unit and fails if then manipulated. */
+  @Test
+  public void testInvalidMarkerFailsCompilation() throws Exception {
+    Box<SyntheticMarkerV2> currentCompilerMarker = new Box<>();
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .setIntermediate(true)
+        .setProgramConsumer(
+            new ClassFileConsumer() {
+              private final List<Attribute> attributes = new ArrayList<>();
+
+              @Override
+              public void accept(ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+                attributes.addAll(readAttributes(data.copyByteData()));
+              }
+
+              @Override
+              public void finished(DiagnosticsHandler handler) {
+                assertEquals(1, attributes.size());
+                assertEquals(SyntheticMarkerV2.PROTO.type, attributes.get(0).type);
+                currentCompilerMarker.set((SyntheticMarkerV2) attributes.get(0));
+              }
+            })
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+
+    // Test that if a "current" marker with invalid content is given to the compiler it will
+    // cause a failure. This test ensures that we can witness the markers being ignored in the
+    // tests below.
+    D8TestBuilder builder =
+        testForD8(parameters.getBackend())
+            .setMinApi(parameters.getApiLevel())
+            .addProgramClassFileData(
+                getTestClassWithMarker(
+                    new SyntheticMarkerV2(
+                        Short.MAX_VALUE, currentCompilerMarker.get().versionHash)));
+    assertThrows(CompilationFailedException.class, builder::compile);
+  }
+
+  @Test
+  public void testIgnoreV1Markers() throws Exception {
+    // Test that inputs with a legacy marker will be ignored.
+    // We do so by injecting an old marker and put in a non-valid ID which would cause the compiler
+    // to fail if it was read.
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClassFileData(getTestClassWithMarker(new SyntheticMarkerV1(Short.MAX_VALUE)))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testIgnorePreviousV2Markers() throws Exception {
+    // Test that inputs with a V2 marker from a previous compiler version are ignored.
+    // We do so by injecting an old marker and put in a non-valid ID which would cause the compiler
+    // to fail if it was read.
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClassFileData(
+            getTestClassWithMarker(new SyntheticMarkerV2(Short.MAX_VALUE, new byte[0])))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  public static class TestClass {
+
+    public static void run(Runnable r) {
+      r.run();
+    }
+
+    public static void main(String[] args) {
+      run(() -> System.out.println("Hello, world"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerDexTest.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerDexTest.java
new file mode 100644
index 0000000..5b00775
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerDexTest.java
@@ -0,0 +1,89 @@
+// 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.synthesis;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexEncodedAnnotation;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SyntheticMarkerDexTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public SyntheticMarkerDexTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    Path out =
+        testForD8(parameters.getBackend())
+            .setMinApi(parameters.getApiLevel())
+            .setIntermediate(true)
+            .addProgramClasses(TestClass.class)
+            .compile()
+            .inspect(this::checkSyntheticClassIsMarked)
+            .writeToZip();
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters.getApiLevel())
+        .addProgramFiles(out)
+        // Use intermediate again to preserve synthetics.
+        .setIntermediate(true)
+        // Disable desugaring so we are sure the lambda synthetic is created in the first round.
+        .disableDesugaring()
+        .compile()
+        .inspect(this::checkSyntheticClassIsMarked)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  private void checkSyntheticClassIsMarked(CodeInspector inspector) {
+    // Compilation gives rise to the main class plus one lambda.
+    assertEquals(2, inspector.allClasses().size());
+    inspector.forAllClasses(
+        clazz -> {
+          if (!clazz.getFinalReference().equals(Reference.classFromClass(TestClass.class))) {
+            // This should be the lambda class.
+            DexAnnotation[] annotations = clazz.getDexProgramClass().annotations().annotations;
+            assertEquals(1, annotations.length);
+            DexEncodedAnnotation annotation = annotations[0].annotation;
+            assertEquals(2, annotation.elements.length);
+            assertEquals(
+                "com.android.tools.r8.annotations.SynthesizedClassV2",
+                annotation.type.toSourceString());
+          }
+        });
+  }
+
+  public static class TestClass {
+
+    public static void run(Runnable r) {
+      r.run();
+    }
+
+    public static void main(String[] args) {
+      run(() -> System.out.println("Hello, world"));
+    }
+  }
+}