Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
51 changes: 46 additions & 5 deletions src/main/javassist/ClassPool.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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());
Expand All @@ -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.
*
* <p>{@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.</p>
*/
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.
Expand Down
2 changes: 1 addition & 1 deletion src/main/javassist/bytecode/AttributeInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/test/javassist/JvstTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
105 changes: 105 additions & 0 deletions src/test/javassist/ToClassNoNeighborTest.java
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p><b>Why this matters:</b> the reflective fallback is illegal reflective
* access to a JDK-internal method. On <b>Java 9-15</b> 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 <b>Java 16+</b>,
* {@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;
}
}
}