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
+    }
 }

Reply via email to