1 /* 2 * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /* 25 * @test 26 * @bug 8246774 27 * @summary InvalidClassException is thrown when the canonical constructor 28 * cannot be found during deserialization. 29 * @library /test/lib 30 * @run junit BadCanonicalCtrTest 31 */ 32 33 import java.io.ByteArrayInputStream; 34 import java.io.ByteArrayOutputStream; 35 import java.io.IOException; 36 import java.io.InvalidClassException; 37 import java.io.ObjectInputStream; 38 import java.io.ObjectOutputStream; 39 import java.io.ObjectStreamClass; 40 import java.lang.classfile.ClassTransform; 41 import java.lang.classfile.ClassFile; 42 import java.lang.classfile.MethodModel; 43 import java.lang.classfile.TypeKind; 44 import java.lang.constant.MethodTypeDesc; 45 46 import jdk.test.lib.compiler.InMemoryJavaCompiler; 47 import jdk.test.lib.ByteCodeLoader; 48 import static java.lang.System.out; 49 import static java.lang.classfile.ClassFile.*; 50 import static java.lang.constant.ConstantDescs.*; 51 52 import org.junit.jupiter.api.Assertions; 53 import static org.junit.jupiter.api.Assertions.assertTrue; 54 import org.junit.jupiter.api.BeforeAll; 55 import org.junit.jupiter.api.TestInstance; 56 import org.junit.jupiter.params.ParameterizedTest; 57 import org.junit.jupiter.params.provider.MethodSource; 58 59 /** 60 * Checks that an InvalidClassException is thrown when the canonical 61 * constructor cannot be found during deserialization. 62 */ 63 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 64 public class BadCanonicalCtrTest { 65 66 // ClassLoader for creating instances of the records to test with. 67 ClassLoader goodRecordClassLoader; 68 // ClassLoader that can be used during deserialization. Loads record 69 // classes where the canonical constructor has been removed. 70 ClassLoader missingCtrClassLoader; 71 // ClassLoader that can be used during deserialization. Loads record 72 // classes where the canonical constructor has been tampered with. 73 ClassLoader nonCanonicalCtrClassLoader; 74 75 /** 76 * Generates the serializable record classes used by the test. First creates 77 * the initial bytecode for the record classes using javac, then removes or 78 * modifies the generated canonical constructor. 79 */ 80 @BeforeAll 81 public void setup() { 82 { 83 byte[] byteCode = InMemoryJavaCompiler.compile("R1", 84 "public record R1 () implements java.io.Serializable { }"); 85 goodRecordClassLoader = new ByteCodeLoader("R1", byteCode, BadCanonicalCtrTest.class.getClassLoader()); 86 byte[] bc1 = removeConstructor(byteCode); 87 missingCtrClassLoader = new ByteCodeLoader("R1", bc1, BadCanonicalCtrTest.class.getClassLoader()); 88 byte[] bc2 = modifyConstructor(byteCode); 89 nonCanonicalCtrClassLoader = new ByteCodeLoader("R1", bc2, BadCanonicalCtrTest.class.getClassLoader()); 90 } 91 { 92 byte[] byteCode = InMemoryJavaCompiler.compile("R2", 93 "public record R2 (int x, int y) implements java.io.Serializable { }"); 94 goodRecordClassLoader = new ByteCodeLoader("R2", byteCode, goodRecordClassLoader); 95 byte[] bc1 = removeConstructor(byteCode); 96 missingCtrClassLoader = new ByteCodeLoader("R2", bc1, missingCtrClassLoader); 97 byte[] bc2 = modifyConstructor(byteCode); 98 nonCanonicalCtrClassLoader = new ByteCodeLoader("R2", bc2, nonCanonicalCtrClassLoader); 99 } 100 { 101 byte[] byteCode = InMemoryJavaCompiler.compile("R3", 102 "public record R3 (long l) implements java.io.Externalizable {" + 103 " public void writeExternal(java.io.ObjectOutput out) { }" + 104 " public void readExternal(java.io.ObjectInput in) { } }"); 105 goodRecordClassLoader = new ByteCodeLoader("R3", byteCode, goodRecordClassLoader); 106 byte[] bc1 = removeConstructor(byteCode); 107 missingCtrClassLoader = new ByteCodeLoader("R3", bc1, missingCtrClassLoader); 108 byte[] bc2 = modifyConstructor(byteCode); 109 nonCanonicalCtrClassLoader = new ByteCodeLoader("R3", bc2, nonCanonicalCtrClassLoader); 110 } 111 } 112 113 /** Constructs a new instance of record R1. */ 114 Object newR1() throws Exception { 115 Class<?> c = Class.forName("R1", true, goodRecordClassLoader); 116 assert c.isRecord(); 117 assert c.getRecordComponents() != null; 118 return c.getConstructor().newInstance(); 119 } 120 121 /** Constructs a new instance of record R2. */ 122 Object newR2(int x, int y) throws Exception{ 123 Class<?> c = Class.forName("R2", true, goodRecordClassLoader); 124 assert c.isRecord(); 125 assert c.getRecordComponents().length == 2; 126 return c.getConstructor(int.class, int.class).newInstance(x, y); 127 } 128 129 /** Constructs a new instance of record R3. */ 130 Object newR3(long l) throws Exception { 131 Class<?> c = Class.forName("R3", true, goodRecordClassLoader); 132 assert c.isRecord(); 133 assert c.getRecordComponents().length == 1; 134 return c.getConstructor(long.class).newInstance(l); 135 } 136 137 public Object[][] recordInstances() throws Exception { 138 return new Object[][] { 139 new Object[] { newR1() }, 140 new Object[] { newR2(19, 20) }, 141 new Object[] { newR3(67L) }, 142 }; 143 } 144 145 static final Class<InvalidClassException> ICE = InvalidClassException.class; 146 147 /** 148 * Tests that InvalidClassException is thrown when no constructor is 149 * present. 150 */ 151 @ParameterizedTest 152 @MethodSource("recordInstances") 153 public void missingConstructorTest(Object objToSerialize) throws Exception { 154 out.println("\n---"); 155 out.println("serializing : " + objToSerialize); 156 byte[] bytes = serialize(objToSerialize); 157 out.println("deserializing"); 158 InvalidClassException ice = Assertions.assertThrows(ICE, () -> deserialize(bytes, missingCtrClassLoader)); 159 out.println("caught expected ICE: " + ice); 160 assertTrue(ice.getMessage().contains("record canonical constructor not found")); 161 } 162 163 /** 164 * Tests that InvalidClassException is thrown when the canonical 165 * constructor is not present. ( a non-canonical constructor is 166 * present ). 167 */ 168 @ParameterizedTest 169 @MethodSource("recordInstances") 170 public void nonCanonicalConstructorTest(Object objToSerialize) throws Exception { 171 out.println("\n---"); 172 out.println("serializing : " + objToSerialize); 173 byte[] bytes = serialize(objToSerialize); 174 out.println("deserializing"); 175 InvalidClassException ice = Assertions.assertThrows(ICE, () -> deserialize(bytes, nonCanonicalCtrClassLoader)); 176 out.println("caught expected ICE: " + ice); 177 assertTrue(ice.getMessage().contains("record canonical constructor not found")); 178 } 179 180 <T> byte[] serialize(T obj) throws IOException { 181 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 182 ObjectOutputStream oos = new ObjectOutputStream(baos); 183 oos.writeObject(obj); 184 oos.close(); 185 return baos.toByteArray(); 186 } 187 188 @SuppressWarnings("unchecked") 189 <T> T deserialize(byte[] streamBytes, ClassLoader cl) 190 throws IOException, ClassNotFoundException 191 { 192 ByteArrayInputStream bais = new ByteArrayInputStream(streamBytes); 193 ObjectInputStream ois = new ObjectInputStream(bais) { 194 @Override 195 protected Class<?> resolveClass(ObjectStreamClass desc) 196 throws ClassNotFoundException { 197 return Class.forName(desc.getName(), false, cl); 198 } 199 }; 200 return (T) ois.readObject(); 201 } 202 203 // -- machinery for augmenting record class bytes -- 204 205 /** 206 * Removes the constructor from the given class bytes. 207 * Assumes just a single, canonical, constructor. 208 */ 209 static byte[] removeConstructor(byte[] classBytes) { 210 var cf = ClassFile.of(); 211 return cf.transformClass(cf.parse(classBytes), ClassTransform.dropping(ce -> 212 ce instanceof MethodModel mm && mm.methodName().equalsString(INIT_NAME))); 213 } 214 215 /** 216 * Modifies the descriptor of the constructor from the given class bytes. 217 * Assumes just a single, canonical, constructor. 218 */ 219 static byte[] modifyConstructor(byte[] classBytes) { 220 var cf = ClassFile.of(); 221 var classModel = cf.parse(classBytes); 222 return cf.transformClass(cf.parse(classBytes), ClassTransform.dropping(ce -> 223 ce instanceof MethodModel mm && mm.methodName().equalsString(INIT_NAME)) 224 .andThen(ClassTransform.endHandler(clb -> clb.withMethodBody(INIT_NAME, 225 MethodTypeDesc.of(CD_void, CD_Object), ACC_PUBLIC, cob -> { 226 // Initialize strict fields, if any 227 for (var field : classModel.fields()) { 228 if ((field.flags().flagsMask() & (ACC_STRICT_INIT | ACC_STATIC)) != ACC_STRICT_INIT) { 229 continue; 230 } 231 var fieldType = field.fieldTypeSymbol(); 232 cob.aload(0); 233 switch (TypeKind.from(fieldType).asLoadable()) { 234 case INT -> cob.iconst_0(); 235 case LONG -> cob.lconst_0(); 236 case FLOAT -> cob.fconst_0(); 237 case DOUBLE -> cob.dconst_0(); 238 case REFERENCE -> cob.aconst_null(); 239 default -> throw new IllegalArgumentException(fieldType.descriptorString()); 240 } 241 var cp = cob.constantPool(); 242 cob.putfield(cp.fieldRefEntry(classModel.thisClass(), cp.nameAndTypeEntry(field.fieldName(), field.fieldType()))); 243 } 244 cob.aload(0); 245 cob.invokespecial(Record.class.describeConstable().orElseThrow(), 246 INIT_NAME, MTD_void); 247 cob.return_(); 248 })))); 249 } 250 } --- EOF ---