This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 8cd772b764 Fix circular config import StackOverflow (juneau-config)
8cd772b764 is described below

commit 8cd772b76409e00e5654e3313bbb34303313c249
Author: James Bognar <[email protected]>
AuthorDate: Tue Jun 9 07:25:16 2026 -0400

    Fix circular config import StackOverflow (juneau-config)
    
    Loading a config with a circular import recursed infinitely
    (ConfigMap.loadIni -> ConfigStore.getMap -> new ConfigMap -> load -> 
loadIni)
    with no cycle detection. Under the JaCoCo agent the resulting 
StackOverflowError
    struck during ThrowableUtils.<clinit>, poisoning the class and cascading
    NoClassDefFoundError across the whole juneau-config test suite.
    
    - Add thread-local cycle detection in ConfigStore.getMap that throws a clean
      ConfigException naming the import loop before recursing.
    - Remove the brittle catch(StackOverflowError) in ConfigMap.loadIni.
    
    Verified: full juneau-config module (436 tests) green under coverage 
instrumentation.
---
 .../apache/juneau/config/internal/ConfigMap.java   |  9 +++-----
 .../apache/juneau/config/store/ConfigStore.java    | 26 +++++++++++++++++++++-
 .../logs/jetty-requests.log.112213788              |  0
 juneau-utest/test-run-history.tsv                  |  1 +
 4 files changed, 29 insertions(+), 7 deletions(-)

diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
index eb3e018cca..efdea7e195 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
@@ -865,12 +865,9 @@ public class ConfigMap implements ConfigStoreListener {
                                                if (! (isEmpty(l3) || 
firstChar(l3) == '#'))
                                                        throw new 
ConfigException("Invalid import config name found in configuration:  {0}", 
line);
                                                var importName = l2.trim();
-                                               try {
-                                                       if (! 
imports2.containsKey(importName))
-                                                               
imports2.put(importName, store.getMap(importName, format));
-                                               } catch 
(@SuppressWarnings("unused") StackOverflowError e) {
-                                                       throw ioex("Import loop 
detected in configuration ''{0}''->''{1}''", name, importName);
-                                               }
+                                               // Circular imports are 
detected in ConfigStore.getMap() which throws a clean ConfigException.
+                                               if (! 
imports2.containsKey(importName))
+                                                       
imports2.put(importName, store.getMap(importName, format));
                                        }
                                }
                                lines.add(line);
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
index c526d8f339..b90135ecf9 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
@@ -84,6 +84,11 @@ public abstract class ConfigStore extends Context implements 
Closeable {
        private final ConcurrentHashMap<String,ConfigMap> configMaps = new 
ConcurrentHashMap<>();
        private final ConcurrentHashMap<String,Set<ConfigStoreListener>> 
listeners = new ConcurrentHashMap<>();
 
+       // Tracks the chain of configs currently being constructed on this 
thread so that a circular import
+       // (a config that directly or indirectly imports itself) can be 
detected and reported cleanly instead
+       // of recursing infinitely through getMap()->new 
ConfigMap()->load()->getMap() until the stack overflows.
+       private static final ThreadLocal<Set<String>> LOADING = 
ThreadLocal.withInitial(LinkedHashSet::new);
+
        /**
         * Constructor.
         *
@@ -129,7 +134,17 @@ public abstract class ConfigStore extends Context 
implements Closeable {
                var cm = configMaps.get(key);
                if (nn(cm))
                        return cm;
-               cm = new ConfigMap(this, name, format2);
+
+               // Detect circular imports before recursing into ConfigMap 
construction.
+               var loading = LOADING.get();
+               if (! loading.add(key))
+                       throw new ConfigException("Import loop detected in 
configuration:  {0}", importChain(loading, name));
+               try {
+                       cm = new ConfigMap(this, name, format2);
+               } finally {
+                       loading.remove(key);
+               }
+
                var cm2 = configMaps.putIfAbsent(key, cm);
                if (nn(cm2))
                        return cm2;
@@ -137,6 +152,15 @@ public abstract class ConfigStore extends Context 
implements Closeable {
                return cm;
        }
 
+       // Renders the chain of configs currently being loaded (e.g. "B -> A2 
-> A1 -> A2") for a circular-import error message.
+       // The entries in the loading set are cache keys of the form 
"<formatId>:<name>", so the format prefix is stripped.
+       private static String importChain(Set<String> loading, String reentry) {
+               var sb = new StringBuilder();
+               for (var key : loading)
+                       sb.append(key.substring(key.indexOf(':') + 1)).append(" 
-> ");
+               return sb.append(reentry).toString();
+       }
+
        /**
         * Returns the contents of the configuration file.
         *
diff --git 
a/juneau-microservice/juneau-microservice-jetty/logs/jetty-requests.log.112213788
 
b/juneau-microservice/juneau-microservice-jetty/logs/jetty-requests.log.112213788
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/juneau-utest/test-run-history.tsv 
b/juneau-utest/test-run-history.tsv
index 7ac3054d10..31325aa3b8 100644
--- a/juneau-utest/test-run-history.tsv
+++ b/juneau-utest/test-run-history.tsv
@@ -76,3 +76,4 @@ timestamp     git_sha branch  tests_run       failures        
errors  skipped surefire_sec    wall_sec
 2026-06-07T16:22:46Z   a0094abd0a55    master  130509  0       0       26      
178
 2026-06-08T20:43:21Z   bdbfa8182af7    master  125262  0       0       26      
360
 2026-06-09T01:09:06Z   ee60417c6dc6    master  94758   0       0       24      
273
+2026-06-09T11:24:07Z   b578ccffc805    master  94758   0       0       24      
265

Reply via email to