Skip to content

Commit 0448e5c

Browse files
committed
Workaround for the flaw in Android's Security Manager preventing the population of exception's stack trace
1 parent 07f6920 commit 0448e5c

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

library/src/main/java/net/loune/log4j2android/LogcatAppender.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ public static LogcatAppender createAppender(@PluginAttribute("name") String name
4343
layout = requireNonNullElseGet(layout, PatternLayout::createDefaultLayout);
4444
boolean useNative = shouldUseNativeStackTraceRendering(stackTraceRendering);
4545

46+
if (!useNative) {
47+
boolean success = WorkaroundSecurityManagerNPE.applyIfNeeded();
48+
if (!success) {
49+
// Force rendering the stack trace by Logcat
50+
useNative = true;
51+
}
52+
}
53+
4654
return new LogcatAppender(name, filter, layout, ignoreExceptions, properties, useNative);
4755
}
4856

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package net.loune.log4j2android;
2+
3+
import java.lang.reflect.Field;
4+
import java.lang.reflect.InvocationTargetException;
5+
import java.lang.reflect.Method;
6+
import lombok.Getter;
7+
8+
import static java.util.Objects.requireNonNullElseGet;
9+
10+
/**
11+
* Class PrivateSecurityManagerStackTraceUtil uses a SecurityManager to populate the stack trace array
12+
* (which is, apparently, much faster than the standard approach). However on Android security managers are not used
13+
*, and the implementation of SecurityManager class contains just stubs; hence the returned array is always null.
14+
* This results in an NPE when Log4J tries to use it to fill the stack trace.
15+
* <p>
16+
* Fortunately, Log4J library will first check `isEnabled()` before using the SecurityManager.
17+
* <p>
18+
* By tweaking the internals of PrivateSecurityManagerStackTraceUtil we force it to always return `isEnabled() == false`.
19+
*/
20+
public class WorkaroundSecurityManagerNPE {
21+
@Getter
22+
private static boolean isApplied;
23+
@Getter
24+
private static Exception error;
25+
26+
public static boolean applyIfNeeded() {
27+
if (!isApplied) {
28+
isApplied = true;
29+
try {
30+
new WorkaroundSecurityManagerNPE().apply();
31+
} catch (PatchException e) {
32+
error = e;
33+
}
34+
}
35+
return (error == null);
36+
}
37+
38+
39+
private static final class PatchException extends Exception {
40+
public PatchException(String message, Throwable cause) {
41+
super(message, cause);
42+
}
43+
}
44+
45+
private Method methodIsEnabled;
46+
private Field fieldSecurityManager;
47+
48+
private WorkaroundSecurityManagerNPE() {}
49+
50+
private void apply() throws PatchException {
51+
loadTargetClass();
52+
// Expecting that the static initializer in that class has executed, let's check:
53+
boolean isEnabled = checkIsEnabled();
54+
if (!isEnabled) {
55+
// Not sure how it didn't get enabled but we are happy this way
56+
return;
57+
}
58+
// Yep, it's set. Let's reset it back to null
59+
clearSecurityManager();
60+
61+
isEnabled = checkIsEnabled();
62+
if (isEnabled) {
63+
throw new PatchException("Failed to apply the patch. The PrivateSecurityManagerStackTraceUtil is still enabled", null);
64+
}
65+
}
66+
67+
private void loadTargetClass() throws PatchException {
68+
ClassLoader classLoader = requireNonNullElseGet(this.getClass().getClassLoader(), ClassLoader::getSystemClassLoader);
69+
try {
70+
Class<?> klass = Class.forName("org.apache.logging.log4j.util.PrivateSecurityManagerStackTraceUtil", true, classLoader);
71+
methodIsEnabled = klass.getDeclaredMethod("isEnabled");
72+
methodIsEnabled.setAccessible(true);
73+
fieldSecurityManager = klass.getDeclaredField("SECURITY_MANAGER");
74+
fieldSecurityManager.setAccessible(true);
75+
76+
} catch (ClassNotFoundException | LinkageError | NoSuchMethodException | NoSuchFieldException e) {
77+
throw new PatchException("The structure of class PrivateSecurityManagerStackTraceUtil differs from the expected", e);
78+
}
79+
}
80+
81+
private boolean checkIsEnabled() throws PatchException {
82+
try {
83+
Boolean result = (Boolean) methodIsEnabled.invoke(null);
84+
//noinspection DataFlowIssue,UnnecessaryUnboxing - covered by the catch clause below
85+
return result.booleanValue();
86+
87+
} catch (IllegalAccessException | InvocationTargetException | NullPointerException e) {
88+
throw new PatchException("Failed to call isEnabled() on PrivateSecurityManagerStackTraceUtil", e);
89+
}
90+
}
91+
92+
private void clearSecurityManager() throws PatchException {
93+
try {
94+
fieldSecurityManager.set(null, null);
95+
} catch (IllegalAccessException e) {
96+
throw new PatchException("Failed to set static field SECURITY_MANAGER in PrivateSecurityManagerStackTraceUtil", e);
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)