From 9777b0d897ee26001287de513fce68b748cfef21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 05:34:37 +0000 Subject: [PATCH 1/4] Initial plan From 4d1e08d6f4928639fbcb3198f96d68c96368f2ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 05:52:17 +0000 Subject: [PATCH 2/4] Implement Proxy.newProxyInstance detection for import packages Add detection for Proxy.newProxyInstance calls to automatically include packages referenced by proxy interface methods. This works similarly to the existing Class.forName detection. Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../test/test/ClassParserTest.java | 22 +++++ .../test/test/proxy/ProxyTest.class | Bin 0 -> 1418 bytes .../test/test/proxy/ProxyTest.java | 16 ++++ .../test/test/proxy/TestInterface.class | Bin 0 -> 305 bytes .../test/test/proxy/TestInterface.java | 26 ++++++ .../src/aQute/bnd/osgi/Clazz.java | 79 +++++++++++++++++- 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.class create mode 100644 biz.aQute.bndlib.tests/test/test/proxy/ProxyTest.java create mode 100644 biz.aQute.bndlib.tests/test/test/proxy/TestInterface.class create mode 100644 biz.aQute.bndlib.tests/test/test/proxy/TestInterface.java diff --git a/biz.aQute.bndlib.tests/test/test/ClassParserTest.java b/biz.aQute.bndlib.tests/test/test/ClassParserTest.java index a3560af649..386f259d79 100644 --- a/biz.aQute.bndlib.tests/test/test/ClassParserTest.java +++ b/biz.aQute.bndlib.tests/test/test/ClassParserTest.java @@ -497,4 +497,26 @@ 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")); + } } 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 0000000000000000000000000000000000000000..6be52f4b04a04ead278e70c247ed8b2c3ad1b471 GIT binary patch literal 1418 zcma)6TT|0O6#lj?L_%n5xuc+9krt5R{SrW>>R6{z&YsQB-^bqptm2u92%-vN8hX&nklf+>JZExiEBCgt zBdU_2cg3&_xyBI9WH%ICU>K6ZmAQsvA2xIEys=Ds6)O4_Xc`6(XGnC2@+Nm(hWM6{ z{<3KEns6A#Guh$^tKdjikYpGOMeza@EGe8#UKJ_IJf&d}LzJ9h?c2M8VXlkJ5t}9@ zpDWO5l}p36UUI8u3P)Elf>8xy8pbifFxp9=?c*z=N?78+UluG^a;r))Dw%)e`{5+6 zhk!Qk>0H9{gKF!2xQs~!QyQ+|YS(p?nywUex&`4-?S_K@hD{e>Sh6G?!RsV$2Gf7=%3E zH{60P<~%pek|Bkj#3EH?MTLS@4GUOg&?&_|TQuVYG>hVqxf0X73`$%Ir mzJ?ow#0Xm=i)GISH*t$*jj-E[] { 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 0000000000000000000000000000000000000000..669ea4f3343da391f1f28044179aa9ab22d8da2a GIT binary patch literal 305 zcmZvXOHRW;5JamJi1Xnm79=M?@g{Qs14|?%OCTAspI8$!ku^r+b`-A0f&*}wdPL8GGxB z{W9bhW^3}-SE I&RcTv3n{@)ZU6uP literal 0 HcmV?d00001 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..b1918622e7 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,18 @@ 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) + // + 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 +1254,8 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { ByteBuffer code = attribute.code.duplicate(); code.rewind(); int lastReference = -1; + List proxyInterfaces = new ArrayList<>(); // Track interface class constants for Proxy.newProxyInstance + boolean inProxyArray = false; // Track if we're building a Class[] for proxy while (code.hasRemaining()) { int instruction = Byte.toUnsignedInt(code.get()); switch (instruction) { @@ -1253,7 +1269,28 @@ 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; + 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_ : { @@ -1280,6 +1317,14 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { } } } + // Handle Proxy.newProxyInstance - process collected proxy interfaces + if (method_ref_index == newProxyInstance && !proxyInterfaces.isEmpty()) { + for (int classIndex : proxyInterfaces) { + processProxyInterface(classIndex); + } + proxyInterfaces.clear(); + inProxyArray = false; + } lastReference = -1; break; } @@ -2088,6 +2133,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(); } From c4f5e76d1b6b7a3fc60fb2533f1b98b100dad656 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 06:00:38 +0000 Subject: [PATCH 3/4] Add test and clarify limitations of Proxy detection Add test case to verify that proxy interfaces are NOT detected when the Class[] array comes from a field/variable. Add comments explaining that detection only works for inline array creation pattern. Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../test/test/ClassParserTest.java | 11 ++++++++++ .../test/test/proxy/ProxyFromField.java | 21 +++++++++++++++++++ .../src/aQute/bnd/osgi/Clazz.java | 13 +++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 biz.aQute.bndlib.tests/test/test/proxy/ProxyFromField.java diff --git a/biz.aQute.bndlib.tests/test/test/ClassParserTest.java b/biz.aQute.bndlib.tests/test/test/ClassParserTest.java index 386f259d79..9b56070d50 100644 --- a/biz.aQute.bndlib.tests/test/test/ClassParserTest.java +++ b/biz.aQute.bndlib.tests/test/test/ClassParserTest.java @@ -519,4 +519,15 @@ public void testNoProxyNewProxyInstance() throws Exception { // 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/src/aQute/bnd/osgi/Clazz.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java index b1918622e7..331857d66d 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java @@ -926,6 +926,11 @@ private synchronized Set parseClassFileData(DataInput in) throws Except // ... // 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;"); @@ -1254,7 +1259,11 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { ByteBuffer code = attribute.code.duplicate(); code.rewind(); int lastReference = -1; - List proxyInterfaces = new ArrayList<>(); // Track interface class constants for Proxy.newProxyInstance + // 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 = new ArrayList<>(); boolean inProxyArray = false; // Track if we're building a Class[] for proxy while (code.hasRemaining()) { int instruction = Byte.toUnsignedInt(code.get()); @@ -1297,6 +1306,8 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { 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 : { From 93c812ba4b7dc789a2bcbfe7f526d963500af035 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 06:03:42 +0000 Subject: [PATCH 4/4] Optimize proxy tracking with lazy ArrayList initialization Only create the proxyInterfaces ArrayList when proxy tracking is actually needed, avoiding unnecessary allocations for methods that don't use Proxy.newProxyInstance. Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java index 331857d66d..288727bca2 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java @@ -1263,7 +1263,7 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { // 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 = new ArrayList<>(); + 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()); @@ -1286,7 +1286,11 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { String className = constantPool.className(class_index); if ("java/lang/Class".equals(className)) { inProxyArray = true; - proxyInterfaces.clear(); + if (proxyInterfaces == null) { + proxyInterfaces = new ArrayList<>(); + } else { + proxyInterfaces.clear(); + } } } lastReference = -1; @@ -1329,7 +1333,7 @@ private void processCode(CodeAttribute attribute, ElementType elementType) { } } // Handle Proxy.newProxyInstance - process collected proxy interfaces - if (method_ref_index == newProxyInstance && !proxyInterfaces.isEmpty()) { + if (method_ref_index == newProxyInstance && proxyInterfaces != null && !proxyInterfaces.isEmpty()) { for (int classIndex : proxyInterfaces) { processProxyInterface(classIndex); }