1 /*
  2  * Copyright (c) 2020, 2023, 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  * @enablePreview
 29  * @run testng/othervm ClassDataTest
 30  */
 31 
 32 import java.io.IOException;
 33 import java.io.OutputStream;
 34 import java.io.UncheckedIOException;
 35 import java.lang.classfile.ClassBuilder;
 36 import java.lang.classfile.ClassFile;
 37 import java.lang.classfile.TypeKind;
 38 import java.lang.constant.ClassDesc;
 39 import java.lang.constant.ConstantDescs;
 40 import java.lang.constant.DirectMethodHandleDesc;
 41 import java.lang.constant.DynamicConstantDesc;
 42 import java.lang.constant.MethodTypeDesc;
 43 import java.lang.invoke.MethodHandle;
 44 import java.lang.invoke.MethodHandles;
 45 import java.lang.invoke.MethodHandles.Lookup;
 46 import java.lang.invoke.MethodType;
 47 import java.lang.reflect.AccessFlag;
 48 import java.lang.reflect.Method;
 49 import java.nio.file.Files;
 50 import java.nio.file.Path;
 51 import java.nio.file.Paths;
 52 import java.util.ArrayList;
 53 import java.util.List;
 54 import java.util.Map;
 55 import java.util.function.Consumer;
 56 import java.util.stream.Stream;
 57 
 58 import org.testng.annotations.DataProvider;
 59 import org.testng.annotations.Test;
 60 
 61 import static java.lang.classfile.ClassFile.*;
 62 import static java.lang.constant.ConstantDescs.*;
 63 import static java.lang.invoke.MethodHandles.Lookup.*;
 64 import static org.testng.Assert.*;
 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(value, 20);
 77 
 78         Integer i = MethodHandles.classData(lookup, "_", Integer.class);
 79         assertEquals(i.intValue(), 20);
 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     @DataProvider(name = "teleportedLookup")
 91     private Object[][] teleportedLookup() throws ReflectiveOperationException {
 92         Lookup lookup = hiddenClass(30);
 93         Class<?> hc = lookup.lookupClass();
 94         assertClassData(lookup, 30);
 95 
 96         int fullAccess = PUBLIC|PROTECTED|PACKAGE|MODULE|PRIVATE;
 97         return new Object[][] {
 98                 new Object[] { MethodHandles.privateLookupIn(hc, LOOKUP), fullAccess},
 99                 new Object[] { LOOKUP.in(hc), fullAccess & ~(PROTECTED|PRIVATE) },
100                 new Object[] { lookup.dropLookupMode(PRIVATE), fullAccess & ~(PROTECTED|PRIVATE) },
101         };
102     }
103 
104     @Test(dataProvider = "teleportedLookup", expectedExceptions = { IllegalAccessException.class })
105     public void illegalAccess(Lookup lookup, int access) throws IllegalAccessException {
106         int lookupModes = lookup.lookupModes();
107         assertTrue((lookupModes & ORIGINAL) == 0);
108         assertEquals(lookupModes, access);
109         MethodHandles.classData(lookup, "_", int.class);
110     }
111 
112     @Test(expectedExceptions = { ClassCastException.class })
113     public void incorrectType() throws IllegalAccessException {
114         Lookup lookup = hiddenClass(20);
115         MethodHandles.classData(lookup, "_", Long.class);
116     }
117 
118     @Test(expectedExceptions = { IndexOutOfBoundsException.class })
119     public void invalidIndex() throws IllegalAccessException {
120         Lookup lookup = hiddenClass(List.of());
121         MethodHandles.classDataAt(lookup, "_", Object.class, 0);
122     }
123 
124     @Test(expectedExceptions = { NullPointerException.class })
125     public void unboxNull() throws IllegalAccessException {
126         List<Integer> list = new ArrayList<>();
127         list.add(null);
128         Lookup lookup = hiddenClass(list);
129         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         assertTrue(MethodHandles.classDataAt(lookup, "_", Object.class, 0) == null);
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(value, 100);
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(value, 0.1234f);
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(value, hc);
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         assertEquals(value, colors);
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(value, expected);
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(value, expected);
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         try {
221             return LOOKUP.defineHiddenClassWithClassData(bytes, value, true);
222         } catch (Throwable e) {
223             throw new RuntimeException(e);
224         }
225     }
226     private static Lookup hiddenClass(List<?> list) {
227         ClassByteBuilder builder = new ClassByteBuilder("HC");
228         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, List.class).build();
229         try {
230             return LOOKUP.defineHiddenClassWithClassData(bytes, list, true);
231         } catch (Throwable e) {
232             throw new RuntimeException(e);
233         }
234     }
235 
236     @Test
237     public void condyInvokedFromVirtualMethod() throws ReflectiveOperationException {
238         ClassByteBuilder builder = new ClassByteBuilder("T5");
239         // generate classData instance method
240         byte[] bytes = builder.classData(ACC_PUBLIC, Class.class).build();
241         Lookup hcLookup = hiddenClass(100);
242         assertClassData(hcLookup, 100);
243         Class<?> hc = hcLookup.lookupClass();
244         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
245         Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
246         assertEquals(value, hc);
247         // call through condy
248         Class<?> c = lookup.lookupClass();
249         assertClassData(lookup, c.newInstance(), hc);
250     }
251 
252     @Test
253     public void immutableListClassData() throws ReflectiveOperationException {
254         ClassByteBuilder builder = new ClassByteBuilder("T6");
255         // generate classDataAt instance method
256         byte[] bytes = builder.classDataAt(ACC_PUBLIC, Integer.class, 2).build();
257         List<Integer> cd = List.of(100, 101, 102, 103);
258         int expected = 102;  // element at index=2
259         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
260         int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
261         assertEquals(value, expected);
262         // call through condy
263         Class<?> c = lookup.lookupClass();
264         assertClassData(lookup, c.newInstance() ,expected);
265     }
266 
267     /*
268      * The return value of MethodHandles::classDataAt is the element
269      * contained in the list when the method is called.
270      * If MethodHandles::classDataAt is called via condy, the value
271      * will be captured as a constant.  If the class data is modified
272      * after the element at the given index is computed via condy,
273      * subsequent LDC of such ConstantDynamic entry will return the same
274      * value. However, direct invocation of MethodHandles::classDataAt
275      * will return the modified value.
276      */
277     @Test
278     public void mutableListClassData() throws ReflectiveOperationException {
279         ClassByteBuilder builder = new ClassByteBuilder("T7");
280         // generate classDataAt instance method
281         byte[] bytes = builder.classDataAt(ACC_PUBLIC, MethodType.class, 0).build();
282         MethodType mtype = MethodType.methodType(int.class, String.class);
283         List<MethodType> cd = new ArrayList<>(List.of(mtype));
284         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
285         // call through condy
286         Class<?> c = lookup.lookupClass();
287         assertClassData(lookup, c.newInstance(), mtype);
288         // modify the class data
289         assertTrue(cd.remove(0) == mtype);
290         cd.add(0,  MethodType.methodType(void.class));
291         MethodType newMType = cd.get(0);
292         // loading the element using condy returns the original value
293         assertClassData(lookup, c.newInstance(), mtype);
294         // direct invocation of MethodHandles.classDataAt returns the modified value
295         assertEquals(MethodHandles.classDataAt(lookup, "_", MethodType.class, 0), newMType);
296     }
297 
298     // helper method to extract from a class data map
299     public static <T> T getClassDataEntry(Lookup lookup, String key, Class<T> type) throws IllegalAccessException {
300         Map<String, T> cd = MethodHandles.classData(lookup, "_", Map.class);
301         return type.cast(cd.get(key));
302     }
303 
304     @Test
305     public void classDataMap() throws ReflectiveOperationException {
306         ClassByteBuilder builder = new ClassByteBuilder("map");
307         // generate classData static method
308         DirectMethodHandleDesc bsm = ConstantDescs.ofConstantBootstrap(CD_ClassDataTest, "getClassDataEntry", CD_Object);
309         // generate two accessor methods to get the entries from class data
310         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Map.class)
311                               .classData(ACC_PUBLIC|ACC_STATIC, "getClass",
312                                          Class.class, DynamicConstantDesc.ofNamed(bsm, "class", CD_Class))
313                               .classData(ACC_PUBLIC|ACC_STATIC, "getMethod",
314                                          MethodHandle.class, DynamicConstantDesc.ofNamed(bsm, "method", CD_MethodHandle))
315                               .build();
316 
317         // generate a hidden class
318         Lookup hcLookup = hiddenClass(100);
319         Class<?> hc = hcLookup.lookupClass();
320         assertClassData(hcLookup, 100);
321 
322         MethodHandle mh = hcLookup.findStatic(hc, "classData", MethodType.methodType(int.class));
323         Map<String, Object> cd = Map.of("class", hc, "method", mh);
324         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
325         assertClassData(lookup, cd);
326 
327         // validate the entries from the class data map
328         Class<?> c = lookup.lookupClass();
329         Method m = c.getMethod("getClass");
330         Class<?> v = (Class<?>)m.invoke(null);
331         assertEquals(hc, v);
332 
333         Method m1 = c.getMethod("getMethod");
334         MethodHandle v1 = (MethodHandle) m1.invoke(null);
335         assertEquals(mh, v1);
336     }
337 
338     @Test(expectedExceptions = { IllegalArgumentException.class })
339     public void nonDefaultName() throws ReflectiveOperationException {
340         ClassByteBuilder builder = new ClassByteBuilder("nonDefaultName");
341         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class)
342                               .build();
343         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, ClassDataTest.class, true);
344         assertClassData(lookup, ClassDataTest.class);
345         // throw IAE
346         MethodHandles.classData(lookup, "non_default_name", Class.class);
347     }
348 
349     static class ClassByteBuilder {
350         private static final String OBJECT_CLS = "java/lang/Object";
351         private static final String MHS_CLS = "java/lang/invoke/MethodHandles";
352         private static final String CLASS_DATA_BSM_DESCR =
353                 "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;";
354         private Consumer<ClassBuilder> cw;
355         private final ClassDesc classname;
356 
357         /**
358          * A builder to generate a class file to access class data
359          * @param classname
360          */
361         ClassByteBuilder(String classname) {
362             this.classname = ClassDesc.ofInternalName(classname);
363             this.cw = clb -> {
364                 clb.withSuperclass(CD_Object);
365                 clb.withFlags(AccessFlag.FINAL);
366                 clb.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, cob -> {
367                     cob.aload(0);
368                     cob.invokespecial(CD_Object, INIT_NAME, MTD_void);
369                     cob.return_();
370                 });
371             };
372         }
373 
374         byte[] build() {
375             byte[] bytes = ClassFile.of().build(classname, cw);
376             Path p = Paths.get(classname + ".class");
377                 try (OutputStream os = Files.newOutputStream(p)) {
378                 os.write(bytes);
379             } catch (IOException e) {
380                 throw new UncheckedIOException(e);
381             }
382             return bytes;
383         }
384 
385         /*
386          * Generate classData method to load class data via condy
387          */
388         ClassByteBuilder classData(int accessFlags, Class<?> returnType) {
389             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
390             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
391             cw = cw.andThen(clb -> {
392                 clb.withMethodBody("classData", mt, accessFlags, cob -> {
393                     cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA, DEFAULT_NAME, returnDesc));
394                     cob.return_(TypeKind.from(returnType));
395                 });
396             });
397             return this;
398         }
399 
400         /*
401          * Generate classDataAt method to load an element from class data via condy
402          */
403         ClassByteBuilder classDataAt(int accessFlags, Class<?> returnType, int index) {
404             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
405             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
406             cw = cw.andThen(clb -> {
407                 clb.withMethodBody("classData", mt, accessFlags, cob -> {
408                     cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA_AT, DEFAULT_NAME, returnDesc, index));
409                     cob.return_(TypeKind.from(returnType));
410                 });
411             });
412             return this;
413         }
414 
415         ClassByteBuilder classData(int accessFlags, String name, Class<?> returnType, DynamicConstantDesc<?> dynamic) {
416             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
417             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
418             cw = cw.andThen(clb -> {
419                 clb.withMethodBody(name, mt, accessFlags, cob -> {
420                     cob.loadConstant(dynamic);
421                     cob.return_(TypeKind.from(returnType));
422                 });
423             });
424             return this;
425         }
426     }
427 
428     /*
429      * Load an int constant from class data via condy and
430      * verify it matches the given value.
431      */
432     private void assertClassData(Lookup lookup, int value) throws ReflectiveOperationException {
433         Class<?> c = lookup.lookupClass();
434         Method m = c.getMethod("classData");
435         int v = (int) m.invoke(null);
436         assertEquals(value, v);
437     }
438 
439     /*
440      * Load an int constant from class data via condy and
441      * verify it matches the given value.
442      */
443     private void assertClassData(Lookup lookup, Object o, int value) throws ReflectiveOperationException {
444         Class<?> c = lookup.lookupClass();
445         Method m = c.getMethod("classData");
446         int v = (int) m.invoke(o);
447         assertEquals(value, v);
448     }
449 
450     /*
451      * Load a float constant from class data via condy and
452      * verify it matches the given value.
453      */
454     private void assertClassData(Lookup lookup, float value) throws ReflectiveOperationException {
455         Class<?> c = lookup.lookupClass();
456         Method m = c.getMethod("classData");
457         float v = (float) m.invoke(null);
458         assertEquals(value, v);
459     }
460 
461     /*
462      * Load a Class constant from class data via condy and
463      * verify it matches the given value.
464      */
465     private void assertClassData(Lookup lookup, Class<?> value) throws ReflectiveOperationException {
466         Class<?> c = lookup.lookupClass();
467         Method m = c.getMethod("classData");
468         Class<?> v = (Class<?>)m.invoke(null);
469         assertEquals(value, v);
470     }
471 
472     /*
473      * Load a Class from class data via condy and
474      * verify it matches the given value.
475      */
476     private void assertClassData(Lookup lookup, Object o, Class<?> value) throws ReflectiveOperationException {
477         Class<?> c = lookup.lookupClass();
478         Method m = c.getMethod("classData");
479         Object v = m.invoke(o);
480         assertEquals(value, v);
481     }
482 
483     /*
484      * Load an Object from class data via condy and
485      * verify it matches the given value.
486      */
487     private void assertClassData(Lookup lookup, Object value) throws ReflectiveOperationException {
488         Class<?> c = lookup.lookupClass();
489         Method m = c.getMethod("classData");
490         Object v = m.invoke(null);
491         assertEquals(value, v);
492     }
493 
494     /*
495      * Load an Object from class data via condy and
496      * verify it matches the given value.
497      */
498     private void assertClassData(Lookup lookup, Object o, Object value) throws ReflectiveOperationException {
499         Class<?> c = lookup.lookupClass();
500         Method m = c.getMethod("classData");
501         Object v = m.invoke(o);
502         assertEquals(value, v);
503     }
504 }
505 
506