From bfd2afe5adc315928fdedbfbe73049d8774400de Mon Sep 17 00:00:00 2001 From: Raffaello Giulietti Date: Thu, 18 Jan 2024 17:05:35 +0000 Subject: [PATCH] 8275338: Add JFR events for notable serialization situations Reviewed-by: rriggs, egahlin --- .../classes/java/io/ObjectStreamClass.java | 6 + .../SerializationMisdeclarationChecker.java | 277 ++++++++++++++ .../SerializationMisdeclarationEvent.java | 91 +++++ .../SerializationMisdeclarationEvent.java | 53 +++ .../jdk/jfr/internal/MirrorEvents.java | 2 + .../jfr/internal/instrument/JDKEvents.java | 1 + src/jdk.jfr/share/conf/jfr/default.jfc | 4 + src/jdk.jfr/share/conf/jfr/profile.jfc | 4 + .../TestSerializationMisdeclarationEvent.java | 342 ++++++++++++++++++ test/lib/jdk/test/lib/jfr/EventNames.java | 1 + 10 files changed, 781 insertions(+) create mode 100644 src/java.base/share/classes/java/io/SerializationMisdeclarationChecker.java create mode 100644 src/java.base/share/classes/jdk/internal/event/SerializationMisdeclarationEvent.java create mode 100644 src/jdk.jfr/share/classes/jdk/jfr/events/SerializationMisdeclarationEvent.java create mode 100644 test/jdk/jdk/jfr/event/io/TestSerializationMisdeclarationEvent.java diff --git a/src/java.base/share/classes/java/io/ObjectStreamClass.java b/src/java.base/share/classes/java/io/ObjectStreamClass.java index 4c171eca2ce..73bb162e843 100644 --- a/src/java.base/share/classes/java/io/ObjectStreamClass.java +++ b/src/java.base/share/classes/java/io/ObjectStreamClass.java @@ -55,6 +55,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.event.SerializationMisdeclarationEvent; import jdk.internal.misc.Unsafe; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.Reflection; @@ -459,6 +461,10 @@ public final class ObjectStreamClass implements Serializable { } } initialized = true; + + if (SerializationMisdeclarationEvent.enabled() && serializable) { + SerializationMisdeclarationChecker.checkMisdeclarations(cl); + } } /** diff --git a/src/java.base/share/classes/java/io/SerializationMisdeclarationChecker.java b/src/java.base/share/classes/java/io/SerializationMisdeclarationChecker.java new file mode 100644 index 00000000000..5eae83e80f7 --- /dev/null +++ b/src/java.base/share/classes/java/io/SerializationMisdeclarationChecker.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2023, 2024, Oracle 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.io; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; + +import static jdk.internal.event.SerializationMisdeclarationEvent.*; +import static java.lang.reflect.Modifier.*; + +final class SerializationMisdeclarationChecker { + + private static final String SUID_NAME = "serialVersionUID"; + private static final String SERIAL_PERSISTENT_FIELDS_NAME = "serialPersistentFields"; + private static final String WRITE_OBJECT_NAME = "writeObject"; + private static final String READ_OBJECT_NAME = "readObject"; + private static final String READ_OBJECT_NO_DATA_NAME = "readObjectNoData"; + private static final String WRITE_REPLACE_NAME = "writeReplace"; + private static final String READ_RESOLVE_NAME = "readResolve"; + + private static final Class[] WRITE_OBJECT_PARAM_TYPES = {ObjectOutputStream.class}; + private static final Class[] READ_OBJECT_PARAM_TYPES = {ObjectInputStream.class}; + + /* + * The sharing of a single Class[] instance here is just to avoid wasting + * space, and should not be considered as a conceptual sharing of types. + */ + private static final Class[] READ_OBJECT_NO_DATA_PARAM_TYPES = {}; + private static final Class[] WRITE_REPLACE_PARAM_TYPES = READ_OBJECT_NO_DATA_PARAM_TYPES; + private static final Class[] READ_RESOLVE_PARAM_TYPES = READ_OBJECT_NO_DATA_PARAM_TYPES; + + static void checkMisdeclarations(Class cl) { + checkSerialVersionUID(cl); + checkSerialPersistentFields(cl); + + checkPrivateMethod(cl, WRITE_OBJECT_NAME, + WRITE_OBJECT_PARAM_TYPES, Void.TYPE); + checkPrivateMethod(cl, READ_OBJECT_NAME, + READ_OBJECT_PARAM_TYPES, Void.TYPE); + checkPrivateMethod(cl, READ_OBJECT_NO_DATA_NAME, + READ_OBJECT_NO_DATA_PARAM_TYPES, Void.TYPE); + + checkAccessibleMethod(cl, WRITE_REPLACE_NAME, + WRITE_REPLACE_PARAM_TYPES, Object.class); + checkAccessibleMethod(cl, READ_RESOLVE_NAME, + READ_RESOLVE_PARAM_TYPES, Object.class); + } + + private static void checkSerialVersionUID(Class cl) { + Field f = privilegedDeclaredField(cl, SUID_NAME); + if (f == null) { + if (isOrdinaryClass(cl)) { + commitEvent(cl, SUID_NAME + " should be declared explicitly" + + " as a private static final long field"); + } + return; + } + if (cl.isEnum()) { + commitEvent(cl, SUID_NAME + " should not be declared in an enum class"); + } + if (!isPrivate(f)) { + commitEvent(cl, SUID_NAME + " should be private"); + } + if (!isStatic(f)) { + commitEvent(cl, SUID_NAME + " must be static"); + } + if (!isFinal(f)) { + commitEvent(cl, SUID_NAME + " must be final"); + } + if (f.getType() != Long.TYPE) { + commitEvent(cl, SUID_NAME + " must be of type long"); + } + } + + private static void checkSerialPersistentFields(Class cl) { + Field f = privilegedDeclaredField(cl, SERIAL_PERSISTENT_FIELDS_NAME); + if (f == null) { + return; + } + if (cl.isRecord()) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + + " should not be declared in a record class"); + } else if (cl.isEnum()) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + + " should not be declared in an enum class"); + } + if (!isPrivate(f)) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be private"); + } + if (!isStatic(f)) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be static"); + } + if (!isFinal(f)) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be final"); + } + if (f.getType() != ObjectStreamField[].class) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + + " should be of type ObjectStreamField[]"); + } + if (!isStatic(f)) { + return; + } + f.setAccessible(true); + Object spf = objectFromStatic(f); + if (spf == null) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " should be non-null"); + return; + } + if (!(spf instanceof ObjectStreamField[])) { + commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + + " must be an instance of ObjectStreamField[]"); + } + } + + private static void checkPrivateMethod(Class cl, + String name, Class[] paramTypes, Class retType) { + for (Method m : privilegedDeclaredMethods(cl)) { + if (m.getName().equals(name)) { + checkPrivateMethod(cl, m, paramTypes, retType); + } + } + } + + private static void checkPrivateMethod(Class cl, + Method m, Class[] paramTypes, Class retType) { + if (cl.isEnum()) { + commitEvent(cl, "method " + m + " should not be declared in an enum class"); + } else if (cl.isRecord()) { + commitEvent(cl, "method " + m + " should not be declared in a record class"); + } + if (!isPrivate(m)) { + commitEvent(cl, "method " + m + " must be private"); + } + if (isStatic(m)) { + commitEvent(cl, "method " + m + " must be non-static"); + } + if (m.getReturnType() != retType) { + commitEvent(cl, "method " + m + " must have return type " + retType); + } + if (!Arrays.equals(m.getParameterTypes(), paramTypes)) { + commitEvent(cl, "method " + m + " must have parameter types " + Arrays.toString(paramTypes)); + } + } + + private static void checkAccessibleMethod(Class cl, + String name, Class[] paramTypes, Class retType) { + for (Class superCl = cl; superCl != null; superCl = superCl.getSuperclass()) { + for (Method m : privilegedDeclaredMethods(superCl)) { + if (m.getName().equals(name)) { + checkAccessibleMethod(cl, superCl, m, paramTypes, retType); + } + } + } + } + + private static void checkAccessibleMethod(Class cl, + Class superCl, Method m, Class[] paramTypes, Class retType) { + if (superCl.isEnum()) { + commitEvent(cl, "method " + m + " should not be declared in an enum class"); + } + if (isAbstract(m)) { + commitEvent(cl, "method " + m + " must be non-abstract"); + } + if (isStatic(m)) { + commitEvent(cl, "method " + m + " must be non-static"); + } + if (m.getReturnType() != retType) { + commitEvent(cl, "method " + m + " must have return type " + retType); + } + if (!Arrays.equals(m.getParameterTypes(), paramTypes)) { + commitEvent(cl, "method " + m + " must have parameter types " + Arrays.toString(paramTypes)); + } + if (isPrivate(m) && cl != superCl + || isPackageProtected(m) && !isSamePackage(cl, superCl)) { + commitEvent(cl, "method " + m + " is not accessible"); + } + } + + private static boolean isSamePackage(Class cl0, Class cl1) { + return cl0.getClassLoader() == cl1.getClassLoader() + && cl0.getPackageName().equals(cl1.getPackageName()); + } + + private static boolean isOrdinaryClass(Class cl) { + /* class Enum and class Record are not considered ordinary classes */ + return !(cl.isRecord() || cl.isEnum() || cl.isArray() + || Enum.class == cl || Record.class == cl + || Proxy.isProxyClass(cl)); + } + + private static boolean isPrivate(Member m) { + return (m.getModifiers() & PRIVATE) != 0; + } + + private static boolean isPackageProtected(Member m) { + return (m.getModifiers() & (PRIVATE | PROTECTED | PUBLIC)) == 0; + } + + private static boolean isAbstract(Member m) { + return (m.getModifiers() & ABSTRACT) != 0; + } + + private static boolean isFinal(Member m) { + return (m.getModifiers() & FINAL) != 0; + } + + private static boolean isStatic(Member m) { + return (m.getModifiers() & STATIC) != 0; + } + + @SuppressWarnings("removal") + private static Field privilegedDeclaredField(Class cl, String name) { + if (System.getSecurityManager() == null) { + return declaredField(cl, name); + } + return AccessController.doPrivileged((PrivilegedAction) () -> + declaredField(cl, name)); + } + + private static Field declaredField(Class cl, String name) { + try { + return cl.getDeclaredField(name); + } catch (NoSuchFieldException ignored) { + } + return null; + } + + @SuppressWarnings("removal") + private static Method[] privilegedDeclaredMethods(Class cl) { + if (System.getSecurityManager() == null) { + return cl.getDeclaredMethods(); + } + return AccessController.doPrivileged( + (PrivilegedAction) cl::getDeclaredMethods); + } + + private static Object objectFromStatic(Field f) { + try { + return f.get(null); + } catch (IllegalAccessException ignored) { + } + return null; + } + + private static void commitEvent(Class cl, String msg) { + commit(timestamp(), cl, msg); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/event/SerializationMisdeclarationEvent.java b/src/java.base/share/classes/jdk/internal/event/SerializationMisdeclarationEvent.java new file mode 100644 index 00000000000..3ffdf4858e7 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/event/SerializationMisdeclarationEvent.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, Oracle 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.event; + +/** + * A JFR event for serialization misdeclarations. + * This event is mirrored in {@code jdk.jfr.events.SerializationMisdeclarationEvent} + * where the metadata for the event is provided with annotations. + * Some of the methods are replaced by generated methods when jfr is enabled. + * Note that the order of the arguments of the {@link #commit(long,Class,String)} + * method must be the same as the order of the fields. + */ +public class SerializationMisdeclarationEvent extends Event { + + public Class misdeclaredClass; + public String message; + + /** + * Commit a serialization misdeclaration event. + * The implementation of this method is generated automatically if jfr is enabled. + * The order of the fields must be the same as the parameters in this method. + * {@code commit(long,Class,String)} + * + * @param start timestamp of the start of the operation + * @param misdeclaredClass the affected class + * @param message the specific event message + */ + public static void commit(long start, Class misdeclaredClass, String message) { + // Generated by JFR + } + + /** + * Determine if an event should be emitted. The duration of the operation + * must exceed some threshold in order to commit the event. The implementation + * of this method is generated automatically if jfr is enabled. + * + * @param duration time in nanoseconds to complete the operation + * @return true if the event should be commited + */ + public static boolean shouldCommit(long duration) { + // Generated by JFR + return false; + } + + /** + * Determine if this kind of event is enabled. The implementation + * of this method is generated automatically if jfr is enabled. + * + * @return whether serialization misdeclaration events are enabled + */ + public static boolean enabled() { + // Generated by JFR + return false; + } + + /** + * Fetch the current timestamp in nanoseconds. This method is used + * to determine the start and end of an operation. The implementation + * of this method is generated automatically if jfr is enabled. + * + * @return the current timestamp value + */ + public static long timestamp() { + // Generated by JFR + return 0L; + } + +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/events/SerializationMisdeclarationEvent.java b/src/jdk.jfr/share/classes/jdk/jfr/events/SerializationMisdeclarationEvent.java new file mode 100644 index 00000000000..b88ba270708 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/events/SerializationMisdeclarationEvent.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023, Oracle 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.jfr.events; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.internal.MirrorEvent; +import jdk.jfr.internal.RemoveFields; +import jdk.jfr.internal.Type; + +@Name(Type.EVENT_NAME_PREFIX + "SerializationMisdeclaration") +@Label("Serialization Misdeclaration") +@Category({"Java Development Kit", "Serialization"}) +@Description("Methods and fields misdeclarations." + + " The checks are usually performed just once per serializable class," + + " the first time it is used by serialization." + + " Under high memory pressure, a class might be re-checked again.") +@MirrorEvent(className = "jdk.internal.event.SerializationMisdeclarationEvent") +@RemoveFields({"duration", "stackTrace", "eventThread"}) +public final class SerializationMisdeclarationEvent extends AbstractJDKEvent { + + @Label("Misdeclared Class") + public Class misdeclaredClass; + + @Label("Message") + public String message; + +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java index 62cf72279fc..7c74bb41f4a 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java @@ -35,6 +35,7 @@ import jdk.jfr.events.ExceptionThrownEvent; import jdk.jfr.events.ProcessStartEvent; import jdk.jfr.events.SecurityPropertyModificationEvent; import jdk.jfr.events.SecurityProviderServiceEvent; +import jdk.jfr.events.SerializationMisdeclarationEvent; import jdk.jfr.events.SocketReadEvent; import jdk.jfr.events.SocketWriteEvent; import jdk.jfr.events.TLSHandshakeEvent; @@ -52,6 +53,7 @@ public final class MirrorEvents { ProcessStartEvent.class, SecurityPropertyModificationEvent.class, SecurityProviderServiceEvent.class, + SerializationMisdeclarationEvent.class, SocketReadEvent.class, SocketWriteEvent.class, ThreadSleepEvent.class, diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/instrument/JDKEvents.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/instrument/JDKEvents.java index f074349e2b2..a7b3c67f2da 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/instrument/JDKEvents.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/instrument/JDKEvents.java @@ -75,6 +75,7 @@ public final class JDKEvents { jdk.internal.event.ProcessStartEvent.class, jdk.internal.event.SecurityPropertyModificationEvent.class, jdk.internal.event.SecurityProviderServiceEvent.class, + jdk.internal.event.SerializationMisdeclarationEvent.class, jdk.internal.event.SocketReadEvent.class, jdk.internal.event.SocketWriteEvent.class, jdk.internal.event.ThreadSleepEvent.class, diff --git a/src/jdk.jfr/share/conf/jfr/default.jfc b/src/jdk.jfr/share/conf/jfr/default.jfc index 346d7e4df5e..b9e1308cd21 100644 --- a/src/jdk.jfr/share/conf/jfr/default.jfc +++ b/src/jdk.jfr/share/conf/jfr/default.jfc @@ -756,6 +756,10 @@ true + + false + + true beginChunk diff --git a/src/jdk.jfr/share/conf/jfr/profile.jfc b/src/jdk.jfr/share/conf/jfr/profile.jfc index 6575455fc7a..ccc8dc6b661 100644 --- a/src/jdk.jfr/share/conf/jfr/profile.jfc +++ b/src/jdk.jfr/share/conf/jfr/profile.jfc @@ -756,6 +756,10 @@ true + + true + + true beginChunk diff --git a/test/jdk/jdk/jfr/event/io/TestSerializationMisdeclarationEvent.java b/test/jdk/jdk/jfr/event/io/TestSerializationMisdeclarationEvent.java new file mode 100644 index 00000000000..5036cb7b41c --- /dev/null +++ b/test/jdk/jdk/jfr/event/io/TestSerializationMisdeclarationEvent.java @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2023, 2024 Oracle 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. + */ + +package jdk.jfr.event.io; + +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingStream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.ObjectStreamField; +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static jdk.test.lib.jfr.EventNames.SerializationMisdeclaration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/* + * @test + * @bug 8275338 + * @summary Check generation of JFR events for misdeclared fields and methods + * relevant to serialization + * @key jfr + * @requires vm.hasJFR + * @library /test/lib + * @run junit/othervm jdk.jfr.event.io.TestSerializationMisdeclarationEvent + */ +public class TestSerializationMisdeclarationEvent { + + private static final List events = new ArrayList<>(); + + @BeforeAll + static void recordEvents() { + try (var rs = new RecordingStream()) { + rs.enable(SerializationMisdeclaration); + rs.onEvent(SerializationMisdeclaration, events::add); + rs.startAsync(); + doLookups(); + rs.stop(); + } + } + + static Arguments[] testSingleClassMisdeclarations() { + return new Arguments[] { + arguments(NoSUID.class, new String[] {"serialVersionUID", "should", "explicitly"}), + arguments(NoSUID.class, new String[] {"serialPersistentFields", "should", "non-null"}), + + arguments(BadClass.class, new String[] {"serialVersionUID", "should", "private"}), + arguments(BadClass.class, new String[] {"serialVersionUID", "must", "type", "long"}), + arguments(BadClass.class, new String[] {"serialVersionUID", "must", "final"}), + arguments(BadClass.class, new String[] {"serialVersionUID", "must", "static"}), + arguments(BadClass.class, new String[] {"serialPersistentFields", "must", "private"}), + arguments(BadClass.class, new String[] {"serialPersistentFields", "must", "static"}), + arguments(BadClass.class, new String[] {"serialPersistentFields", "must", "final"}), + arguments(BadClass.class, new String[] {"serialPersistentFields", "should", "type", "ObjectStreamField[]"}), + arguments(BadClass.class, new String[] {"method", "writeObject", "must", "private"}), + arguments(BadClass.class, new String[] {"method", "writeObject", "must", "non-static"}), + arguments(BadClass.class, new String[] {"method", "writeObject", "must", "return"}), + arguments(BadClass.class, new String[] {"method", "writeObject", "must", "parameter"}), + arguments(BadClass.class, new String[] {"method", "readObject(", "must", "parameter"}), + arguments(BadClass.class, new String[] {"method", "readObjectNoData", "must", "parameter"}), + + arguments(EnumClass.class, new String[] {"serialVersionUID", "enum"}), + arguments(EnumClass.class, new String[] {"serialPersistentFields", "enum"}), + arguments(EnumClass.class, new String[] {"method", "writeObject", "enum"}), + arguments(EnumClass.class, new String[] {"method", "readResolve", "enum"}), + + arguments(RecordClass.class, new String[] {"serialPersistentFields", "record"}), + arguments(RecordClass.class, new String[] {"method", "record"}), + + arguments(C.class, new String[] {"method", "not", "accessible"}), + + arguments(Acc.class, new String[] {"serialPersistentFields", "should", "type", "ObjectStreamField[]"}), + arguments(Acc.class, new String[] {"serialPersistentFields", "must", "instance", "ObjectStreamField[]"}), + arguments(Acc.class, new String[] {"method", "readResolve", "must", "non-abstract"}), + arguments(Acc.class, new String[] {"method", "writeReplace", "must", "non-static"}), + arguments(Acc.class, new String[] {"method", "writeReplace", "must", "return"}), + arguments(Acc.class, new String[] {"method", "writeReplace", "must", "parameter"}), + }; + } + + static Arguments[] testGoodClass() { + return new Arguments[] { + arguments(A.class), + arguments(B.class), + }; + } + + @ParameterizedTest + @MethodSource + public void testSingleClassMisdeclarations(Class cls, String[] keywords) { + singleClassEvent(cls, keywords); + } + + @ParameterizedTest + @MethodSource + public void testGoodClass(Class cls) { + assertEquals(0, getEventsFor(cls).size(), cls.getName()); + } + + private static void doLookups() { + ObjectStreamClass.lookup(NoSUID.class); + ObjectStreamClass.lookup(BadClass.class); + ObjectStreamClass.lookup(EnumClass.class); + ObjectStreamClass.lookup(RecordClass.class); + ObjectStreamClass.lookup(Acc.class); + + ObjectStreamClass.lookup(A.class); + ObjectStreamClass.lookup(B.class); + ObjectStreamClass.lookup(C.class); + } + + private static void singleClassEvent(Class cls, String[] keywords) { + assertEquals(1, getEventsFor(cls, keywords).size(), cls.getName()); + } + + private static List getEventsFor(Class cls, String[] keywords) { + return events.stream() + .filter(e -> e.getClass("misdeclaredClass").getName().equals(cls.getName()) + && matchesAllKeywords(e.getString("message"), keywords)) + .toList(); + } + + private static boolean matchesAllKeywords(String msg, String[] keywords) { + return Arrays.stream(keywords).allMatch(msg::contains); + } + + private static List getEventsFor(Class cls) { + return events.stream() + .filter(e -> e.getClass("misdeclaredClass").getName().equals(cls.getName())) + .toList(); + } + + private static class A implements Serializable { + + @Serial + private static final long serialVersionUID = 0xAAAAL; + + @Serial + private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; + + @Serial + private void writeObject(ObjectOutputStream oos) { + } + + @Serial + private void readObject(ObjectInputStream ois) { + } + + @Serial + private void readObjectNoData() { + } + + @Serial + Object writeReplace() { + return null; + } + + } + + private static class B extends A { + + @Serial + private static final long serialVersionUID = 0xBBBBL; + + @Serial + private Object readResolve() { + return null; + } + + } + + private static final class C extends B { + + @Serial + private static final long serialVersionUID = 0xCCCCL; + + /* + * readResolve() in superclass is not accessible + */ + + } + + private static final class NoSUID implements Serializable { + + /* + * should declare serialVersionUID + */ + + /* + * value should be non-null + */ + private static final ObjectStreamField[] serialPersistentFields = null; + + } + + private static final class BadClass implements Serializable { + /* + * should be private + * must be long + * must be final + */ + Object serialVersionUID = 1.2; + + /* + * must be private + * must be static + * must be final + * should be ObjectStreamField[] + */ + Object serialPersistentFields = new String[0]; + + /* + * must be private + * must be non-static + * must return void + * must accept ObjectOutputStream + */ + static int writeObject(int i) { + return 0; + } + + /* + * must accept ObjectInputStream + */ + private void readObject(ObjectOutputStream oos) { + } + + /* + * must not accept parameters + */ + private void readObjectNoData(ObjectInputStream ois) { + } + + } + + private enum EnumClass implements Serializable { + __; // ignored constant + + /* + * non-effective on enum + */ + private static final long serialVersionUID = 0xABCDL; + + /* + * non-effective on enum + */ + private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; + + /* + * non-effective on enum + */ + private void writeObject(ObjectOutputStream oos) { + } + + /* + * non-effective on enum + */ + public Object readResolve() { + return null; + } + + } + + private record RecordClass() implements Serializable { + + /* + * allowed on records + */ + private static final long serialVersionUID = 0x1234L; + + /* + * non-effective on records + */ + private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; + + /* + * non-effective on records + */ + static int writeObject(int i) { + return 0; + } + + } + + private abstract static class Acc implements Serializable { + + private static final long serialVersionUID = 0x5678L; + + private static final Object serialPersistentFields = new String[0]; + /* + * must be non-abstract + */ + abstract Object readResolve(); + + /* + * must be non-static + */ + static Object writeReplace() { + return null; + } + + /* + * must return Object + * must have empty parameter types + */ + String writeReplace(String s) { + return null; + } + + } + +} diff --git a/test/lib/jdk/test/lib/jfr/EventNames.java b/test/lib/jdk/test/lib/jfr/EventNames.java index 0d6afe53daf..32bffc8dda7 100644 --- a/test/lib/jdk/test/lib/jfr/EventNames.java +++ b/test/lib/jdk/test/lib/jfr/EventNames.java @@ -209,6 +209,7 @@ public class EventNames { public static final String SecurityProviderService = PREFIX + "SecurityProviderService"; public static final String DirectBufferStatistics = PREFIX + "DirectBufferStatistics"; public static final String Deserialization = PREFIX + "Deserialization"; + public static final String SerializationMisdeclaration = PREFIX + "SerializationMisdeclaration"; public static final String VirtualThreadStart = PREFIX + "VirtualThreadStart"; public static final String VirtualThreadEnd = PREFIX + "VirtualThreadEnd"; public static final String VirtualThreadPinned = PREFIX + "VirtualThreadPinned";