From 665dc490c2a1bcaa1fa1cf6f0ea2396ce4b31863 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Tue, 10 Feb 2026 09:08:54 +0000 Subject: [PATCH] 8208752: Calling a deserialized Lambda might fail with ClassCastException 8374654: Inconsistent handling of lambda deserialization for Object method references on interfaces Reviewed-by: vromero, jlahoda --- .../com/sun/tools/javac/code/Types.java | 10 ++- .../sun/tools/javac/comp/LambdaToMethod.java | 41 +++++---- .../tools/javac/resources/compiler.properties | 5 +- .../LambdaSerializedClassCastException.java | 63 ++++++++++++++ .../LambdaSerializedClassCastException.out | 2 + ...bleObjectMethodReferencesOnInterfaces.java | 84 +++++++++++++++++++ ...ableObjectMethodReferencesOnInterfaces.out | 4 + .../lambda/SerializableObjectMethods.out | 4 +- 8 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.java create mode 100644 test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.out create mode 100644 test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.java create mode 100644 test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.out diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java index 3f3eb1f9623..539b1470a75 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java @@ -2858,13 +2858,17 @@ public class Types { hasSameArgs(t, erasure(s)) || hasSameArgs(erasure(t), s); } - public boolean overridesObjectMethod(TypeSymbol origin, Symbol msym) { + public Symbol overriddenObjectMethod(TypeSymbol origin, Symbol msym) { for (Symbol sym : syms.objectType.tsym.members().getSymbolsByName(msym.name)) { if (msym.overrides(sym, origin, Types.this, true)) { - return true; + return sym; } } - return false; + return null; + } + + public boolean overridesObjectMethod(TypeSymbol origin, Symbol msym) { + return overriddenObjectMethod(origin, msym) != null; } /** diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java index 7d0c5192039..40628709dfe 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/LambdaToMethod.java @@ -701,22 +701,24 @@ public class LambdaToMethod extends TreeTranslator { rs.resolveConstructor(null, attrEnv, ctype, TreeInfo.types(args), List.nil())); } - private void addDeserializationCase(MethodHandleSymbol refSym, Type targetType, MethodSymbol samSym, + private void addDeserializationCase(MethodHandleSymbol refSym, Type targetType, MethodSymbol samSym, Type samType, DiagnosticPosition pos, List staticArgs, MethodType indyType) { String functionalInterfaceClass = classSig(targetType); String functionalInterfaceMethodName = samSym.getSimpleName().toString(); String functionalInterfaceMethodSignature = typeSig(types.erasure(samSym.type)); - Symbol baseMethod = refSym.baseSymbol(); - Symbol origMethod = baseMethod.baseSymbol(); - if (baseMethod != origMethod && origMethod.owner == syms.objectType.tsym) { - //the implementation method is a java.lang.Object method transferred to an - //interface that does not declare it. Runtime will refer to this method as to - //a java.lang.Object method, so do the same: - refSym = ((MethodSymbol) origMethod).asHandle(); + if (refSym.enclClass().isInterface()) { + Symbol baseMethod = types.overriddenObjectMethod(refSym.enclClass(), refSym); + if (baseMethod != null) { + // The implementation method is a java.lang.Object method, runtime will resolve this method to + // a java.lang.Object method, so do the same. + // This case can be removed if JDK-8172817 is fixed. + refSym = ((MethodSymbol) baseMethod).asHandle(); + } } String implClass = classSig(types.erasure(refSym.owner.type)); String implMethodName = refSym.getQualifiedName().toString(); String implMethodSignature = typeSig(types.erasure(refSym.type)); + String instantiatedMethodType = typeSig(types.erasure(samType)); int implMethodKind = refSym.referenceKind(); JCExpression kindTest = eqTest(syms.intType, deserGetter("getImplMethodKind", syms.intType), @@ -730,13 +732,14 @@ public class LambdaToMethod extends TreeTranslator { ++i; } JCStatement stmt = make.If( - deserTest(deserTest(deserTest(deserTest(deserTest( - kindTest, - "getFunctionalInterfaceClass", functionalInterfaceClass), - "getFunctionalInterfaceMethodName", functionalInterfaceMethodName), - "getFunctionalInterfaceMethodSignature", functionalInterfaceMethodSignature), - "getImplClass", implClass), - "getImplMethodSignature", implMethodSignature), + deserTest(deserTest(deserTest(deserTest(deserTest(deserTest( + kindTest, + "getFunctionalInterfaceClass", functionalInterfaceClass), + "getFunctionalInterfaceMethodName", functionalInterfaceMethodName), + "getFunctionalInterfaceMethodSignature", functionalInterfaceMethodSignature), + "getImplClass", implClass), + "getImplMethodSignature", implMethodSignature), + "getInstantiatedMethodType", instantiatedMethodType), make.Return(makeIndyCall( pos, syms.lambdaMetafactory, @@ -756,7 +759,8 @@ public class LambdaToMethod extends TreeTranslator { implMethodKind, implClass, implMethodName, - implMethodSignature)); + implMethodSignature, + instantiatedMethodType)); } stmts.append(stmt); } @@ -818,10 +822,11 @@ public class LambdaToMethod extends TreeTranslator { List indy_args) { //determine the static bsm args MethodSymbol samSym = (MethodSymbol) types.findDescriptorSymbol(tree.target.tsym); + MethodType samType = typeToMethodType(tree.getDescriptorType(types)); List staticArgs = List.of( typeToMethodType(samSym.type), refSym.asHandle(), - typeToMethodType(tree.getDescriptorType(types))); + samType); //computed indy arg types ListBuffer indy_args_types = new ListBuffer<>(); @@ -885,7 +890,7 @@ public class LambdaToMethod extends TreeTranslator { int prevPos = make.pos; try { make.at(kInfo.clazz); - addDeserializationCase(refSym, tree.type, samSym, + addDeserializationCase(refSym, tree.type, samSym, samType, tree, staticArgs, indyType); } finally { make.at(prevPos); diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties index bb81916becb..915d7f8a8d8 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties @@ -1721,7 +1721,7 @@ compiler.note.mref.stat.1=\ alternate metafactory = {0}\n\ bridge method = {1} -# 0: string, 1: string, 2: string, 3: number, 4: string, 5: string, 6: string +# 0: string, 1: string, 2: string, 3: number, 4: string, 5: string, 6: string, 7: string compiler.note.lambda.deserialization.stat=\ Generating lambda deserialization\n\ functionalInterfaceClass: {0}\n\ @@ -1730,7 +1730,8 @@ compiler.note.lambda.deserialization.stat=\ implMethodKind: {3}\n\ implClass: {4}\n\ implMethodName: {5}\n\ - implMethodSignature: {6} + implMethodSignature: {6}\n\ + instantiatedMethodType: {7} compiler.note.note=\ Note:\u0020 diff --git a/test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.java b/test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.java new file mode 100644 index 00000000000..f63f4b64e79 --- /dev/null +++ b/test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, Alphabet LLC. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8208752 + * @summary NPE generating serializedLambdaName for nested lambda + * @compile/ref=LambdaSerializedClassCastException.out -XDrawDiagnostics --debug=dumpLambdaDeserializationStats LambdaSerializedClassCastException.java + * @run main LambdaSerializedClassCastException + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.function.Function; + +public class LambdaSerializedClassCastException { + + public static void main(String[] args) throws Exception { + + Function lambda1 = + (Function & Serializable) Object::toString; + Function lambda2 = + (Function & Serializable) Object::toString; + + Function deserial = serialDeserial(lambda2); + deserial.apply(new Object()); + } + + @SuppressWarnings("unchecked") + static T serialDeserial(T object) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(object); + } + try (ObjectInputStream ois = + new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + return (T) ois.readObject(); + } + } +} diff --git a/test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.out b/test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.out new file mode 100644 index 00000000000..4ad61e5e00e --- /dev/null +++ b/test/langtools/tools/javac/lambda/LambdaSerializedClassCastException.out @@ -0,0 +1,2 @@ +LambdaSerializedClassCastException.java:44:59: compiler.note.lambda.deserialization.stat: java/util/function/Function, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, toString, ()Ljava/lang/String;, (Ljava/lang/String;)Ljava/lang/String; +LambdaSerializedClassCastException.java:46:59: compiler.note.lambda.deserialization.stat: java/util/function/Function, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, toString, ()Ljava/lang/String;, (Ljava/lang/Object;)Ljava/lang/String; diff --git a/test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.java b/test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.java new file mode 100644 index 00000000000..18d732fb0dd --- /dev/null +++ b/test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Google LLC and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8374654 8208752 + * @summary test lambda deserialization for Object method references on interfaces + * @compile/ref=SerializableObjectMethodReferencesOnInterfaces.out -XDrawDiagnostics --debug=dumpLambdaDeserializationStats SerializableObjectMethodReferencesOnInterfaces.java + * @run main SerializableObjectMethodReferencesOnInterfaces + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +public class SerializableObjectMethodReferencesOnInterfaces { + + public static void main(String[] args) throws Exception { + new Test().run(); + } + + static class Test { + interface I1 extends Serializable {} + + interface I2 extends I1 { + @Override + public int hashCode(); + } + + interface F extends Serializable { + R apply(T t); + } + + enum E { + ONE + } + + void run() throws Exception { + F f1 = I1::hashCode; + F f2 = I2::hashCode; + F f3 = E::hashCode; + F f4 = Object::hashCode; + + serialDeserial(f1).apply(new I1() {}); + serialDeserial(f2).apply(new I2() {}); + serialDeserial(f3).apply(E.ONE); + serialDeserial(f4).apply(new Object()); + } + } + + @SuppressWarnings("unchecked") + static T serialDeserial(T object) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(object); + } + try (ObjectInputStream ois = + new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + return (T) ois.readObject(); + } + } +} diff --git a/test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.out b/test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.out new file mode 100644 index 00000000000..657235aa6fe --- /dev/null +++ b/test/langtools/tools/javac/lambda/SerializableObjectMethodReferencesOnInterfaces.out @@ -0,0 +1,4 @@ +SerializableObjectMethodReferencesOnInterfaces.java:61:33: compiler.note.lambda.deserialization.stat: SerializableObjectMethodReferencesOnInterfaces$Test$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, hashCode, ()I, (LSerializableObjectMethodReferencesOnInterfaces$Test$I1;)Ljava/lang/Integer; +SerializableObjectMethodReferencesOnInterfaces.java:62:33: compiler.note.lambda.deserialization.stat: SerializableObjectMethodReferencesOnInterfaces$Test$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, hashCode, ()I, (LSerializableObjectMethodReferencesOnInterfaces$Test$I2;)Ljava/lang/Integer; +SerializableObjectMethodReferencesOnInterfaces.java:63:32: compiler.note.lambda.deserialization.stat: SerializableObjectMethodReferencesOnInterfaces$Test$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Enum, hashCode, ()I, (LSerializableObjectMethodReferencesOnInterfaces$Test$E;)Ljava/lang/Integer; +SerializableObjectMethodReferencesOnInterfaces.java:64:37: compiler.note.lambda.deserialization.stat: SerializableObjectMethodReferencesOnInterfaces$Test$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, hashCode, ()I, (Ljava/lang/Object;)Ljava/lang/Integer; diff --git a/test/langtools/tools/javac/lambda/SerializableObjectMethods.out b/test/langtools/tools/javac/lambda/SerializableObjectMethods.out index d03cc145794..56caaabc6de 100644 --- a/test/langtools/tools/javac/lambda/SerializableObjectMethods.out +++ b/test/langtools/tools/javac/lambda/SerializableObjectMethods.out @@ -1,4 +1,4 @@ -SerializableObjectMethods.java:59:35: compiler.note.lambda.deserialization.stat: SerializableObjectMethods$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, hashCode, ()I -SerializableObjectMethods.java:60:35: compiler.note.lambda.deserialization.stat: SerializableObjectMethods$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 9, SerializableObjectMethods$I2, hashCode, ()I +SerializableObjectMethods.java:59:35: compiler.note.lambda.deserialization.stat: SerializableObjectMethods$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, hashCode, ()I, (LSerializableObjectMethods$I1;)Ljava/lang/Integer; +SerializableObjectMethods.java:60:35: compiler.note.lambda.deserialization.stat: SerializableObjectMethods$F, apply, (Ljava/lang/Object;)Ljava/lang/Object;, 5, java/lang/Object, hashCode, ()I, (LSerializableObjectMethods$I2;)Ljava/lang/Integer; - compiler.note.unchecked.filename: SerializableObjectMethods.java - compiler.note.unchecked.recompile