Skip to content

Commit 18a1deb

Browse files
authored
Hardens PropertiesUtil against recursive property sources (#3263)
As showed in #3252, Spring's `JndiPropertySource` not only can throw exceptions, but can also perform logging calls. Such a call causes a recursive call to `PropertiesUtil.getProperty("log4j2.flowMessageFactory"`) and a `StackOverflowException` in the best scenario. The worst scenario includes a deadlock. This PR: - Moves the creation of the default `MessageFactory` and `FlowMessageFactory` to the static initializer of `LoggerContext`. This should be close enough to the pre-2.23.0 location in `AbstractLogger`. The `LoggerContext` class is usually initialized, before Spring Boot adds its property sources to `PropertiesUtil`. - Adds a check to `PropertiesUtil` to ignore recursive calls. Closes #3252.
1 parent bad8b56 commit 18a1deb

File tree

9 files changed

+223
-137
lines changed

9 files changed

+223
-137
lines changed

log4j-api-test/src/test/java/org/apache/logging/log4j/util/PropertiesUtilTest.java

+68-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
import java.util.Map;
3939
import java.util.Properties;
4040
import java.util.stream.Stream;
41+
import org.apache.logging.log4j.Level;
42+
import org.apache.logging.log4j.test.ListStatusListener;
43+
import org.apache.logging.log4j.test.junit.UsingStatusListener;
4144
import org.junit.jupiter.api.BeforeEach;
4245
import org.junit.jupiter.api.Test;
4346
import org.junit.jupiter.api.parallel.ResourceAccessMode;
@@ -193,16 +196,56 @@ void testPublish() {
193196
@Test
194197
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
195198
@Issue("https://github.com/spring-projects/spring-boot/issues/33450")
196-
void testBadPropertySource() {
199+
@UsingStatusListener
200+
void testErrorPropertySource(ListStatusListener statusListener) {
197201
final String key = "testKey";
198202
final Properties props = new Properties();
199203
props.put(key, "test");
200204
final PropertiesUtil util = new PropertiesUtil(props);
201205
final ErrorPropertySource source = new ErrorPropertySource();
202206
util.addPropertySource(source);
203207
try {
208+
statusListener.clear();
204209
assertEquals("test", util.getStringProperty(key));
205210
assertTrue(source.exceptionThrown);
211+
assertThat(statusListener.findStatusData(Level.WARN))
212+
.anySatisfy(data ->
213+
assertThat(data.getMessage().getFormattedMessage()).contains("Failed"));
214+
} finally {
215+
util.removePropertySource(source);
216+
}
217+
}
218+
219+
@Test
220+
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
221+
@Issue("https://github.com/apache/logging-log4j2/issues/3252")
222+
@UsingStatusListener
223+
void testRecursivePropertySource(ListStatusListener statusListener) {
224+
final String key = "testKey";
225+
final Properties props = new Properties();
226+
props.put(key, "test");
227+
final PropertiesUtil util = new PropertiesUtil(props);
228+
final PropertySource source = new RecursivePropertySource(util);
229+
util.addPropertySource(source);
230+
try {
231+
// We ignore the recursive source
232+
statusListener.clear();
233+
assertThat(util.getStringProperty(key)).isEqualTo("test");
234+
assertThat(statusListener.findStatusData(Level.WARN))
235+
.anySatisfy(data -> assertThat(data.getMessage().getFormattedMessage())
236+
.contains("Recursive call", "getProperty"));
237+
238+
statusListener.clear();
239+
// To check for existence, the sources are looked up in a random order.
240+
assertThat(util.hasProperty(key)).isTrue();
241+
// To find a missing key, all the sources must be used.
242+
assertThat(util.hasProperty("noSuchKey")).isFalse();
243+
assertThat(statusListener.findStatusData(Level.WARN))
244+
.anySatisfy(data -> assertThat(data.getMessage().getFormattedMessage())
245+
.contains("Recursive call", "containsProperty"));
246+
// We check that the source is recursive
247+
assertThat(source.getProperty(key)).isEqualTo("test");
248+
assertThat(source.containsProperty(key)).isTrue();
206249
} finally {
207250
util.removePropertySource(source);
208251
}
@@ -289,4 +332,28 @@ public boolean containsProperty(final String key) {
289332
throw new IllegalStateException("Test");
290333
}
291334
}
335+
336+
private static class RecursivePropertySource implements PropertySource {
337+
338+
private final PropertiesUtil propertiesUtil;
339+
340+
private RecursivePropertySource(PropertiesUtil propertiesUtil) {
341+
this.propertiesUtil = propertiesUtil;
342+
}
343+
344+
@Override
345+
public int getPriority() {
346+
return Integer.MIN_VALUE;
347+
}
348+
349+
@Override
350+
public String getProperty(String key) {
351+
return propertiesUtil.getStringProperty(key);
352+
}
353+
354+
@Override
355+
public boolean containsProperty(String key) {
356+
return propertiesUtil.hasProperty(key);
357+
}
358+
}
292359
}

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

+25-10
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ public void reload() {}
496496
private static final class Environment {
497497

498498
private final Set<PropertySource> sources = ConcurrentHashMap.newKeySet();
499+
private final ThreadLocal<PropertySource> CURRENT_PROPERTY_SOURCE = new ThreadLocal<>();
499500

500501
private Environment(final PropertySource propertySource) {
501502
final PropertySource sysProps = new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME, false);
@@ -547,21 +548,35 @@ private String get(final String key) {
547548
}
548549

549550
private boolean sourceContainsProperty(final PropertySource source, final String key) {
550-
try {
551-
return source.containsProperty(key);
552-
} catch (final Exception e) {
553-
LOGGER.warn("Failed to retrieve Log4j property {} from property source {}.", key, source, e);
554-
return false;
551+
PropertySource recursiveSource = CURRENT_PROPERTY_SOURCE.get();
552+
if (recursiveSource == null) {
553+
CURRENT_PROPERTY_SOURCE.set(source);
554+
try {
555+
return source.containsProperty(key);
556+
} catch (final Exception e) {
557+
LOGGER.warn("Failed to retrieve Log4j property {} from property source {}.", key, source, e);
558+
} finally {
559+
CURRENT_PROPERTY_SOURCE.remove();
560+
}
555561
}
562+
LOGGER.warn("Recursive call to `containsProperty()` from property source {}.", recursiveSource);
563+
return false;
556564
}
557565

558566
private String sourceGetProperty(final PropertySource source, final String key) {
559-
try {
560-
return source.getProperty(key);
561-
} catch (final Exception e) {
562-
LOGGER.warn("Failed to retrieve Log4j property {} from property source {}.", key, source, e);
563-
return null;
567+
PropertySource recursiveSource = CURRENT_PROPERTY_SOURCE.get();
568+
if (recursiveSource == null) {
569+
CURRENT_PROPERTY_SOURCE.set(source);
570+
try {
571+
return source.getProperty(key);
572+
} catch (final Exception e) {
573+
LOGGER.warn("Failed to retrieve Log4j property {} from property source {}.", key, source, e);
574+
} finally {
575+
CURRENT_PROPERTY_SOURCE.remove();
576+
}
564577
}
578+
LOGGER.warn("Recursive call to `getProperty()` from property source {}.", recursiveSource);
579+
return null;
565580
}
566581

567582
private boolean containsKey(final String key) {

log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerMessageFactoryCustomizationTest.java

+34-25
Original file line numberDiff line numberDiff line change
@@ -20,52 +20,61 @@
2020

2121
import org.apache.logging.log4j.message.AbstractMessageFactory;
2222
import org.apache.logging.log4j.message.DefaultFlowMessageFactory;
23+
import org.apache.logging.log4j.message.FlowMessageFactory;
2324
import org.apache.logging.log4j.message.Message;
2425
import org.apache.logging.log4j.message.MessageFactory;
2526
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
2627
import org.junit.jupiter.api.Test;
27-
import org.junitpioneer.jupiter.ClearSystemProperty;
28+
import org.junit.jupiter.api.TestInfo;
2829
import org.junitpioneer.jupiter.SetSystemProperty;
2930

31+
@SetSystemProperty(
32+
key = "log4j2.messageFactory",
33+
value = "org.apache.logging.log4j.core.LoggerMessageFactoryCustomizationTest$AlternativeTestMessageFactory")
34+
@SetSystemProperty(
35+
key = "log4j2.flowMessageFactory",
36+
value = "org.apache.logging.log4j.core.LoggerMessageFactoryCustomizationTest$AlternativeTestFlowMessageFactory")
3037
class LoggerMessageFactoryCustomizationTest {
3138

3239
@Test
33-
@ClearSystemProperty(key = "log4j2.messageFactory")
34-
@ClearSystemProperty(key = "log4j2.flowMessageFactory")
35-
void arguments_should_be_honored() {
36-
final LoggerContext loggerContext =
37-
new LoggerContext(LoggerMessageFactoryCustomizationTest.class.getSimpleName());
38-
final Logger logger = new Logger(
39-
loggerContext, "arguments_should_be_honored", new TestMessageFactory(), new TestFlowMessageFactory());
40-
assertTestMessageFactories(logger);
40+
void arguments_should_be_honored(TestInfo testInfo) {
41+
try (LoggerContext loggerContext =
42+
new LoggerContext(LoggerMessageFactoryCustomizationTest.class.getSimpleName())) {
43+
Logger logger = new Logger(
44+
loggerContext, testInfo.getDisplayName(), new TestMessageFactory(), new TestFlowMessageFactory());
45+
assertTestMessageFactories(logger, TestMessageFactory.class, TestFlowMessageFactory.class);
46+
}
4147
}
4248

4349
@Test
44-
@SetSystemProperty(
45-
key = "log4j2.messageFactory",
46-
value = "org.apache.logging.log4j.core.LoggerMessageFactoryCustomizationTest$TestMessageFactory")
47-
@SetSystemProperty(
48-
key = "log4j2.flowMessageFactory",
49-
value = "org.apache.logging.log4j.core.LoggerMessageFactoryCustomizationTest$TestFlowMessageFactory")
50-
void properties_should_be_honored() {
51-
final LoggerContext loggerContext =
52-
new LoggerContext(LoggerMessageFactoryCustomizationTest.class.getSimpleName());
53-
final Logger logger = new Logger(loggerContext, "properties_should_be_honored", null, null);
54-
assertTestMessageFactories(logger);
50+
void properties_should_be_honored(TestInfo testInfo) {
51+
try (LoggerContext loggerContext =
52+
new LoggerContext(LoggerMessageFactoryCustomizationTest.class.getSimpleName())) {
53+
Logger logger = loggerContext.getLogger(testInfo.getDisplayName());
54+
assertTestMessageFactories(
55+
logger, AlternativeTestMessageFactory.class, AlternativeTestFlowMessageFactory.class);
56+
}
5557
}
5658

57-
private static void assertTestMessageFactories(Logger logger) {
58-
assertThat((MessageFactory) logger.getMessageFactory()).isInstanceOf(TestMessageFactory.class);
59-
assertThat(logger.getFlowMessageFactory()).isInstanceOf(TestFlowMessageFactory.class);
59+
private static void assertTestMessageFactories(
60+
Logger logger,
61+
Class<? extends MessageFactory> messageFactoryClass,
62+
Class<? extends FlowMessageFactory> flowMessageFactoryClass) {
63+
assertThat((MessageFactory) logger.getMessageFactory()).isInstanceOf(messageFactoryClass);
64+
assertThat(logger.getFlowMessageFactory()).isInstanceOf(flowMessageFactoryClass);
6065
}
6166

62-
public static final class TestMessageFactory extends AbstractMessageFactory {
67+
public static class TestMessageFactory extends AbstractMessageFactory {
6368

6469
@Override
6570
public Message newMessage(final String message, final Object... params) {
6671
return ParameterizedMessageFactory.INSTANCE.newMessage(message, params);
6772
}
6873
}
6974

70-
public static final class TestFlowMessageFactory extends DefaultFlowMessageFactory {}
75+
public static class AlternativeTestMessageFactory extends TestMessageFactory {}
76+
77+
public static class TestFlowMessageFactory extends DefaultFlowMessageFactory {}
78+
79+
public static class AlternativeTestFlowMessageFactory extends TestFlowMessageFactory {}
7180
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerMessageFactoryDefaultsTlaDisabledTest.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,20 @@
2323
import org.apache.logging.log4j.message.MessageFactory;
2424
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
2525
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.TestInfo;
2627
import org.junitpioneer.jupiter.SetSystemProperty;
2728

2829
class LoggerMessageFactoryDefaultsTlaDisabledTest {
2930

3031
@Test
3132
@SetSystemProperty(key = "log4j2.enableThreadLocals", value = "false")
32-
void defaults_should_match_when_thread_locals_disabled() {
33+
void defaults_should_match_when_thread_locals_disabled(TestInfo testInfo) {
3334
assertThat(Constants.ENABLE_THREADLOCALS).isFalse();
34-
final LoggerContext loggerContext =
35-
new LoggerContext(LoggerMessageFactoryDefaultsTlaDisabledTest.class.getSimpleName());
36-
final Logger logger =
37-
new Logger(loggerContext, "defaults_should_match_when_thread_locals_disabled", null, null);
38-
assertThat((MessageFactory) logger.getMessageFactory()).isSameAs(ParameterizedMessageFactory.INSTANCE);
39-
assertThat(logger.getFlowMessageFactory()).isSameAs(DefaultFlowMessageFactory.INSTANCE);
35+
try (LoggerContext loggerContext =
36+
new LoggerContext(LoggerMessageFactoryDefaultsTlaDisabledTest.class.getSimpleName())) {
37+
final Logger logger = loggerContext.getLogger(testInfo.getDisplayName());
38+
assertThat((MessageFactory) logger.getMessageFactory()).isSameAs(ParameterizedMessageFactory.INSTANCE);
39+
assertThat(logger.getFlowMessageFactory()).isSameAs(DefaultFlowMessageFactory.INSTANCE);
40+
}
4041
}
4142
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerMessageFactoryDefaultsTlaEnabledTest.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@
2323
import org.apache.logging.log4j.message.MessageFactory;
2424
import org.apache.logging.log4j.message.ReusableMessageFactory;
2525
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.TestInfo;
2627
import org.junitpioneer.jupiter.SetSystemProperty;
2728

2829
class LoggerMessageFactoryDefaultsTlaEnabledTest {
2930

3031
@Test
31-
@SetSystemProperty(key = "log4j2.is.webapp", value = "false")
32-
@SetSystemProperty(key = "log4j2.enableThreadLocals", value = "true")
33-
void defaults_should_match_when_thread_locals_enabled() {
32+
@SetSystemProperty(key = "log4j2.isWebapp", value = "false")
33+
@SetSystemProperty(key = "log4j2.enableThreadlocals", value = "true")
34+
void defaults_should_match_when_thread_locals_enabled(TestInfo testInfo) {
3435
assertThat(Constants.ENABLE_THREADLOCALS).isTrue();
35-
final LoggerContext loggerContext =
36-
new LoggerContext(LoggerMessageFactoryDefaultsTlaEnabledTest.class.getSimpleName());
37-
final Logger logger = new Logger(loggerContext, "defaults_should_match_when_thread_locals_enabled", null, null);
38-
assertThat((MessageFactory) logger.getMessageFactory()).isSameAs(ReusableMessageFactory.INSTANCE);
39-
assertThat(logger.getFlowMessageFactory()).isSameAs(DefaultFlowMessageFactory.INSTANCE);
36+
try (LoggerContext loggerContext =
37+
new LoggerContext(LoggerMessageFactoryDefaultsTlaEnabledTest.class.getSimpleName())) {
38+
Logger logger = loggerContext.getLogger(testInfo.getDisplayName());
39+
assertThat((MessageFactory) logger.getMessageFactory()).isSameAs(ReusableMessageFactory.INSTANCE);
40+
assertThat(logger.getFlowMessageFactory()).isSameAs(DefaultFlowMessageFactory.INSTANCE);
41+
}
4042
}
4143
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java

-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.apache.logging.log4j.ThreadContext;
3434
import org.apache.logging.log4j.core.ContextDataInjector;
3535
import org.apache.logging.log4j.spi.ThreadContextMap;
36-
import org.apache.logging.log4j.util.PropertiesUtil;
3736
import org.apache.logging.log4j.util.ProviderUtil;
3837
import org.apache.logging.log4j.util.SortedArrayStringMap;
3938
import org.apache.logging.log4j.util.StringMap;
@@ -59,7 +58,6 @@ public static Collection<String[]> threadContextMapClassNames() {
5958
public String threadContextMapClassName;
6059

6160
private static void resetThreadContextMap() {
62-
PropertiesUtil.getProperties().reload();
6361
final Log4jProvider provider = (Log4jProvider) ProviderUtil.getProvider();
6462
provider.resetThreadContextMap();
6563
ThreadContext.init();

0 commit comments

Comments
 (0)