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

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

commit 2cb589ea80f0fdb6a0d621d3d13f7a5702b623f2
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 d954df7664..757fe61795 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 cf8938bd52..9640b4a005 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