1 /* 2 * Copyright (c) 2019, 2025, 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.constant.MethodTypeDesc; 44 45 import jdk.test.lib.compiler.InMemoryJavaCompiler; 46 import jdk.test.lib.ByteCodeLoader; 47 import static java.lang.System.out; 48 import static java.lang.classfile.ClassFile.ACC_PUBLIC; 49 import static java.lang.constant.ConstantDescs.CD_Object; 50 import static java.lang.constant.ConstantDescs.CD_void; 51 import static java.lang.constant.ConstantDescs.INIT_NAME; 52 import static java.lang.constant.ConstantDescs.MTD_void; 53 54 import org.junit.jupiter.api.Assertions; 55 import static org.junit.jupiter.api.Assertions.assertTrue; 56 import org.junit.jupiter.api.BeforeAll; 57 import org.junit.jupiter.api.TestInstance; 58 import org.junit.jupiter.params.ParameterizedTest; 59 import org.junit.jupiter.params.provider.MethodSource; 60 61 /** 62 * Checks that an InvalidClassException is thrown when the canonical 63 * constructor cannot be found during deserialization. 64 */ 65 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 66 public class BadCanonicalCtrTest { 67 68 // ClassLoader for creating instances of the records to test with. 69 ClassLoader goodRecordClassLoader; 70 // ClassLoader that can be used during deserialization. Loads record 71 // classes where the canonical constructor has been removed. 72 ClassLoader missingCtrClassLoader; 73 // ClassLoader that can be used during deserialization. Loads record 74 // classes where the canonical constructor has been tampered with. 75 ClassLoader nonCanonicalCtrClassLoader; 76 77 /** 78 * Generates the serializable record classes used by the test. First creates 79 * the initial bytecode for the record classes using javac, then removes or 80 * modifies the generated canonical constructor. 81 */ 82 @BeforeAll 83 public void setup() { 84 { 85 byte[] byteCode = InMemoryJavaCompiler.compile("R1", 86 "public record R1 () implements java.io.Serializable { }"); 87 goodRecordClassLoader = new ByteCodeLoader("R1", byteCode, BadCanonicalCtrTest.class.getClassLoader()); 88 byte[] bc1 = removeConstructor(byteCode); 89 missingCtrClassLoader = new ByteCodeLoader("R1", bc1, BadCanonicalCtrTest.class.getClassLoader()); 90 byte[] bc2 = modifyConstructor(byteCode); 91 nonCanonicalCtrClassLoader = new ByteCodeLoader("R1", bc2, BadCanonicalCtrTest.class.getClassLoader()); 92 } 93 { 94 byte[] byteCode = InMemoryJavaCompiler.compile("R2", 95 "public record R2 (int x, int y) implements java.io.Serializable { }"); 96 goodRecordClassLoader = new ByteCodeLoader("R2", byteCode, goodRecordClassLoader); 97 byte[] bc1 = removeConstructor(byteCode); 98 missingCtrClassLoader = new ByteCodeLoader("R2", bc1, missingCtrClassLoader); 99 byte[] bc2 = modifyConstructor(byteCode); 100 nonCanonicalCtrClassLoader = new ByteCodeLoader("R2", bc2, nonCanonicalCtrClassLoader); 101 } 102 { 103 byte[] byteCode = InMemoryJavaCompiler.compile("R3", 104 "public record R3 (long l) implements java.io.Externalizable {" + 105 " public void writeExternal(java.io.ObjectOutput out) { }" + 106 " public void readExternal(java.io.ObjectInput in) { } }"); 107 goodRecordClassLoader = new ByteCodeLoader("R3", byteCode, goodRecordClassLoader); 108 byte[] bc1 = removeConstructor(byteCode); 109 missingCtrClassLoader = new ByteCodeLoader("R3", bc1, missingCtrClassLoader); 110 byte[] bc2 = modifyConstructor(byteCode); 111 nonCanonicalCtrClassLoader = new ByteCodeLoader("R3", bc2, nonCanonicalCtrClassLoader); 112 } 113 } 114 115 /** Constructs a new instance of record R1. */ 116 Object newR1() throws Exception { 117 Class<?> c = Class.forName("R1", true, goodRecordClassLoader); 118 assert c.isRecord(); 119 assert c.getRecordComponents() != null; 120 return c.getConstructor().newInstance(); 121 } 122 123 /** Constructs a new instance of record R2. */ 124 Object newR2(int x, int y) throws Exception{ 125 Class<?> c = Class.forName("R2", true, goodRecordClassLoader); 126 assert c.isRecord(); 127 assert c.getRecordComponents().length == 2; 128 return c.getConstructor(int.class, int.class).newInstance(x, y); 129 } 130 131 /** Constructs a new instance of record R3. */ 132 Object newR3(long l) throws Exception { 133 Class<?> c = Class.forName("R3", true, goodRecordClassLoader); 134 assert c.isRecord(); 135 assert c.getRecordComponents().length == 1; 136 return c.getConstructor(long.class).newInstance(l); 137 } 138 139 public Object[][] recordInstances() throws Exception { 140 return new Object[][] { 141 new Object[] { newR1() }, 142 new Object[] { newR2(19, 20) }, 143 new Object[] { newR3(67L) }, 144 }; 145 } 146 147 static final Class<InvalidClassException> ICE = InvalidClassException.class; 148 149 /** 150 * Tests that InvalidClassException is thrown when no constructor is 151 * present. 152 */ 153 @ParameterizedTest 154 @MethodSource("recordInstances") 155 public void missingConstructorTest(Object objToSerialize) throws Exception { 156 out.println("\n---"); 157 out.println("serializing : " + objToSerialize); 158 byte[] bytes = serialize(objToSerialize); 159 out.println("deserializing"); 160 InvalidClassException ice = Assertions.assertThrows(ICE, () -> deserialize(bytes, missingCtrClassLoader)); 161 out.println("caught expected ICE: " + ice); 162 assertTrue(ice.getMessage().contains("record canonical constructor not found")); 163 } 164 165 /** 166 * Tests that InvalidClassException is thrown when the canonical 167 * constructor is not present. ( a non-canonical constructor is 168 * present ). 169 */ 170 @ParameterizedTest 171 @MethodSource("recordInstances") 172 public void nonCanonicalConstructorTest(Object objToSerialize) throws Exception { 173 out.println("\n---"); 174 out.println("serializing : " + objToSerialize); 175 byte[] bytes = serialize(objToSerialize); 176 out.println("deserializing"); 177 InvalidClassException ice = Assertions.assertThrows(ICE, () -> deserialize(bytes, nonCanonicalCtrClassLoader)); 178 out.println("caught expected ICE: " + ice); 179 assertTrue(ice.getMessage().contains("record canonical constructor not found")); 180 } 181 182 <T> byte[] serialize(T obj) throws IOException { 183 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 184 ObjectOutputStream oos = new ObjectOutputStream(baos); 185 oos.writeObject(obj); 186 oos.close(); 187 return baos.toByteArray(); 188 } 189 190 @SuppressWarnings("unchecked") 191 <T> T deserialize(byte[] streamBytes, ClassLoader cl) 192 throws IOException, ClassNotFoundException 193 { 194 ByteArrayInputStream bais = new ByteArrayInputStream(streamBytes); 195 ObjectInputStream ois = new ObjectInputStream(bais) { 196 @Override 197 protected Class<?> resolveClass(ObjectStreamClass desc) 198 throws ClassNotFoundException { 199 return Class.forName(desc.getName(), false, cl); 200 } 201 }; 202 return (T) ois.readObject(); 203 } 204 205 // -- machinery for augmenting record class bytes -- 206 207 /** 208 * Removes the constructor from the given class bytes. 209 * Assumes just a single, canonical, constructor. 210 */ 211 static byte[] removeConstructor(byte[] classBytes) { 212 var cf = ClassFile.of(); 213 return cf.transformClass(cf.parse(classBytes), ClassTransform.dropping(ce -> 214 ce instanceof MethodModel mm && mm.methodName().equalsString(INIT_NAME))); 215 } 216 217 /** 218 * Modifies the descriptor of the constructor from the given class bytes. 219 * Assumes just a single, canonical, constructor. 220 */ 221 static byte[] modifyConstructor(byte[] classBytes) { 222 var cf = ClassFile.of(); 223 return cf.transformClass(cf.parse(classBytes), ClassTransform.dropping(ce -> 224 ce instanceof MethodModel mm && mm.methodName().equalsString(INIT_NAME)) 225 .andThen(ClassTransform.endHandler(clb -> clb.withMethodBody(INIT_NAME, 226 MethodTypeDesc.of(CD_void, CD_Object), ACC_PUBLIC, cob -> { 227 cob.aload(0); 228 cob.invokespecial(Record.class.describeConstable().orElseThrow(), 229 INIT_NAME, MTD_void); 230 cob.return_(); 231 })))); 232 } 233 } --- EOF ---