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 } 504 505