ACC_SYNTHETIC attribute is still lost if constant pool is retained
The original problem still persists and from looking at the code, this makes sense, too. Given is a Java 4 class file where the synthetic attribute (JVMS 4.7.8) is not set for a method but only the ACC_SYNTHETIC modifier. If this class is is copied via a new ClassWriter(classReader, 0)
then the introduced check
boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5;
if (useSyntheticAttribute && (access & Opcodes.ACC_SYNTHETIC) != (accessFlags & Opcodes.ACC_SYNTHETIC)) {
return false;
}
will not be triggered as the modifier is set in the class reader coming from the method's actual modifier that has the ACC_SYNTHETIC bit set even for this older class file. During the write, ASM will does again filter the modifier due to the class file being older then Java 5 but copies the reader's constant pool entry for the method where the Synthetic attribute is not contained.
You can of course argue that the original input class file was not spec-compliant to begin with but the JVM does create such class files in the Instrumentation API upon a retransformation. The javadoc mentions explicitly that attributes are lost for such reconstructed class files such that only the ACC_SYNTHETIC modifier is set.
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_4, Opcodes.ACC_ABSTRACT, "foo/Bar", null, "java/lang/Object", null);
classWriter.visitMethod(Opcodes.ACC_ABSTRACT | Opcodes.ACC_SYNTHETIC, "qux", "()V", null, null).visitEnd();
classWriter.visitEnd();
final byte[] bytes = classWriter.toByteArray();
ClassLoader classLoader = new ClassLoader(null) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals("foo.Bar")) {
return defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}
};
final Class<?> type = Class.forName("foo.Bar", false, classLoader);
Instrumentation inst = ... // Use Java agent.
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (classBeingRedefined == type) {
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter transform = new ClassWriter(classReader, 0);
classReader.accept(transform, 0);
return transform.toByteArray();
}
return null;
}
}, true);
inst.retransformClasses(type); // causes UnsupportedOperationException due to changed modifier
A working safe guard must therefore verify that the attribute is set in the class reader's constant pool and not look at the verifier. In return, if the written class file is of version Java 1.5 or newer, the class writer must also verify that the constant pool does not contain the Synthetic Attribute.
The same mistake is made when changing a class file from one version to the other:
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_5, Opcodes.ACC_ABSTRACT, "foo/Bar", null, "java/lang/Object", null);
classWriter.visitMethod(Opcodes.ACC_ABSTRACT | Opcodes.ACC_SYNTHETIC, "qux", "()V", null, null).visitEnd();
classWriter.visitEnd();
ClassReader classReader = new ClassReader(classWriter.toByteArray());
ClassWriter transform = new ClassWriter(classReader, 0);
classReader.accept(new ClassVisitor(Opcodes.ASM6, transform) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(Opcodes.V1_4, access, name, signature, superName, interfaces);
}
}, 0);
new ClassReader(transform.toByteArray()).accept(new ClassVisitor(Opcodes.ASM6) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if ((access & Opcodes.ACC_SYNTHETIC) == 0) {
throw new AssertionError("Expected to be synthetic: " + name + descriptor);
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}, 0);