diff --git a/Changes.md b/Changes.md index 09afbadb..45f7a126 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,9 @@ ### Changes +### version 3.34 + +* GitHub PR #524 (Issue #443) + ### version 3.33 on August 23, 2026 * API changes suggested by `@waydeshi` and `@lucianjohnhouse` diff --git a/src/main/javassist/ClassPool.java b/src/main/javassist/ClassPool.java index 326c643b..519ba24f 100644 --- a/src/main/javassist/ClassPool.java +++ b/src/main/javassist/ClassPool.java @@ -23,10 +23,7 @@ import java.io.OutputStream; import java.net.URL; import java.security.ProtectionDomain; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Iterator; +import java.util.*; import javassist.bytecode.ClassFile; import javassist.bytecode.Descriptor; @@ -842,7 +839,7 @@ public synchronized CtClass makeClass(String classname, CtClass superclass) /** * Creates a new public nested class. - * This method is called by {@link CtClassType#makeNestedClass()}. + * This method is called by {@link CtClassType#makeNestedClass(String, boolean)}. * * @param classname a fully-qualified class name. * @return the nested class. @@ -1236,6 +1233,9 @@ public Class toClass(CtClass ct, Class neighbor, ClassLoader loader, ProtectionDomain domain) throws CannotCompileException { + if (neighbor == null) + neighbor = findNeighborInSamePackage(ct, loader); + try { return javassist.util.proxy.DefineClassHelper.toClass(ct.getName(), neighbor, loader, domain, ct.toBytecode()); @@ -1245,6 +1245,47 @@ public Class toClass(CtClass ct, Class neighbor, ClassLoader loader, } } + /** + * Attempts to find an already loadable class in the same package as + * {@code ct} and the same class loader, so that {@code toClass()} can use + * {@code java.lang.invoke.MethodHandles.Lookup} instead of falling back + * to a reflective call to {@code ClassLoader#defineClass}, which triggers + * an illegal-access warning on Java 9 and later. + * + *

