This is an automated email from the ASF dual-hosted git repository. paulk-asert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 9d5fd190e7c248aa00784f41ff8b9a0384e5eb92 Author: Paul King <[email protected]> AuthorDate: Wed May 6 18:56:41 2026 +1000 GROOVY-11999: ProxyGeneratorAdapter NPE when proxy interfaces mix bootstrap and user classloaders --- .../groovy/runtime/ProxyGeneratorAdapter.java | 12 +++++-- .../lang/IntersectionClosureLiteralTest.groovy | 14 ++++++++ .../groovy/util/ProxyGeneratorAdapterTest.groovy | 39 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java b/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java index 34bd8ae551..fa8a32f554 100644 --- a/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java +++ b/src/main/java/org/codehaus/groovy/runtime/ProxyGeneratorAdapter.java @@ -840,11 +840,16 @@ public class ProxyGeneratorAdapter extends ClassVisitor { super(parent); if (interfaces != null) { for (Class<?> c : interfaces) { - if (c.getClassLoader() != parent) { + // GROOVY-11999: bootstrap-loaded classes (e.g., java.lang.Runnable, + // java.io.Serializable) report a null classloader. Don't store + // null in the extras list — bootstrap is reachable via every + // non-null parent's delegation chain anyway. + ClassLoader cl = c.getClassLoader(); + if (cl != null && cl != parent) { if (internalClassLoaders == null) internalClassLoaders = new ArrayList<>(interfaces.length); - if (!internalClassLoaders.contains(c.getClassLoader())) { - internalClassLoaders.add(c.getClassLoader()); + if (!internalClassLoaders.contains(cl)) { + internalClassLoaders.add(cl); } } } @@ -884,6 +889,7 @@ public class ProxyGeneratorAdapter extends ClassVisitor { // Not loaded, try to load it if (internalClassLoaders != null) { for (ClassLoader i : internalClassLoaders) { + if (i == null) continue; // GROOVY-11999: defensive, see InnerLoader ctor try { // Ignore parent delegation and just try to load locally loadedClass = i.loadClass(name); diff --git a/src/test/groovy/groovy/lang/IntersectionClosureLiteralTest.groovy b/src/test/groovy/groovy/lang/IntersectionClosureLiteralTest.groovy index b8bd4cee3b..801560e6b0 100644 --- a/src/test/groovy/groovy/lang/IntersectionClosureLiteralTest.groovy +++ b/src/test/groovy/groovy/lang/IntersectionClosureLiteralTest.groovy @@ -96,6 +96,20 @@ final class IntersectionClosureLiteralTest { ''') } + // GROOVY-11999: this previously NPE'd inside ProxyGeneratorAdapter when + // interfaces list mixed bootstrap (Runnable) and user (MyMarker) loaders. + @Test + void 'dynamic closure as (Runnable & MyMarker) builds a multi-interface proxy'() { + def shell = new GroovyShell() + shell.evaluate(''' + interface MyMarker {} + def c = ({ -> "hi" } as (Runnable & MyMarker)) + assert c instanceof Runnable + assert c instanceof MyMarker + c.run() + ''') + } + @Test void 'static cast form succeeds where dynamic would need a runtime proxy'() { // Without PR5, (R & MyMarker) cast form on a closure literal would throw diff --git a/src/test/groovy/groovy/util/ProxyGeneratorAdapterTest.groovy b/src/test/groovy/groovy/util/ProxyGeneratorAdapterTest.groovy index 73830e1f2d..9086715547 100644 --- a/src/test/groovy/groovy/util/ProxyGeneratorAdapterTest.groovy +++ b/src/test/groovy/groovy/util/ProxyGeneratorAdapterTest.groovy @@ -244,4 +244,43 @@ class ProxyGeneratorAdapterTest { static interface OtherInterface { int calc(int x) } + + static interface UserMarker {} // user-defined marker; classloader is the test classloader + + // GROOVY-11999: building a proxy whose interface list mixes a bootstrap-loaded + // interface (Runnable/Serializable) with a user-defined one used to NPE in + // InnerLoader because the bootstrap classloader (null) was added to the + // internalClassLoaders list and dereferenced during class definition. + @Test + void testProxyMixingBootstrapAndUserInterfaces() { + def closure = { -> /* doCall */ } + def closureMap = ['*': closure] + def adapter = new ProxyGeneratorAdapter( + closureMap, + Object, + [Runnable, UserMarker] as Class[], + this.class.classLoader, + false, + null) + def obj = adapter.proxy(closureMap, null) + assert obj instanceof Runnable + assert obj instanceof UserMarker + obj.run() // does not throw + } + + // GROOVY-11999: same scenario via the public ProxyGenerator entry point used + // by the runtime intersection-cast path (IntersectionCastSupport.asType). + @Test + void testInstantiateAggregateMixingBootstrapAndUserInterfaces() { + def calls = 0 + def proxy = ProxyGenerator.INSTANCE.instantiateAggregate( + ['run': { -> calls++ }], + [Runnable, java.io.Serializable, UserMarker] as List<Class>) + assert proxy instanceof Runnable + assert proxy instanceof java.io.Serializable + assert proxy instanceof UserMarker + proxy.run() + proxy.run() + assert calls == 2 + } }
