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

vy pushed a commit to branch 2.x-fix-api-recursive-formatting
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit a258a589234d3fe11da7f0f7656462ad714ed1ff
Author: Volkan Yazıcı <[email protected]>
AuthorDate: Fri Feb 9 10:48:06 2024 +0100

    Fix `StringBuilder` cache corruption on recursive access
---
 .../logging/log4j/internal/ArrayQueueTest.java     | 101 +++++++++++++++++++++
 ...eterizedMessageRecursiveFormattingTestBase.java |  53 +++++++++++
 ...ageRecursiveFormattingWithThreadLocalsTest.java |  31 +++++++
 ...RecursiveFormattingWithoutThreadLocalsTest.java |  31 +++++++
 .../apache/logging/log4j/internal/ArrayQueue.java  | 101 +++++++++++++++++++++
 .../log4j/internal/DummyStringBuilderRecycler.java |  35 +++++++
 .../QueueingThreadLocalStringBuilderRecycler.java  |  55 +++++++++++
 .../log4j/internal/StringBuilderRecycler.java      |  61 +++++++++++++
 .../internal/ThreadLocalStringBuilderRecycler.java |  54 +++++++++++
 .../log4j/message/ParameterizedMessage.java        |  50 +++++-----
 .../org/apache/logging/log4j/util/Strings.java     |  11 ++-
 .../.2.x.x/fix_api_recursive_formatting.xml        |   9 ++
 12 files changed, 562 insertions(+), 30 deletions(-)

diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
new file mode 100644
index 0000000000..e063e7f81c
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.ArrayBlockingQueue;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class ArrayQueueTest {
+
+    @ParameterizedTest
+    @ValueSource(ints = {-1, 0})
+    void invalid_capacity_should_not_be_allowed(final int invalidCapacity) {
+        assertThatThrownBy(() -> new ArrayQueue<>(invalidCapacity))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("invalid capacity: " + invalidCapacity);
+    }
+
+    @Test
+    void should_work_with_capacity_1() {
+
+        // Verify initials
+        final Queue<String> queue = new ArrayQueue<>(1);
+        assertThat(queue.size()).isEqualTo(0);
+        assertThat(queue.peek()).isNull();
+        assertThat(queue.poll()).isNull();
+        assertThat(queue).isEmpty();
+
+        // Verify enqueue & deque
+        assertThat(queue.offer("foo")).isTrue();
+        assertThat(queue.offer("bar")).isFalse();
+        assertThat(queue.size()).isEqualTo(1);
+        assertThat(queue).containsOnly("foo");
+        assertThat(queue.peek()).isEqualTo("foo");
+        assertThat(queue.poll()).isEqualTo("foo");
+
+        // Verify final state
+        assertThat(queue.size()).isEqualTo(0);
+        assertThat(queue.peek()).isNull();
+        assertThat(queue.poll()).isNull();
+        assertThat(queue).isEmpty();
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        "1,0.3", "1,0.5", "1,0.8", "2,0.3", "2,0.5", "2,0.8", "3,0.3", 
"3,0.5", "3,0.8", "4,0.3", "4,0.5", "4,0.8"
+    })
+    void ops_should_match_with_std_lib(final int capacity, final double 
pollRatio) {
+
+        // Set the stage
+        final Random random = new Random(0);
+        final int opCount = random.nextInt(100);
+        final Queue<String> queueRef = new ArrayBlockingQueue<>(capacity);
+        final Queue<String> queueTarget = new ArrayQueue<>(capacity);
+
+        for (int opIndex = 0; opIndex < opCount; opIndex++) {
+
+            // Verify entry
+            assertThat(queueTarget.size()).isEqualTo(queueRef.size());
+            assertThat(queueTarget.peek()).isEqualTo(queueRef.peek());
+            assertThat(queueTarget).containsExactlyElementsOf(queueRef);
+
+            // Is this a `poll()`?
+            if (pollRatio >= random.nextDouble()) {
+                assertThat(queueTarget.poll()).isEqualTo(queueRef.poll());
+            }
+
+            // Then this is an `offer()`
+            else {
+                final String item = "op@" + opIndex;
+                
assertThat(queueTarget.offer(item)).isEqualTo(queueRef.offer(item));
+            }
+
+            // Verify exit
+            assertThat(queueTarget.size()).isEqualTo(queueRef.size());
+            assertThat(queueTarget.peek()).isEqualTo(queueRef.peek());
+            assertThat(queueTarget).containsExactlyElementsOf(queueRef);
+        }
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingTestBase.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingTestBase.java
new file mode 100644
index 0000000000..072696029a
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingTestBase.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.message;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.logging.log4j.util.Constants;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link ParameterizedMessage#getFormattedMessage()} when formatted 
arguments causes another {@code ParameterizedMessage#getFormattedMessage()} 
(i.e., recursive) invocation.
+ */
+abstract class ParameterizedMessageRecursiveFormattingTestBase {
+
+    private final boolean threadLocalsEnabled;
+
+    ParameterizedMessageRecursiveFormattingTestBase(boolean 
threadLocalsEnabled) {
+        this.threadLocalsEnabled = threadLocalsEnabled;
+    }
+
+    @Test
+    void thread_locals_toggle_should_match() {
+        
assertThat(Constants.ENABLE_THREADLOCALS).isEqualTo(threadLocalsEnabled);
+    }
+
+    @Test
+    void recursion_should_not_corrupt_formatting() {
+        final Object argInvokingParameterizedMessageFormatting = new Object() {
+            @Override
+            public String toString() {
+                return new ParameterizedMessage("bar {}", 
"baz").getFormattedMessage();
+            }
+        };
+        final ParameterizedMessage message =
+                new ParameterizedMessage("foo {}", 
argInvokingParameterizedMessageFormatting);
+        final String actualText = message.getFormattedMessage();
+        assertThat(actualText).isEqualTo("foo bar baz");
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingWithThreadLocalsTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingWithThreadLocalsTest.java
new file mode 100644
index 0000000000..6496a5f12f
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingWithThreadLocalsTest.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.message;
+
+import org.junitpioneer.jupiter.SetSystemProperty;
+
+/**
+ * {@link ParameterizedMessageRecursiveFormattingTestBase} subclass with 
thread locals disabled, i.e, with {@link StringBuilder} recycling.
+ */
+@SetSystemProperty(key = "log4j2.enable.threadlocals", value = "true")
+class ParameterizedMessageRecursiveFormattingWithThreadLocalsTest
+        extends ParameterizedMessageRecursiveFormattingTestBase {
+
+    ParameterizedMessageRecursiveFormattingWithThreadLocalsTest() {
+        super(true);
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest.java
new file mode 100644
index 0000000000..456d271d87
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.message;
+
+import org.junitpioneer.jupiter.SetSystemProperty;
+
+/**
+ * {@link ParameterizedMessageRecursiveFormattingTestBase} subclass with 
thread locals disabled, i.e, no {@link StringBuilder} recycling.
+ */
+@SetSystemProperty(key = "log4j2.enable.threadlocals", value = "false")
+class ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest
+        extends ParameterizedMessageRecursiveFormattingTestBase {
+
+    ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest() {
+        super(false);
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java
new file mode 100644
index 0000000000..6777d56930
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import java.util.AbstractQueue;
+import java.util.Iterator;
+import java.util.stream.IntStream;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.apache.logging.log4j.util.InternalApi;
+
+/**
+ * An array-backed, fixed-length, not-thread-safe {@link java.util.Queue} 
implementation.
+ *
+ * @param <E> the element type
+ * @since 2.23.0
+ */
+@InternalApi
+@NotThreadSafe
+final class ArrayQueue<E> extends AbstractQueue<E> {
+
+    private final E[] buffer;
+
+    private int head;
+
+    private int tail;
+
+    private int size;
+
+    @SuppressWarnings("unchecked")
+    ArrayQueue(final int capacity) {
+        if (capacity < 1) {
+            throw new IllegalArgumentException("invalid capacity: " + 
capacity);
+        }
+        buffer = (E[]) new Object[capacity];
+        head = 0;
+        tail = -1;
+        size = 0;
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        int[] i = {head};
+        return IntStream.range(0, size)
+                .mapToObj(ignored -> {
+                    final E item = buffer[i[0]];
+                    i[0] = (i[0] + 1) % buffer.length;
+                    return item;
+                })
+                .iterator();
+    }
+
+    @Override
+    public boolean offer(final E item) {
+        if (size == buffer.length) {
+            return false;
+        }
+        tail = (tail + 1) % buffer.length;
+        buffer[tail] = item;
+        size++;
+        return true;
+    }
+
+    @Override
+    public E poll() {
+        if (isEmpty()) {
+            return null;
+        }
+        final E item = buffer[head];
+        buffer[head] = null; // Clear refs for GC
+        head = (head + 1) % buffer.length;
+        size--;
+        return item;
+    }
+
+    @Override
+    public E peek() {
+        if (isEmpty()) {
+            return null;
+        }
+        return buffer[head];
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/DummyStringBuilderRecycler.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/DummyStringBuilderRecycler.java
new file mode 100644
index 0000000000..9deecccce5
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/DummyStringBuilderRecycler.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+/**
+ * A {@link StringBuilderRecycler} that <b>always</b> allocates a new instance.
+ *
+ * @since 2.23.0
+ */
+final class DummyStringBuilderRecycler implements StringBuilderRecycler {
+
+    DummyStringBuilderRecycler() {}
+
+    @Override
+    public StringBuilder acquire() {
+        return new StringBuilder();
+    }
+
+    @Override
+    public void release(final StringBuilder stringBuilder) {}
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingThreadLocalStringBuilderRecycler.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingThreadLocalStringBuilderRecycler.java
new file mode 100644
index 0000000000..d867a8df28
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingThreadLocalStringBuilderRecycler.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Queue;
+import org.apache.logging.log4j.util.StringBuilders;
+
+/**
+ * A {@link StringBuilderRecycler} that keeps a {@link ThreadLocal} queue to 
recycle instances.
+ * <p>
+ * When the queue capacity is exceeded, {@link #acquire()} starts allocating 
new instances.
+ * The queue provides convenience when recursive {@link #acquire()} calls are 
expected.
+ * If not, use {@link ThreadLocalStringBuilderRecycler} instead to avoid the 
cost of maintaining a queue.
+ * </p>
+ *
+ * @since 2.23.0
+ */
+final class QueueingThreadLocalStringBuilderRecycler implements 
StringBuilderRecycler {
+
+    private final ThreadLocal<Queue<StringBuilder>> queueHolder;
+
+    QueueingThreadLocalStringBuilderRecycler(final int 
threadLocalQueueCapacity) {
+        this.queueHolder = ThreadLocal.withInitial(() -> new 
ArrayQueue<>(threadLocalQueueCapacity));
+    }
+
+    @Override
+    public StringBuilder acquire() {
+        Queue<StringBuilder> queue = queueHolder.get();
+        @Nullable final StringBuilder stringBuilder = queue.poll();
+        return stringBuilder != null ? stringBuilder : new StringBuilder();
+    }
+
+    @Override
+    public void release(final StringBuilder stringBuilder) {
+        StringBuilders.trimToMaxSize(stringBuilder, 
MAX_STRING_BUILDER_CAPACITY);
+        stringBuilder.setLength(0);
+        final Queue<StringBuilder> queue = queueHolder.get();
+        queue.offer(stringBuilder);
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringBuilderRecycler.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringBuilderRecycler.java
new file mode 100644
index 0000000000..3ff96c1152
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringBuilderRecycler.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import org.apache.logging.log4j.util.Constants;
+import org.apache.logging.log4j.util.InternalApi;
+
+/**
+ * An <b>internal</b> tool to recycle {@link StringBuilder} instances to 
implement garbage-free formatting.
+ * <h1>Internal usage only!</h1>
+ * <p>
+ * This interface is only intended to be used for internals of the Log4j API.
+ * Log4j API can break this contract at any time to adapt to its needs.
+ * <b>{@link StringBuilderRecycler} should not be used by any other code 
except the Log4j API!</b>
+ * </p>
+ *
+ * @since 2.23.0
+ */
+@InternalApi
+public interface StringBuilderRecycler {
+
+    int MAX_STRING_BUILDER_CAPACITY = Constants.MAX_REUSABLE_MESSAGE_SIZE;
+
+    StringBuilder acquire();
+
+    void release(final StringBuilder stringBuilder);
+
+    /**
+     * Creates a {@link StringBuilderRecycler} using the current environment, 
e.g., {@link Constants}, {@link org.apache.logging.log4j.util.PropertiesUtil}, 
etc.
+     *
+     * @param recursionDepth This value indicates the maximum recursion depth 
to be supported before falling back to allocating new instances.
+     *                       For instance, {@link 
QueueingThreadLocalStringBuilderRecycler} uses this to set the capacity of the 
instances to be pooled in a {@link ThreadLocal}.
+     * @return a new {@link StringBuilderRecycler} instance
+     */
+    static StringBuilderRecycler ofEnvironment(final int recursionDepth) {
+        if (recursionDepth < 0) {
+            throw new IllegalArgumentException("was expecting a positive 
`recursionDepth`, found: " + recursionDepth);
+        }
+        if (!Constants.ENABLE_THREADLOCALS) {
+            return new DummyStringBuilderRecycler();
+        } else if (recursionDepth == 0) {
+            return new ThreadLocalStringBuilderRecycler();
+        } else {
+            return new 
QueueingThreadLocalStringBuilderRecycler(recursionDepth);
+        }
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalStringBuilderRecycler.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalStringBuilderRecycler.java
new file mode 100644
index 0000000000..6638fb6e6a
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalStringBuilderRecycler.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import org.apache.logging.log4j.util.StringBuilders;
+
+/**
+ * A {@link StringBuilderRecycler} that stores a single {@link StringBuilder} 
in a {@link ThreadLocal}.
+ * <p>
+ * This implementation causes new instance creation when {@link #acquire()} is 
called recursively.
+ * If your use case needs recursion support, prefer {@link 
QueueingThreadLocalStringBuilderRecycler} instead.
+ * </p>
+ *
+ * @since 2.23.0
+ */
+final class ThreadLocalStringBuilderRecycler implements StringBuilderRecycler {
+
+    private final ThreadLocal<StringBuilder> stringBuilderHolder = 
ThreadLocal.withInitial(StringBuilder::new);
+
+    ThreadLocalStringBuilderRecycler() {}
+
+    @Override
+    public StringBuilder acquire() {
+        @Nullable StringBuilder stringBuilder = stringBuilderHolder.get();
+        if (stringBuilder == null) {
+            return new StringBuilder();
+        }
+        // noinspection ThreadLocalSetWithNull
+        stringBuilderHolder.set(null);
+        return stringBuilder;
+    }
+
+    @Override
+    public void release(final StringBuilder stringBuilder) {
+        StringBuilders.trimToMaxSize(stringBuilder, 
MAX_STRING_BUILDER_CAPACITY);
+        stringBuilder.setLength(0);
+        stringBuilderHolder.set(stringBuilder);
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
index db325327b7..30677a8565 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
@@ -25,10 +25,9 @@ import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.Objects;
+import org.apache.logging.log4j.internal.StringBuilderRecycler;
 import 
org.apache.logging.log4j.message.ParameterFormatter.MessagePatternAnalysis;
-import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.StringBuilderFormattable;
-import org.apache.logging.log4j.util.StringBuilders;
 import org.apache.logging.log4j.util.internal.SerializationUtil;
 
 /**
@@ -44,10 +43,6 @@ import 
org.apache.logging.log4j.util.internal.SerializationUtil;
  * {bar
  * {buzz}
  * </pre>
- * <p>
- * This class was originally written for <a 
href="http://lilithapp.com/";>Lilith</a> by Jörn Huxhorn and licensed under the 
LGPL.
- * It has been relicensed here with his permission providing that this 
attribution remain.
- * </p>
  */
 public class ParameterizedMessage implements Message, StringBuilderFormattable 
{
 
@@ -83,9 +78,15 @@ public class ParameterizedMessage implements Message, 
StringBuilderFormattable {
 
     private static final long serialVersionUID = -665975803997290697L;
 
-    private static final ThreadLocal<StringBuilder> STRING_BUILDER_HOLDER = 
Constants.ENABLE_THREADLOCALS
-            ? ThreadLocal.withInitial(() -> new 
StringBuilder(Constants.MAX_REUSABLE_MESSAGE_SIZE))
-            : null;
+    private static final StringBuilderRecycler STRING_BUILDER_RECYCLER = 
StringBuilderRecycler.ofEnvironment(
+            // This value indicates the maximum recursion depth supported 
before the recycler starts creating new
+            // instances.
+            // Consider a `ParameterizedMessage` containing an argument whose 
`toString()` causes another
+            // `ParameterizedMessage` formatting.
+            // This value indicates the depth we support garbage-free 
formatting in such nested formatting situations.
+            // When this depth is exceeded, code still works, but starts 
generating garbage due to new `StringBuilder`
+            // allocations.
+            3);
 
     private final String pattern;
 
@@ -242,16 +243,13 @@ public class ParameterizedMessage implements Message, 
StringBuilderFormattable {
     @Override
     public String getFormattedMessage() {
         if (formattedMessage == null) {
-            if (STRING_BUILDER_HOLDER != null) {
-                final StringBuilder buffer = STRING_BUILDER_HOLDER.get();
+            final StringBuilder buffer = STRING_BUILDER_RECYCLER.acquire();
+            try {
                 buffer.setLength(0);
                 formatTo(buffer);
                 formattedMessage = buffer.toString();
-                StringBuilders.trimToMaxSize(buffer, 
Constants.MAX_REUSABLE_MESSAGE_SIZE);
-            } else {
-                final StringBuilder buffer = new StringBuilder();
-                formatTo(buffer);
-                formattedMessage = buffer.toString();
+            } finally {
+                STRING_BUILDER_RECYCLER.release(buffer);
             }
         }
         return formattedMessage;
@@ -355,26 +353,28 @@ public class ParameterizedMessage implements Message, 
StringBuilderFormattable {
 
     @Override
     public String toString() {
-        return "ParameterizedMessage[messagePattern=" + pattern + ", 
stringArgs=" + Arrays.toString(args)
-                + ", throwable=" + throwable + ']';
+        // Avoid formatting arguments!
+        // It can cause recursion, which can become pretty unpleasant while 
troubleshooting.
+        return "ParameterizedMessage[messagePattern=" + pattern + ", 
argCount=" + args.length + ", throwableProvided="
+                + (throwable != null) + ']';
     }
 
     private void writeObject(final ObjectOutputStream out) throws IOException {
         out.defaultWriteObject();
         out.writeInt(args.length);
-        for (int i = 0; i < args.length; i++) {
-            SerializationUtil.writeWrappedObject(
-                    args[i] instanceof Serializable ? (Serializable) args[i] : 
String.valueOf(args[i]), out);
+        for (final Object arg : args) {
+            final Serializable serializableArg = arg instanceof Serializable ? 
(Serializable) arg : String.valueOf(arg);
+            SerializationUtil.writeWrappedObject(serializableArg, out);
         }
     }
 
     private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
         SerializationUtil.assertFiltered(in);
         in.defaultReadObject();
-        final int length = in.readInt();
-        args = new Object[length];
-        for (int i = 0; i < args.length; i++) {
-            args[i] = SerializationUtil.readWrappedObject(in);
+        final int argCount = in.readInt();
+        args = new Object[argCount];
+        for (int argIndex = 0; argIndex < args.length; argIndex++) {
+            args[argIndex] = SerializationUtil.readWrappedObject(in);
         }
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
index a1a5bb1aab..512784d3c8 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.util;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.Objects;
+import org.apache.logging.log4j.internal.StringBuilderRecycler;
 
 /**
  * <em>Consider this class private.</em>
@@ -28,7 +29,7 @@ import java.util.Objects;
 @InternalApi
 public final class Strings {
 
-    private static final ThreadLocal<StringBuilder> tempStr = 
ThreadLocal.withInitial(StringBuilder::new);
+    private static final StringBuilderRecycler STRING_BUILDER_RECYCLER = 
StringBuilderRecycler.ofEnvironment(0);
 
     /**
      * The empty string.
@@ -318,11 +319,11 @@ public final class Strings {
         } else if (isEmpty(str2)) {
             return str1;
         }
-        final StringBuilder sb = tempStr.get();
+        final StringBuilder sb = STRING_BUILDER_RECYCLER.acquire();
         try {
             return sb.append(str1).append(str2).toString();
         } finally {
-            sb.setLength(0);
+            STRING_BUILDER_RECYCLER.release(sb);
         }
     }
 
@@ -338,14 +339,14 @@ public final class Strings {
         if (count < 0) {
             throw new IllegalArgumentException("count");
         }
-        final StringBuilder sb = tempStr.get();
+        final StringBuilder sb = STRING_BUILDER_RECYCLER.acquire();
         try {
             for (int index = 0; index < count; index++) {
                 sb.append(str);
             }
             return sb.toString();
         } finally {
-            sb.setLength(0);
+            STRING_BUILDER_RECYCLER.release(sb);
         }
     }
 }
diff --git a/src/changelog/.2.x.x/fix_api_recursive_formatting.xml 
b/src/changelog/.2.x.x/fix_api_recursive_formatting.xml
new file mode 100644
index 0000000000..558152b60c
--- /dev/null
+++ b/src/changelog/.2.x.x/fix_api_recursive_formatting.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns="http://logging.apache.org/log4j/changelog";
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog 
https://logging.apache.org/log4j/changelog-0.1.3.xsd";
+       type="fixed">
+  <description format="asciidoc">
+    Fix `StringBuilder` cache corruption on recursive access
+  </description>
+</entry>

Reply via email to