{@code MethodHandles.Lookup#defineClass} requires the neighbor to be + * in the same runtime package (same class loader, same package name) as + * the class being defined, so this only returns a candidate when that + * condition can be verified; otherwise it returns {@code null} and the + * caller falls back to the existing behavior.

+ */ + private static Class findNeighborInSamePackage(CtClass ct, ClassLoader loader) { + String pkg = ct.getPackageName(); + + try { + CtClass superclass = ct.getSuperclass(); + if (superclass != null && samePackage(pkg, superclass.getPackageName())) + return Class.forName(superclass.getName(), false, loader); + } + catch (NotFoundException | ClassNotFoundException | LinkageError e) { + // fall through and try interfaces, or give up + } + + try { + for (CtClass itf : ct.getInterfaces()) + if (samePackage(pkg, itf.getPackageName())) + return Class.forName(itf.getName(), false, loader); + } + catch (NotFoundException | ClassNotFoundException | LinkageError e) { + // give up; caller falls back to the reflective defineClass path + } + + return null; + } + + private static boolean samePackage(String pkg1, String pkg2) { + return Objects.equals(pkg1, pkg2); + } + /** * Defines a new package. If the package is already defined, this method * performs nothing. diff --git a/src/main/javassist/bytecode/AttributeInfo.java b/src/main/javassist/bytecode/AttributeInfo.java index 66d5dd5c..0f3804fd 100644 --- a/src/main/javassist/bytecode/AttributeInfo.java +++ b/src/main/javassist/bytecode/AttributeInfo.java @@ -79,7 +79,7 @@ protected AttributeInfo(ConstPool cp, int n, DataInputStream in) * The default value is 0x7FFFFFFD. * The value must be greater than or equal to 0xFFFF. * - * @param n + * @param n the maximum length. * @since 3.33.0 */ public static void setMaxAttributeLength(int n) { diff --git a/src/test/javassist/JvstTest.java b/src/test/javassist/JvstTest.java index da21f983..64f9b7ba 100644 --- a/src/test/javassist/JvstTest.java +++ b/src/test/javassist/JvstTest.java @@ -1185,6 +1185,7 @@ public static Test suite() { suite.addTestSuite(test.javassist.convert.ArrayAccessReplaceTest2.class); suite.addTestSuite(test.javassist.bytecode.analysis.DomTreeTest.class); suite.addTestSuite(javassist.bytecode.SignatureAttributeTest.class); + suite.addTestSuite(ToClassNoNeighborTest.class); return suite; } } diff --git a/src/test/javassist/ToClassNoNeighborTest.java b/src/test/javassist/ToClassNoNeighborTest.java new file mode 100644 index 00000000..857d89e2 --- /dev/null +++ b/src/test/javassist/ToClassNoNeighborTest.java @@ -0,0 +1,105 @@ +package javassist; + +import junit.framework.TestCase; + +/** + * Regression test for {@code ClassPool#toClass(CtClass, Class, ClassLoader, ProtectionDomain)} + * deriving a same-package neighbor when the caller supplies none, so that + * {@code CtClass#toClass()} (and the other no-neighbor overloads) can use + * {@code java.lang.invoke.MethodHandles.Lookup} instead of falling back to a + * reflective call to the protected {@code ClassLoader#defineClass}. + * + *

Why this matters: the reflective fallback is illegal reflective + * access to a JDK-internal method. On Java 9-15 it's allowed but + * prints a warning ("An illegal reflective access operation has occurred ... + * javassist.util.proxy.SecurityActions ...") straight to the process's + * stderr file descriptor, bypassing {@code System.setErr()} — not something + * a test can assert on from within the same JVM. On Java 16+, + * {@code --illegal-access} was removed (JEP 403) and the access is denied + * outright, throwing {@code InaccessibleObjectException} instead — an + * ordinary exception a test can assert on directly. Both are the same root + * cause: no same-package neighbor was available, so {@code DefineClassHelper} + * fell back to reflection. + */ +public class ToClassNoNeighborTest extends TestCase { + + public static class Base {} + + /** + * {@link Base} is in the same package as the generated class, so + * {@code ClassPool} derives it as a neighbor and never needs the + * reflective fallback described in the class Javadoc. Passes on every + * JDK. + */ + public void testToClassWithSamePackageSuperclassAvoidsReflectiveFallback() + throws Exception + { + ClassPool cp = ClassPool.getDefault(); + CtClass base = cp.get(Base.class.getName()); + CtClass generated = cp.makeClass( + "javassist.ToClassNoNeighborTest$SamePackageGenerated", base); + + Class loaded = generated.toClass(); + + assertEquals(Base.class, loaded.getSuperclass()); + } + + /** + * Control case: {@link Object}, the generated class's only ancestor, is + * not in the same package, so {@code ClassPool} has no neighbor to + * derive and must still fall back to reflection. Proves the positive + * test above is exercising the fix rather than passing regardless. + * Expected outcome depends on the JDK (see class Javadoc); on Java 16+ + * it also depends on whether {@code --add-opens + * java.base/java.lang=ALL-UNNAMED} was granted, detected via + * {@code Module.isOpen} so both outcomes are still asserted precisely. + */ + public void testToClassWithoutDerivableNeighborStillUsesReflectiveFallback() + throws Exception + { + ClassPool cp = ClassPool.getDefault(); + CtClass generated = cp.makeClass( + "javassist.ToClassNoNeighborTest$NoNeighborGenerated"); + + if (isJava16OrLater()) { + boolean javaLangOpened = + Object.class.getModule().isOpen("java.lang", ToClassNoNeighborTest.class.getModule()); + + if (javaLangOpened) { + Class loaded = generated.toClass(); + assertEquals(Object.class, loaded.getSuperclass()); + } + else { + try { + generated.toClass(); + fail("Expected the reflective defineClass fallback to be denied by the JVM " + + "(java.lang.reflect.InaccessibleObjectException), since java.lang is not " + + "opened to this module."); + } + catch (RuntimeException e) { + assertEquals("java.lang.reflect.InaccessibleObjectException", e.getClass().getName()); + } + } + } + else { + Class loaded = generated.toClass(); + assertEquals(Object.class, loaded.getSuperclass()); + } + } + + /** + * Returns true if {@code java.specification.version} is 16 or higher. + */ + private static boolean isJava16OrLater() { + String v = System.getProperty("java.specification.version"); + if (v.startsWith("1.")) + return false; // Java 8 or older ("1.6", "1.7", "1.8") + + try { + return Integer.parseInt(v) >= 16; + } + catch (NumberFormatException e) { + return false; + } + } +}