diff --git a/biz.aQute.bndlib.tests/test/test/ClassParserTest.java b/biz.aQute.bndlib.tests/test/test/ClassParserTest.java index a3560af649..9b56070d50 100644 --- a/biz.aQute.bndlib.tests/test/test/ClassParserTest.java +++ b/biz.aQute.bndlib.tests/test/test/ClassParserTest.java @@ -497,4 +497,37 @@ public void testNoClassForName() throws Exception { c.parseClassFile(getClass().getResourceAsStream("classforname/ClassForName.class")); assertThat(c.getReferred()).doesNotContain(a.getPackageRef("javax.swing")); } + + @Test + public void testProxyNewProxyInstance() throws Exception { + a.setProperty("-noclassforname", "false"); + Clazz c = new Clazz(a, "test/proxy", null); + c.parseClassFile(getClass().getResourceAsStream("proxy/ProxyTest.class")); + // TestInterface.getPath() returns Path which is in java.nio.file package + // TestInterface.getList() returns List which is in java.util package + assertThat(c.getReferred()).contains(a.getPackageRef("java.nio.file")); + assertThat(c.getReferred()).contains(a.getPackageRef("java.util")); + } + + @Test + public void testNoProxyNewProxyInstance() throws Exception { + a.setProperty("-noclassforname", "true"); + Clazz c = new Clazz(a, "test/proxy", null); + c.parseClassFile(getClass().getResourceAsStream("proxy/ProxyTest.class")); + // With -noclassforname, proxy detection should also be disabled + // Note: java.util might still be referenced directly in the code + // but java.nio.file should not be referenced + assertThat(c.getReferred()).doesNotContain(a.getPackageRef("java.nio.file")); + } + + @Test + public void testProxyFromFieldNotDetected() throws Exception { + a.setProperty("-noclassforname", "false"); + Clazz c = new Clazz(a, "test/proxy", null); + c.parseClassFile(getClass().getResourceAsStream("proxy/ProxyFromField.class")); + // When the Class[] array comes from a field, we cannot reliably detect + // which interfaces are being proxied, so we should NOT add java.nio.file + // The array is created in the static initializer, not inline with newProxyInstance + assertThat(c.getReferred()).doesNotContain(a.getPackageRef("java.nio.file")); + } } diff --git a/biz.aQute.bndlib.tests/test/test/proxy/ProxyFromField.java b/biz.aQute.bndlib.tests/test/test/proxy/ProxyFromField.java new file mode 100644 index 0000000000..c11c90acd3 --- /dev/null +++ b/biz.aQute.bndlib.tests/test/test/proxy/ProxyFromField.java @@ -0,0 +1,21 @@ +package test.proxy; + +import java.lang.reflect.Proxy; + +/** + * Test that we don't incorrectly detect proxy interfaces when the Class[] array + * comes from a static field rather than being created inline. + */ +public class ProxyFromField { + + private static final Class[] INTERFACES = new Class[] { TestInterface.class }; + + public static void main(String[] args) { + TestInterface proxy = (TestInterface) Proxy.newProxyInstance( + ProxyFromField.class.getClassLoader(), + INTERFACES, // Array from field - we cannot detect interfaces reliably + (proxy1, method, args1) -> null + ); + System.err.println(proxy); + } +} diff --git a/biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.class b/biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.class new file mode 100644 index 0000000000..6be52f4b04 Binary files /dev/null and b/biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.class differ diff --git a/biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.java b/biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.java new file mode 100644 index 0000000000..5a6051115d --- /dev/null +++ b/biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.java @@ -0,0 +1,16 @@ +package test.proxy; + +import java.lang.reflect.Proxy; + +public class ProxyTest { + + public static void main(String[] args) { + // TestInterface has methods that reference types from java.nio.file and java.util + TestInterface proxy = (TestInterface) Proxy.newProxyInstance( + ProxyTest.class.getClassLoader(), + new Class[] { TestInterface.class }, + (proxy1, method, args1) -> null + ); + System.err.println(proxy); + } +} diff --git a/biz.aQute.bndlib.tests/test/test/proxy/TestInterface.class b/biz.aQute.bndlib.tests/test/test/proxy/TestInterface.class new file mode 100644 index 0000000000..669ea4f334 Binary files /dev/null and b/biz.aQute.bndlib.tests/test/test/proxy/TestInterface.class differ diff --git a/biz.aQute.bndlib.tests/test/test/proxy/TestInterface.java b/biz.aQute.bndlib.tests/test/test/proxy/TestInterface.java new file mode 100644 index 0000000000..798b260aa2 --- /dev/null +++ b/biz.aQute.bndlib.tests/test/test/proxy/TestInterface.java @@ -0,0 +1,26 @@ +package test.proxy; + +import java.nio.file.Path; +import java.util.List; + +/** + * Test interface that has methods returning types from different packages. + * This is used to test that Proxy.newProxyInstance detection works correctly. + */ +public interface TestInterface { + + /** + * Method returning a type from java.nio.file + */ + Path getPath(); + + /** + * Method returning a type from java.util + */ + List getList(); + + /** + * Method with parameter from java.nio.file + */ + void setPath(Path path); +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java index 585513a7f3..288727bca2 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java @@ -25,6 +25,7 @@ import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; @@ -783,6 +784,7 @@ public TypeRef getType() { private Set annotations; private int forName = 0; private int class$ = 0; + private int newProxyInstance = 0; private Set api; private ClassFile classFile = null; @@ -916,6 +918,23 @@ private synchronized Set parseClassFileData(DataInput in) throws Except : findMethodReference("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;"); class$ = findMethodReference(classFile.this_class, "class$", "(Ljava/lang/String;)Ljava/lang/Class;"); + // We also look for Proxy.newProxyInstance calls to detect dynamic proxy creation: + // + // anewarray #n // class java/lang/Class + // ldc(_w) // interface classes + // aastore + // ... + // invokestatic Proxy.newProxyInstance(ClassLoader, Class[], InvocationHandler) + // + // Note: We only detect interfaces when the Class[] array is created inline + // (the anewarray pattern above). We cannot detect interfaces when the array + // comes from a field, local variable, or parameter, as we cannot reliably + // determine the array contents from bytecode alone in those cases. + // + newProxyInstance = analyzer.is(Constants.NOCLASSFORNAME) ? -1 + : findMethodReference("java/lang/reflect/Proxy", "newProxyInstance", + "(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;"); + for (MethodInfo methodInfo : classFile.methods) { referTo(methodInfo.descriptor, methodInfo.access); ElementType elementType = elementType(methodInfo); @@ -1240,6 +1259,12 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { ByteBuffer code = attribute.code.duplicate(); code.rewind(); int lastReference = -1; + // Track interface class constants for Proxy.newProxyInstance + // Note: We only track interfaces when the Class[] array is created inline + // (anewarray + ldc + aastore pattern). We cannot reliably detect interfaces + // when the array comes from a field, variable, or parameter. + List proxyInterfaces = null; // Lazy initialization + boolean inProxyArray = false; // Track if we're building a Class[] for proxy while (code.hasRemaining()) { int instruction = Byte.toUnsignedInt(code.get()); switch (instruction) { @@ -1253,13 +1278,40 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { classConstRef(lastReference); break; } - case OpCodes.anewarray : + case OpCodes.anewarray : { + int class_index = Short.toUnsignedInt(code.getShort()); + classConstRef(class_index); + // Check if this is creating a Class[] array (potential Proxy.newProxyInstance pattern) + if (newProxyInstance != -1 && constantPool.tag(class_index) == CONSTANT_Class) { + String className = constantPool.className(class_index); + if ("java/lang/Class".equals(className)) { + inProxyArray = true; + if (proxyInterfaces == null) { + proxyInterfaces = new ArrayList<>(); + } else { + proxyInterfaces.clear(); + } + } + } + lastReference = -1; + break; + } + case OpCodes.aastore : { + // Store into array - if we're in proxy array building and have a class reference, collect it + if (inProxyArray && lastReference != -1 && constantPool.tag(lastReference) == CONSTANT_Class) { + proxyInterfaces.add(lastReference); + } + lastReference = -1; + break; + } case OpCodes.checkcast : case OpCodes.instanceof_ : case OpCodes.new_ : { int class_index = Short.toUnsignedInt(code.getShort()); classConstRef(class_index); lastReference = -1; + // Reset proxy tracking if we see unrelated instructions + inProxyArray = false; break; } case OpCodes.multianewarray : { @@ -1280,6 +1332,14 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { } } } + // Handle Proxy.newProxyInstance - process collected proxy interfaces + if (method_ref_index == newProxyInstance && proxyInterfaces != null && !proxyInterfaces.isEmpty()) { + for (int classIndex : proxyInterfaces) { + processProxyInterface(classIndex); + } + proxyInterfaces.clear(); + inProxyArray = false; + } lastReference = -1; break; } @@ -2088,6 +2148,38 @@ private void classConstRef(String name) { } } + /** + * Process a proxy interface - treat it as if the class implements the + * interface, which means we need to reference all types from the + * interface's method signatures (parameters and return types). + */ + private void processProxyInterface(int classIndex) { + String interfaceName = constantPool.className(classIndex); + if (interfaceName == null) { + return; + } + + TypeRef interfaceType = analyzer.getTypeRef(interfaceName); + referTo(interfaceType, 0); + + // Load the interface class to analyze its methods + try { + Clazz interfaceClazz = analyzer.findClass(interfaceType); + if (interfaceClazz != null) { + // Process all methods in the interface + interfaceClazz.parseClassFile(); + interfaceClazz.methods().forEach(method -> { + // Reference all types in the method descriptor (parameters and return type) + String descriptor = method.descriptor(); + referTo(descriptor, 0); + }); + } + } catch (Exception e) { + // If we can't load the interface, just reference the interface type itself + logger.debug("Unable to load proxy interface {} for detailed analysis: {}", interfaceName, e.getMessage()); + } + } + public String getClassSignature() { return classDef.getSignature(); }