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

rmaucher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 385ad90ee66c9c530f13dbfafe05ca5ef7ffd631
Author: opencode <[email protected]>
AuthorDate: Tue Oct 20 08:53:00 2026 +0200

    Fix infinite loop in JsonFormatter for cyclic cause chains
    
    When a LogRecord throwable has a cyclic cause chain, the do/while loop
    in format() that walks getCause() never terminates, appending to the
    StringBuilder until the JVM runs out of memory, or hanging the writing
    thread indefinitely on a large heap. Throwable.initCause() only rejects
    a direct self-reference, so a cycle of length two or more is legal,
    plain application code. With AsyncFileHandler the single writer thread
    would be permanently lost, taking down all async logging.
    
    The walk of the chain now tracks the visited exceptions in an
    identity-based set and stops at the first repeated throwable, emitting
    it as a circular reference entry, in the manner of the output of
    Throwable.printStackTrace(). Non-cyclic chains produce byte-identical
    output. Added a regression test which, without the fix, fails with an
    OutOfMemoryError.
---
 java/org/apache/juli/JsonFormatter.java     | 12 ++++++++++++
 test/org/apache/juli/TestJsonFormatter.java | 25 +++++++++++++++++++++++++
 2 files changed, 37 insertions(+)

diff --git a/java/org/apache/juli/JsonFormatter.java 
b/java/org/apache/juli/JsonFormatter.java
index 3ed301c1e4..7e4e50284d 100644
--- a/java/org/apache/juli/JsonFormatter.java
+++ b/java/org/apache/juli/JsonFormatter.java
@@ -16,6 +16,9 @@
  */
 package org.apache.juli;
 
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
 import java.util.logging.LogManager;
 import java.util.logging.LogRecord;
 
@@ -93,12 +96,21 @@ public class JsonFormatter extends OneLineFormatter {
             // Stack trace
             sb.append("\"throwable\": [");
             boolean first = true;
+            // A cause chain may legally contain cycles (Throwable.initCause() 
only
+            // rejects a direct self-reference) so track the visited 
exceptions and
+            // stop the walk at the first repeated throwable, reporting it as a
+            // circular reference in the manner of Throwable.printStackTrace().
+            Set<Throwable> seen = Collections.newSetFromMap(new 
IdentityHashMap<>());
             do {
                 if (!first) {
                     sb.append(',');
                 } else {
                     first = false;
                 }
+                if (!seen.add(t)) {
+                    sb.append("\"[Circular reference] 
").append(JSONFilter.escape(t.toString())).append('\"');
+                    break;
+                }
                 
sb.append('\"').append(JSONFilter.escape(t.toString())).append('\"');
                 for (StackTraceElement element : t.getStackTrace()) {
                     sb.append(',').append('\"').append(' 
').append(JSONFilter.escape(element.toString())).append('\"');
diff --git a/test/org/apache/juli/TestJsonFormatter.java 
b/test/org/apache/juli/TestJsonFormatter.java
index 0e52633eaf..c82f1c260e 100644
--- a/test/org/apache/juli/TestJsonFormatter.java
+++ b/test/org/apache/juli/TestJsonFormatter.java
@@ -55,6 +55,31 @@ public class TestJsonFormatter {
         Assert.assertEquals(trace.get(0), "java.lang.IllegalStateException: 
Bad state");
     }
 
+    @Test
+    public void testCircularCause() throws Exception {
+
+        Exception a = new Exception("A");
+        Exception b = new Exception("B");
+        a.initCause(b);
+        b.initCause(a);
+
+        LogRecord logRecord = new LogRecord(Level.FINE, "Test log message");
+        logRecord.setThrown(a);
+
+        Formatter formatter = new JsonFormatter();
+
+        // Without cycle detection this call would never complete
+        String result = formatter.format(logRecord);
+
+        // Verify JSON content
+        Assert.assertTrue(result.startsWith("{"));
+        JSONParser parser = new JSONParser(new StringReader(result));
+        LinkedHashMap<String,Object> json = parser.object();
+        @SuppressWarnings("unchecked")
+        ArrayList<Object> trace = (ArrayList<Object>) json.get("throwable");
+        Assert.assertTrue(String.valueOf(trace.get(trace.size() - 
1)).startsWith("[Circular reference]"));
+    }
+
     @Test
     public void testEscaping() {
         String[][] tests = new String[][] {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to