1 /*
2 * Copyright (c) 2020, 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 8230501
27 * @library /test/lib
28 * @run junit/othervm ClassDataTest
29 */
30
31 import java.io.IOException;
32 import java.io.OutputStream;
33 import java.io.UncheckedIOException;
34 import java.lang.classfile.ClassBuilder;
35 import java.lang.classfile.ClassFile;
36 import java.lang.classfile.TypeKind;
37 import java.lang.constant.ClassDesc;
38 import java.lang.constant.ConstantDescs;
39 import java.lang.constant.DirectMethodHandleDesc;
40 import java.lang.constant.DynamicConstantDesc;
41 import java.lang.constant.MethodTypeDesc;
42 import java.lang.invoke.MethodHandle;
43 import java.lang.invoke.MethodHandles;
44 import java.lang.invoke.MethodHandles.Lookup;
45 import java.lang.invoke.MethodType;
46 import java.lang.reflect.AccessFlag;
47 import java.lang.reflect.Method;
48 import java.nio.file.Files;
49 import java.nio.file.Path;
50 import java.nio.file.Paths;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.function.Consumer;
55 import java.util.stream.Stream;
56
57
58 import static java.lang.classfile.ClassFile.*;
59 import static java.lang.constant.ConstantDescs.*;
60 import static java.lang.invoke.MethodHandles.Lookup.*;
61 import static org.junit.jupiter.api.Assertions.*;
62 import org.junit.jupiter.api.Test;
63 import org.junit.jupiter.params.ParameterizedTest;
64 import org.junit.jupiter.params.provider.MethodSource;
65
66 public class ClassDataTest {
67 private static final Lookup LOOKUP = MethodHandles.lookup();
68 private static final ClassDesc CD_ClassDataTest = ClassDataTest.class.describeConstable().orElseThrow();
69
70 @Test
71 public void testOriginalAccess() throws IllegalAccessException {
72 Lookup lookup = hiddenClass(20);
73 assertTrue(lookup.hasFullPrivilegeAccess());
74
75 int value = MethodHandles.classData(lookup, "_", int.class);
76 assertEquals(20, value);
77
78 Integer i = MethodHandles.classData(lookup, "_", Integer.class);
79 assertEquals(20, i.intValue());
80 }
81
82 /*
83 * A lookup class with no class data.
84 */
85 @Test
86 public void noClassData() throws IllegalAccessException {
87 assertNull(MethodHandles.classData(LOOKUP, "_", Object.class));
88 }
89
90 private static Object[][] teleportedLookup() throws ReflectiveOperationException {
91 Lookup lookup = hiddenClass(30);
92 Class<?> hc = lookup.lookupClass();
93 assertClassData(lookup, 30);
94
95 int fullAccess = PUBLIC|PROTECTED|PACKAGE|MODULE|PRIVATE;
96 return new Object[][] {
97 new Object[] { MethodHandles.privateLookupIn(hc, LOOKUP), fullAccess},
98 new Object[] { LOOKUP.in(hc), fullAccess & ~(PROTECTED|PRIVATE) },
99 new Object[] { lookup.dropLookupMode(PRIVATE), fullAccess & ~(PROTECTED|PRIVATE) },
100 };
101 }
102
103 @ParameterizedTest
104 @MethodSource("teleportedLookup")
105 public void illegalAccess(Lookup lookup, int access) throws IllegalAccessException {
106 int lookupModes = lookup.lookupModes();
107 assertEquals(0, lookupModes & ORIGINAL);
108 assertEquals(access, lookupModes);
109 assertThrows(IllegalAccessException.class, () -> MethodHandles.classData(lookup, "_", int.class));
110 }
111
112 @Test
113 public void incorrectType() throws IllegalAccessException {
114 Lookup lookup = hiddenClass(20);
115 assertThrows(ClassCastException.class, () -> MethodHandles.classData(lookup, "_", Long.class));
116 }
117
118 @Test
119 public void invalidIndex() throws IllegalAccessException {
120 Lookup lookup = hiddenClass(List.of());
121 assertThrows(IndexOutOfBoundsException.class, () -> MethodHandles.classDataAt(lookup, "_", Object.class, 0));
122 }
123
124 @Test
125 public void unboxNull() throws IllegalAccessException {
126 List<Integer> list = new ArrayList<>();
127 list.add(null);
128 Lookup lookup = hiddenClass(list);
129 assertThrows(NullPointerException.class, () -> MethodHandles.classDataAt(lookup, "_", int.class, 0));
130 }
131
132 @Test
133 public void nullElement() throws IllegalAccessException {
134 List<Object> list = new ArrayList<>();
135 list.add(null);
136 Lookup lookup = hiddenClass(list);
137 assertNull(MethodHandles.classDataAt(lookup, "_", Object.class, 0));
138 }
139
140 @Test
141 public void intClassData() throws ReflectiveOperationException {
142 ClassByteBuilder builder = new ClassByteBuilder("T1-int");
143 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build();
144 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 100, true);
145 int value = MethodHandles.classData(lookup, "_", int.class);
146 assertEquals(100, value);
147 // call through condy
148 assertClassData(lookup, 100);
149 }
150
151 @Test
152 public void floatClassData() throws ReflectiveOperationException {
153 ClassByteBuilder builder = new ClassByteBuilder("T1-float");
154 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, float.class).build();
155 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 0.1234f, true);
156 float value = MethodHandles.classData(lookup, "_", float.class);
157 assertEquals(0.1234f, value);
158 // call through condy
159 assertClassData(lookup, 0.1234f);
160 }
161
162 @Test
163 public void classClassData() throws ReflectiveOperationException {
164 Class<?> hc = hiddenClass(100).lookupClass();
165 ClassByteBuilder builder = new ClassByteBuilder("T2");
166 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class).build();
167 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
168 Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
169 assertEquals(hc, value);
170 // call through condy
171 assertClassData(lookup, hc);
172 }
173
174 @Test
175 public void arrayClassData() throws ReflectiveOperationException {
176 ClassByteBuilder builder = new ClassByteBuilder("T3");
177 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, String[].class).build();
178 String[] colors = new String[] { "red", "yellow", "blue"};
179 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, colors, true);
180 assertClassData(lookup, colors.clone());
181 // class data is modifiable and not a constant
182 colors[0] = "black";
183 // it will get back the modified class data
184 String[] value = MethodHandles.classData(lookup, "_", String[].class);
185 assertArrayEquals(colors, value);
186 // even call through condy as it's not a constant
187 assertClassData(lookup, colors);
188 }
189
190 @Test
191 public void listClassData() throws ReflectiveOperationException {
192 ClassByteBuilder builder = new ClassByteBuilder("T4");
193 byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 2).build();
194 List<Integer> cd = List.of(100, 101, 102, 103);
195 int expected = 102; // element at index=2
196 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
197 int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
198 assertEquals(expected, value);
199 // call through condy
200 assertClassData(lookup, expected);
201 }
202
203 @Test
204 public void arrayListClassData() throws ReflectiveOperationException {
205 ClassByteBuilder builder = new ClassByteBuilder("T4");
206 byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 1).build();
207 ArrayList<Integer> cd = new ArrayList<>();
208 Stream.of(100, 101, 102, 103).forEach(cd::add);
209 int expected = 101; // element at index=1
210 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
211 int value = MethodHandles.classDataAt(lookup, "_", int.class, 1);
212 assertEquals(expected, value);
213 // call through condy
214 assertClassData(lookup, expected);
215 }
216
217 private static Lookup hiddenClass(int value) {
218 ClassByteBuilder builder = new ClassByteBuilder("HC");
219 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build();
220 return assertDoesNotThrow(() -> LOOKUP.defineHiddenClassWithClassData(bytes, value, true));
221 }
222 private static Lookup hiddenClass(List<?> list) {
223 ClassByteBuilder builder = new ClassByteBuilder("HC");
224 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, List.class).build();
225 return assertDoesNotThrow(() -> LOOKUP.defineHiddenClassWithClassData(bytes, list, true));
226 }
227
228 @Test
229 public void condyInvokedFromVirtualMethod() throws ReflectiveOperationException {
230 ClassByteBuilder builder = new ClassByteBuilder("T5");
231 // generate classData instance method
232 byte[] bytes = builder.classData(ACC_PUBLIC, Class.class).build();
233 Lookup hcLookup = hiddenClass(100);
234 assertClassData(hcLookup, 100);
235 Class<?> hc = hcLookup.lookupClass();
236 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
237 Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
238 assertEquals(hc, value);
239 // call through condy
240 Class<?> c = lookup.lookupClass();
241 assertClassData(lookup, c.newInstance(), hc);
242 }
243
244 @Test
245 public void immutableListClassData() throws ReflectiveOperationException {
246 ClassByteBuilder builder = new ClassByteBuilder("T6");
247 // generate classDataAt instance method
248 byte[] bytes = builder.classDataAt(ACC_PUBLIC, Integer.class, 2).build();
249 List<Integer> cd = List.of(100, 101, 102, 103);
250 int expected = 102; // element at index=2
251 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
252 int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
253 assertEquals(expected, value);
254 // call through condy
255 Class<?> c = lookup.lookupClass();
256 assertClassData(lookup, c.newInstance() ,expected);
257 }
258
259 /*
260 * The return value of MethodHandles::classDataAt is the element
261 * contained in the list when the method is called.
262 * If MethodHandles::classDataAt is called via condy, the value
263 * will be captured as a constant. If the class data is modified
264 * after the element at the given index is computed via condy,
265 * subsequent LDC of such ConstantDynamic entry will return the same
266 * value. However, direct invocation of MethodHandles::classDataAt
267 * will return the modified value.
268 */
269 @Test
270 public void mutableListClassData() throws ReflectiveOperationException {
271 ClassByteBuilder builder = new ClassByteBuilder("T7");
272 // generate classDataAt instance method
273 byte[] bytes = builder.classDataAt(ACC_PUBLIC, MethodType.class, 0).build();
274 MethodType mtype = MethodType.methodType(int.class, String.class);
275 List<MethodType> cd = new ArrayList<>(List.of(mtype));
276 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
277 // call through condy
278 Class<?> c = lookup.lookupClass();
279 assertClassData(lookup, c.newInstance(), mtype);
280 // modify the class data
281 assertSame(mtype, cd.remove(0));
282 cd.add(0, MethodType.methodType(void.class));
283 MethodType newMType = cd.get(0);
284 // loading the element using condy returns the original value
285 assertClassData(lookup, c.newInstance(), mtype);
286 // direct invocation of MethodHandles.classDataAt returns the modified value
287 assertEquals(newMType, MethodHandles.classDataAt(lookup, "_", MethodType.class, 0));
288 }
289
290 // helper method to extract from a class data map
291 public static <T> T getClassDataEntry(Lookup lookup, String key, Class<T> type) throws IllegalAccessException {
292 Map<String, T> cd = MethodHandles.classData(lookup, "_", Map.class);
293 return type.cast(cd.get(key));
294 }
295
296 @Test
297 public void classDataMap() throws ReflectiveOperationException {
298 ClassByteBuilder builder = new ClassByteBuilder("map");
299 // generate classData static method
300 DirectMethodHandleDesc bsm = ConstantDescs.ofConstantBootstrap(CD_ClassDataTest, "getClassDataEntry", CD_Object);
301 // generate two accessor methods to get the entries from class data
302 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Map.class)
303 .classData(ACC_PUBLIC|ACC_STATIC, "getClass",
304 Class.class, DynamicConstantDesc.ofNamed(bsm, "class", CD_Class))
305 .classData(ACC_PUBLIC|ACC_STATIC, "getMethod",
306 MethodHandle.class, DynamicConstantDesc.ofNamed(bsm, "method", CD_MethodHandle))
307 .build();
308
309 // generate a hidden class
310 Lookup hcLookup = hiddenClass(100);
311 Class<?> hc = hcLookup.lookupClass();
312 assertClassData(hcLookup, 100);
313
314 MethodHandle mh = hcLookup.findStatic(hc, "classData", MethodType.methodType(int.class));
315 Map<String, Object> cd = Map.of("class", hc, "method", mh);
316 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
317 assertClassData(lookup, cd);
318
319 // validate the entries from the class data map
320 Class<?> c = lookup.lookupClass();
321 Method m = c.getMethod("getClass");
322 Class<?> v = (Class<?>)m.invoke(null);
323 assertEquals(hc, v);
324
325 Method m1 = c.getMethod("getMethod");
326 MethodHandle v1 = (MethodHandle) m1.invoke(null);
327 assertEquals(mh, v1);
328 }
329
330 @Test
331 public void nonDefaultName() throws ReflectiveOperationException {
332 ClassByteBuilder builder = new ClassByteBuilder("nonDefaultName");
333 byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class)
334 .build();
335 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, ClassDataTest.class, true);
336 assertClassData(lookup, ClassDataTest.class);
337 // throw IAE
338 assertThrows(IllegalArgumentException.class, () -> MethodHandles.classData(lookup, "non_default_name", Class.class));
339 }
340
341 static class ClassByteBuilder {
342 private static final String OBJECT_CLS = "java/lang/Object";
343 private static final String MHS_CLS = "java/lang/invoke/MethodHandles";
344 private static final String CLASS_DATA_BSM_DESCR =
345 "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;";
346 private Consumer<ClassBuilder> cw;
347 private final ClassDesc classname;
348
349 /**
350 * A builder to generate a class file to access class data
351 * @param classname
352 */
353 ClassByteBuilder(String classname) {
354 this.classname = ClassDesc.ofInternalName(classname);
355 this.cw = clb -> {
356 clb.withSuperclass(CD_Object);
357 clb.withFlags(AccessFlag.FINAL);
358 clb.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, cob -> {
359 cob.aload(0);
360 cob.invokespecial(CD_Object, INIT_NAME, MTD_void);
361 cob.return_();
362 });
363 };
364 }
365
366 byte[] build() {
367 byte[] bytes = ClassFile.of().build(classname, cw);
368 Path p = Paths.get(classname + ".class");
369 try (OutputStream os = Files.newOutputStream(p)) {
370 os.write(bytes);
371 } catch (IOException e) {
372 throw new UncheckedIOException(e);
373 }
374 return bytes;
375 }
376
377 /*
378 * Generate classData method to load class data via condy
379 */
380 ClassByteBuilder classData(int accessFlags, Class<?> returnType) {
381 ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
382 MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
383 cw = cw.andThen(clb -> {
384 clb.withMethodBody("classData", mt, accessFlags, cob -> {
385 cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA, DEFAULT_NAME, returnDesc));
386 cob.return_(TypeKind.from(returnType));
387 });
388 });
389 return this;
390 }
391
392 /*
393 * Generate classDataAt method to load an element from class data via condy
394 */
395 ClassByteBuilder classDataAt(int accessFlags, Class<?> returnType, int index) {
396 ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
397 MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
398 cw = cw.andThen(clb -> {
399 clb.withMethodBody("classData", mt, accessFlags, cob -> {
400 cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA_AT, DEFAULT_NAME, returnDesc, index));
401 cob.return_(TypeKind.from(returnType));
402 });
403 });
404 return this;
405 }
406
407 ClassByteBuilder classData(int accessFlags, String name, Class<?> returnType, DynamicConstantDesc<?> dynamic) {
408 ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
409 MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
410 cw = cw.andThen(clb -> {
411 clb.withMethodBody(name, mt, accessFlags, cob -> {
412 cob.loadConstant(dynamic);
413 cob.return_(TypeKind.from(returnType));
414 });
415 });
416 return this;
417 }
418 }
419
420 /*
421 * Load an int constant from class data via condy and
422 * verify it matches the given value.
423 */
424 private static void assertClassData(Lookup lookup, int value) throws ReflectiveOperationException {
425 Class<?> c = lookup.lookupClass();
426 Method m = c.getMethod("classData");
427 int v = (int) m.invoke(null);
428 assertEquals(value, v);
429 }
430
431 /*
432 * Load an int constant from class data via condy and
433 * verify it matches the given value.
434 */
435 private static void assertClassData(Lookup lookup, Object o, int value) throws ReflectiveOperationException {
436 Class<?> c = lookup.lookupClass();
437 Method m = c.getMethod("classData");
438 int v = (int) m.invoke(o);
439 assertEquals(value, v);
440 }
441
442 /*
443 * Load a float constant from class data via condy and
444 * verify it matches the given value.
445 */
446 private static void assertClassData(Lookup lookup, float value) throws ReflectiveOperationException {
447 Class<?> c = lookup.lookupClass();
448 Method m = c.getMethod("classData");
449 float v = (float) m.invoke(null);
450 assertEquals(value, v);
451 }
452
453 /*
454 * Load a Class constant from class data via condy and
455 * verify it matches the given value.
456 */
457 private static void assertClassData(Lookup lookup, Class<?> value) throws ReflectiveOperationException {
458 Class<?> c = lookup.lookupClass();
459 Method m = c.getMethod("classData");
460 Class<?> v = (Class<?>)m.invoke(null);
461 assertEquals(value, v);
462 }
463
464 /*
465 * Load a Class from class data via condy and
466 * verify it matches the given value.
467 */
468 private static void assertClassData(Lookup lookup, Object o, Class<?> value) throws ReflectiveOperationException {
469 Class<?> c = lookup.lookupClass();
470 Method m = c.getMethod("classData");
471 Object v = m.invoke(o);
472 assertEquals(value, v);
473 }
474
475 /*
476 * Load an Object from class data via condy and
477 * verify it matches the given value.
478 */
479 private static void assertClassData(Lookup lookup, Object value) throws ReflectiveOperationException {
480 Class<?> c = lookup.lookupClass();
481 Method m = c.getMethod("classData");
482 Object v = m.invoke(null);
483 assertEquals(value, v);
484 }
485
486 /*
487 * Load an Object array from class data via condy and
488 * verify it matches the given value in content.
489 */
490 private static void assertClassData(Lookup lookup, Object[] value) throws ReflectiveOperationException {
491 Class<?> c = lookup.lookupClass();
492 Method m = c.getMethod("classData");
493 Object v = m.invoke(null);
494 assertArrayEquals(value, (Object[]) v);
495 }
496
497 /*
498 * Load an Object from class data via condy and
499 * verify it matches the given value.
500 */
501 private static void assertClassData(Lookup lookup, Object o, Object value) throws ReflectiveOperationException {
502 Class<?> c = lookup.lookupClass();
503 Method m = c.getMethod("classData");
504 Object v = m.invoke(o);
505 assertEquals(value, v);
506 }
507 }
508
509