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