8378792: ObjectMethods.bootstrap missing getter validation

Reviewed-by: rriggs, jvernee
This commit is contained in:
Chen Liang 2026-02-27 17:52:24 +00:00
parent 5606036793
commit 1fb608e1bc
2 changed files with 68 additions and 26 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -363,9 +363,9 @@ public final class ObjectMethods {
* @return the method handle
*/
private static MethodHandle makeToString(MethodHandles.Lookup lookup,
Class<?> receiverClass,
MethodHandle[] getters,
List<String> names) {
Class<?> receiverClass,
MethodHandle[] getters,
List<String> names) {
assert getters.length == names.size();
if (getters.length == 0) {
// special case
@ -516,8 +516,8 @@ public final class ObjectMethods {
requireNonNull(type);
requireNonNull(recordClass);
requireNonNull(names);
requireNonNull(getters);
Arrays.stream(getters).forEach(Objects::requireNonNull);
List<MethodHandle> getterList = List.of(getters); // deep null check
MethodType methodType;
if (type instanceof MethodType mt)
methodType = mt;
@ -526,7 +526,14 @@ public final class ObjectMethods {
if (!MethodHandle.class.equals(type))
throw new IllegalArgumentException(type.toString());
}
List<MethodHandle> getterList = List.of(getters);
for (MethodHandle getter : getterList) {
var getterType = getter.type();
if (getterType.parameterCount() != 1 || getterType.returnType() == void.class || getterType.parameterType(0) != recordClass) {
throw new IllegalArgumentException("Illegal getter type %s for recordClass %s".formatted(getterType, recordClass.getTypeName()));
}
}
MethodHandle handle = switch (methodName) {
case "equals" -> {
if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
@ -541,7 +548,7 @@ public final class ObjectMethods {
case "toString" -> {
if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass)))
throw new IllegalArgumentException("Bad method type: " + methodType);
List<String> nameList = "".equals(names) ? List.of() : List.of(names.split(";"));
List<String> nameList = names.isEmpty() ? List.of() : List.of(names.split(";"));
if (nameList.size() != getterList.size())
throw new IllegalArgumentException("Name list and accessor list do not match");
yield makeToString(lookup, recordClass, getters, nameList);

View File

@ -36,11 +36,11 @@ import java.lang.invoke.MethodType;
import java.lang.runtime.ObjectMethods;
import static java.lang.invoke.MethodType.methodType;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
public class ObjectMethodsTest {
@ -144,8 +144,8 @@ public class ObjectMethodsTest {
assertEquals("Empty[]", (String)handle.invokeExact(new Empty()));
}
Class<NullPointerException> NPE = NullPointerException.class;
Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
private static final Class<NullPointerException> NPE = NullPointerException.class;
private static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
@Test
public void exceptions() {
@ -157,25 +157,60 @@ public class ObjectMethodsTest {
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, "toString", C.EQUALS_DESC, C.class, "x;y", C.ACCESSORS));
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, "hashCode", C.TO_STRING_DESC, C.class, "x;y", C.ACCESSORS));
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, "equals", C.HASHCODE_DESC, C.class, "x;y", C.ACCESSORS));
}
record NamePlusType(String mn, MethodType mt) {}
List<NamePlusType> namePlusTypeList = List.of(
record NamePlusType(String name, MethodType type) {}
static List<NamePlusType> namePlusTypeList() {
return List.of(
new NamePlusType("toString", C.TO_STRING_DESC),
new NamePlusType("equals", C.EQUALS_DESC),
new NamePlusType("hashCode", C.HASHCODE_DESC)
);
for (NamePlusType npt : namePlusTypeList) {
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, npt.mn(), npt.mt(), C.class, "x;y", null));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, npt.mn(), npt.mt(), C.class, "x;y", new MethodHandle[]{null}));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, npt.mn(), npt.mt(), C.class, null, C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, npt.mn(), npt.mt(), null, "x;y", C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, npt.mn(), null, C.class, "x;y", C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, null, npt.mt(), C.class, "x;y", C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(null, npt.mn(), npt.mt(), C.class, "x;y", C.ACCESSORS));
}
}
@MethodSource("namePlusTypeList")
@ParameterizedTest
void commonExceptions(NamePlusType npt) {
String name = npt.name();
MethodType type = npt.type();
// Null checks
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, C.class, "x;y", null));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, C.class, "x;y", new MethodHandle[]{null}));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, C.class, null, C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, null, "x;y", C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, name, null, C.class, "x;y", C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(LOOKUP, null, type, C.class, "x;y", C.ACCESSORS));
assertThrows(NPE, () -> ObjectMethods.bootstrap(null, name, type, C.class, "x;y", C.ACCESSORS));
// Bad indy call receiver type - change C to this test class
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, name, type.changeParameterType(0, this.getClass()), C.class, "x;y", C.ACCESSORS));
// Bad getter types
var wrongReceiverGetter = assertDoesNotThrow(() -> MethodHandles.lookup().findGetter(this.getClass(), "y", int.class));
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, C.class, "x;y",
new MethodHandle[]{
C.ACCESSORS[0],
wrongReceiverGetter,
}));
var extraArgGetter = MethodHandles.dropArguments(C.ACCESSORS[1], 1, int.class);
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, C.class, "x;y",
new MethodHandle[]{
C.ACCESSORS[0],
extraArgGetter,
}));
var voidReturnGetter = MethodHandles.empty(MethodType.methodType(void.class, C.class));
assertThrows(IAE, () -> ObjectMethods.bootstrap(LOOKUP, name, type, C.class, "x;y",
new MethodHandle[]{
C.ACCESSORS[0],
voidReturnGetter,
}));
}
// same field name and type as C::y, for wrongReceiverGetter
private int y;
// Based on the ObjectMethods internal implementation
private static int hashCombiner(int x, int y) {
return x*31 + y;