Skip to content

Commit c6832d6

Browse files
authored
Fix StringBuilder cache corruption on recursive access (#2275)
1 parent 090123c commit c6832d6

File tree

7 files changed

+186
-51
lines changed

7 files changed

+186
-51
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.message;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import org.apache.logging.log4j.util.Constants;
22+
import org.junit.jupiter.api.Test;
23+
24+
/**
25+
* Tests {@link ParameterizedMessage#getFormattedMessage()} when formatted arguments causes another {@code ParameterizedMessage#getFormattedMessage()} (i.e., recursive) invocation.
26+
*/
27+
abstract class ParameterizedMessageRecursiveFormattingTestBase {
28+
29+
private final boolean threadLocalsEnabled;
30+
31+
ParameterizedMessageRecursiveFormattingTestBase(boolean threadLocalsEnabled) {
32+
this.threadLocalsEnabled = threadLocalsEnabled;
33+
}
34+
35+
@Test
36+
void thread_locals_toggle_should_match() {
37+
assertThat(Constants.ENABLE_THREADLOCALS).isEqualTo(threadLocalsEnabled);
38+
}
39+
40+
@Test
41+
void recursion_should_not_corrupt_formatting() {
42+
final Object argInvokingParameterizedMessageFormatting = new Object() {
43+
@Override
44+
public String toString() {
45+
return new ParameterizedMessage("bar {}", "baz").getFormattedMessage();
46+
}
47+
};
48+
final ParameterizedMessage message =
49+
new ParameterizedMessage("foo {}", argInvokingParameterizedMessageFormatting);
50+
final String actualText = message.getFormattedMessage();
51+
assertThat(actualText).isEqualTo("foo bar baz");
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.message;
18+
19+
import org.junitpioneer.jupiter.SetSystemProperty;
20+
21+
/**
22+
* {@link ParameterizedMessageRecursiveFormattingTestBase} subclass with thread locals disabled, i.e, with {@link StringBuilder} recycling.
23+
*/
24+
@SetSystemProperty(key = "log4j2.enable.threadlocals", value = "true")
25+
class ParameterizedMessageRecursiveFormattingWithThreadLocalsTest
26+
extends ParameterizedMessageRecursiveFormattingTestBase {
27+
28+
ParameterizedMessageRecursiveFormattingWithThreadLocalsTest() {
29+
super(true);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.message;
18+
19+
import org.junitpioneer.jupiter.SetSystemProperty;
20+
21+
/**
22+
* {@link ParameterizedMessageRecursiveFormattingTestBase} subclass with thread locals disabled, i.e, no {@link StringBuilder} recycling.
23+
*/
24+
@SetSystemProperty(key = "log4j2.enable.threadlocals", value = "false")
25+
class ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest
26+
extends ParameterizedMessageRecursiveFormattingTestBase {
27+
28+
ParameterizedMessageRecursiveFormattingWithoutThreadLocalsTest() {
29+
super(false);
30+
}
31+
}

log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.logging.log4j.message;
1818

1919
import static org.apache.logging.log4j.message.ParameterFormatter.analyzePattern;
20+
import static org.apache.logging.log4j.util.StringBuilders.trimToMaxSize;
2021

2122
import com.google.errorprone.annotations.InlineMe;
2223
import java.io.IOException;
@@ -28,7 +29,6 @@
2829
import org.apache.logging.log4j.message.ParameterFormatter.MessagePatternAnalysis;
2930
import org.apache.logging.log4j.util.Constants;
3031
import org.apache.logging.log4j.util.StringBuilderFormattable;
31-
import org.apache.logging.log4j.util.StringBuilders;
3232
import org.apache.logging.log4j.util.internal.SerializationUtil;
3333

3434
/**
@@ -44,10 +44,6 @@
4444
* {bar
4545
* {buzz}
4646
* </pre>
47-
* <p>
48-
* This class was originally written for <a href="http://lilithapp.com/">Lilith</a> by Jörn Huxhorn and licensed under the LGPL.
49-
* It has been relicensed here with his permission providing that this attribution remain.
50-
* </p>
5147
*/
5248
public class ParameterizedMessage implements Message, StringBuilderFormattable {
5349

@@ -83,9 +79,15 @@ public class ParameterizedMessage implements Message, StringBuilderFormattable {
8379

8480
private static final long serialVersionUID = -665975803997290697L;
8581

86-
private static final ThreadLocal<StringBuilder> STRING_BUILDER_HOLDER = Constants.ENABLE_THREADLOCALS
87-
? ThreadLocal.withInitial(() -> new StringBuilder(Constants.MAX_REUSABLE_MESSAGE_SIZE))
88-
: null;
82+
private static final ThreadLocal<FormatBufferHolder> FORMAT_BUFFER_HOLDER_REF =
83+
Constants.ENABLE_THREADLOCALS ? ThreadLocal.withInitial(FormatBufferHolder::new) : null;
84+
85+
private static final class FormatBufferHolder {
86+
87+
private final StringBuilder buffer = new StringBuilder(Constants.MAX_REUSABLE_MESSAGE_SIZE);
88+
89+
private boolean used = false;
90+
}
8991

9092
private final String pattern;
9193

@@ -242,17 +244,26 @@ public Throwable getThrowable() {
242244
@Override
243245
public String getFormattedMessage() {
244246
if (formattedMessage == null) {
245-
if (STRING_BUILDER_HOLDER != null) {
246-
final StringBuilder buffer = STRING_BUILDER_HOLDER.get();
247-
buffer.setLength(0);
248-
formatTo(buffer);
249-
formattedMessage = buffer.toString();
250-
StringBuilders.trimToMaxSize(buffer, Constants.MAX_REUSABLE_MESSAGE_SIZE);
251-
} else {
252-
final StringBuilder buffer = new StringBuilder();
247+
final FormatBufferHolder bufferHolder;
248+
// If there isn't a format buffer to reuse
249+
if (FORMAT_BUFFER_HOLDER_REF == null || (bufferHolder = FORMAT_BUFFER_HOLDER_REF.get()).used) {
250+
final StringBuilder buffer = new StringBuilder(Constants.MAX_REUSABLE_MESSAGE_SIZE);
253251
formatTo(buffer);
254252
formattedMessage = buffer.toString();
255253
}
254+
// If there is a format buffer to reuse
255+
else {
256+
bufferHolder.used = true;
257+
final StringBuilder buffer = bufferHolder.buffer;
258+
try {
259+
formatTo(buffer);
260+
formattedMessage = buffer.toString();
261+
} finally {
262+
trimToMaxSize(buffer, Constants.MAX_REUSABLE_MESSAGE_SIZE);
263+
buffer.setLength(0);
264+
bufferHolder.used = false;
265+
}
266+
}
256267
}
257268
return formattedMessage;
258269
}
@@ -355,26 +366,28 @@ public static String identityToString(final Object obj) {
355366

356367
@Override
357368
public String toString() {
358-
return "ParameterizedMessage[messagePattern=" + pattern + ", stringArgs=" + Arrays.toString(args)
359-
+ ", throwable=" + throwable + ']';
369+
// Avoid formatting arguments!
370+
// It can cause recursion, which can become pretty unpleasant while troubleshooting.
371+
return "ParameterizedMessage[messagePattern=" + pattern + ", argCount=" + args.length + ", throwableProvided="
372+
+ (throwable != null) + ']';
360373
}
361374

362375
private void writeObject(final ObjectOutputStream out) throws IOException {
363376
out.defaultWriteObject();
364377
out.writeInt(args.length);
365-
for (int i = 0; i < args.length; i++) {
366-
SerializationUtil.writeWrappedObject(
367-
args[i] instanceof Serializable ? (Serializable) args[i] : String.valueOf(args[i]), out);
378+
for (final Object arg : args) {
379+
final Serializable serializableArg = arg instanceof Serializable ? (Serializable) arg : String.valueOf(arg);
380+
SerializationUtil.writeWrappedObject(serializableArg, out);
368381
}
369382
}
370383

371384
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
372385
SerializationUtil.assertFiltered(in);
373386
in.defaultReadObject();
374-
final int length = in.readInt();
375-
args = new Object[length];
376-
for (int i = 0; i < args.length; i++) {
377-
args[i] = SerializationUtil.readWrappedObject(in);
387+
final int argCount = in.readInt();
388+
args = new Object[argCount];
389+
for (int argIndex = 0; argIndex < args.length; argIndex++) {
390+
args[argIndex] = SerializationUtil.readWrappedObject(in);
378391
}
379392
}
380393
}

log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
*/
1717
package org.apache.logging.log4j.message;
1818

19+
import static org.apache.logging.log4j.util.StringBuilders.trimToMaxSize;
20+
1921
import java.util.Arrays;
2022
import org.apache.logging.log4j.message.ParameterFormatter.MessagePatternAnalysis;
2123
import org.apache.logging.log4j.util.Constants;
2224
import org.apache.logging.log4j.util.PerformanceSensitive;
23-
import org.apache.logging.log4j.util.StringBuilders;
2425

2526
/**
2627
* Reusable parameterized message. This message is mutable and is not safe to be accessed or modified by multiple
@@ -32,13 +33,12 @@
3233
@PerformanceSensitive("allocation")
3334
public class ReusableParameterizedMessage implements ReusableMessage, ParameterVisitable, Clearable {
3435

35-
private static final int MIN_BUILDER_SIZE = 512;
3636
private static final int MAX_PARAMS = 10;
3737
private static final long serialVersionUID = 7800075879295123856L;
38-
private transient ThreadLocal<StringBuilder> buffer; // non-static: LOG4J2-1583
3938

4039
private String messagePattern;
4140
private final MessagePatternAnalysis patternAnalysis = new MessagePatternAnalysis();
41+
private final StringBuilder formatBuffer = new StringBuilder(Constants.MAX_REUSABLE_MESSAGE_SIZE);
4242
private int argCount;
4343
private transient Object[] varargs;
4444
private transient Object[] params = new Object[MAX_PARAMS];
@@ -336,25 +336,13 @@ public Throwable getThrowable() {
336336
*/
337337
@Override
338338
public String getFormattedMessage() {
339-
final StringBuilder sb = getBuffer();
340-
formatTo(sb);
341-
final String result = sb.toString();
342-
StringBuilders.trimToMaxSize(sb, Constants.MAX_REUSABLE_MESSAGE_SIZE);
343-
return result;
344-
}
345-
346-
private StringBuilder getBuffer() {
347-
if (buffer == null) {
348-
buffer = new ThreadLocal<>();
349-
}
350-
StringBuilder result = buffer.get();
351-
if (result == null) {
352-
final int currentPatternLength = messagePattern == null ? 0 : messagePattern.length();
353-
result = new StringBuilder(Math.max(MIN_BUILDER_SIZE, currentPatternLength * 2));
354-
buffer.set(result);
339+
try {
340+
formatTo(formatBuffer);
341+
return formatBuffer.toString();
342+
} finally {
343+
trimToMaxSize(formatBuffer, Constants.MAX_REUSABLE_MESSAGE_SIZE);
344+
formatBuffer.setLength(0);
355345
}
356-
result.setLength(0);
357-
return result;
358346
}
359347

360348
@Override
@@ -374,8 +362,10 @@ ReusableParameterizedMessage reserve() {
374362

375363
@Override
376364
public String toString() {
377-
return "ReusableParameterizedMessage[messagePattern=" + getFormat() + ", stringArgs="
378-
+ Arrays.toString(getParameters()) + ", throwable=" + getThrowable() + ']';
365+
// Avoid formatting arguments!
366+
// It can cause recursion, which can become pretty unpleasant while troubleshooting.
367+
return "ReusableParameterizedMessage[messagePattern=" + getFormat() + ", argCount=" + getParameterCount()
368+
+ ", throwableProvided=" + (getThrowable() != null) + ']';
379369
}
380370

381371
@Override

log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.apache.logging.log4j.util;
1818

19+
import static org.apache.logging.log4j.util.StringBuilders.trimToMaxSize;
20+
1921
import java.util.Iterator;
2022
import java.util.Locale;
2123
import java.util.Objects;
@@ -28,7 +30,11 @@
2830
@InternalApi
2931
public final class Strings {
3032

31-
private static final ThreadLocal<StringBuilder> tempStr = ThreadLocal.withInitial(StringBuilder::new);
33+
// 518 allows the `StringBuilder` to resize three times from its initial size.
34+
// This should be sufficient for most use cases.
35+
private static final int MAX_FORMAT_BUFFER_LENGTH = 518;
36+
37+
private static final ThreadLocal<StringBuilder> FORMAT_BUFFER_REF = ThreadLocal.withInitial(StringBuilder::new);
3238

3339
/**
3440
* The empty string.
@@ -318,10 +324,11 @@ public static String concat(final String str1, final String str2) {
318324
} else if (isEmpty(str2)) {
319325
return str1;
320326
}
321-
final StringBuilder sb = tempStr.get();
327+
final StringBuilder sb = FORMAT_BUFFER_REF.get();
322328
try {
323329
return sb.append(str1).append(str2).toString();
324330
} finally {
331+
trimToMaxSize(sb, MAX_FORMAT_BUFFER_LENGTH);
325332
sb.setLength(0);
326333
}
327334
}
@@ -338,13 +345,14 @@ public static String repeat(final String str, final int count) {
338345
if (count < 0) {
339346
throw new IllegalArgumentException("count");
340347
}
341-
final StringBuilder sb = tempStr.get();
348+
final StringBuilder sb = FORMAT_BUFFER_REF.get();
342349
try {
343350
for (int index = 0; index < count; index++) {
344351
sb.append(str);
345352
}
346353
return sb.toString();
347354
} finally {
355+
trimToMaxSize(sb, MAX_FORMAT_BUFFER_LENGTH);
348356
sb.setLength(0);
349357
}
350358
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://logging.apache.org/log4j/changelog"
4+
xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.3.xsd"
5+
type="fixed">
6+
<description format="asciidoc">
7+
Fix `StringBuilder` cache corruption on recursive access
8+
</description>
9+
</entry>

0 commit comments

Comments
 (0)