Skip to content

Commit 28c7bfa

Browse files
committed
Add more bound checks and null guards
1 parent d33ccfc commit 28c7bfa

File tree

8 files changed

+96
-23
lines changed

8 files changed

+96
-23
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ public class io/sentry/android/core/anr/AnrException : java/lang/Exception {
566566
}
567567

568568
public class io/sentry/android/core/anr/AnrProfile {
569-
public final field endtimeMs J
569+
public final field endTimeMs J
570570
public final field stacks Ljava/util/List;
571571
public final field startTimeMs J
572572
public fun <init> (Ljava/util/List;)V

sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,10 @@ private void applyAnrProfile(
830830
final long anrTimestamp;
831831
if (anrTimestampObj != null) {
832832
anrTimestamp = anrTimestampObj;
833-
} else {
833+
} else if (event.getTimestamp() != null) {
834834
anrTimestamp = event.getTimestamp().getTime();
835+
} else {
836+
return;
835837
}
836838

837839
AnrProfile anrProfile = null;
@@ -858,7 +860,7 @@ private void applyAnrProfile(
858860
}
859861

860862
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
861-
if (anrTimestamp < anrProfile.startTimeMs || anrTimestamp > anrProfile.endtimeMs) {
863+
if (anrTimestamp < anrProfile.startTimeMs || anrTimestamp > anrProfile.endTimeMs) {
862864
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
863865
return;
864866
}
@@ -886,6 +888,8 @@ private void applyAnrProfile(
886888
mechanism.setType("ANR");
887889
e.setMechanism(mechanism);
888890
}
891+
// Replaces the original ANR exception with the profile-derived one,
892+
// as we assume the profiling stacktrace is more detailed
889893
event.setExceptions(sentryException);
890894

891895
if (profilerId != null) {

sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfile.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class AnrProfile {
1010
public final List<AnrStackTrace> stacks;
1111

1212
public final long startTimeMs;
13-
public final long endtimeMs;
13+
public final long endTimeMs;
1414

1515
public AnrProfile(List<AnrStackTrace> stacks) {
1616
this.stacks = new ArrayList<>(stacks.size());
@@ -25,10 +25,10 @@ public AnrProfile(List<AnrStackTrace> stacks) {
2525
startTimeMs = this.stacks.get(0).timestampMs;
2626

2727
// adding 10s to be less strict around end time
28-
endtimeMs = this.stacks.get(this.stacks.size() - 1).timestampMs + 10_000L;
28+
endTimeMs = this.stacks.get(this.stacks.size() - 1).timestampMs + 10_000L;
2929
} else {
3030
startTimeMs = 0L;
31-
endtimeMs = 0L;
31+
endTimeMs = 0L;
3232
}
3333
}
3434
}

sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.sentry.SentryOptions;
99
import io.sentry.cache.tape.ObjectQueue;
1010
import io.sentry.cache.tape.QueueFile;
11+
import io.sentry.util.Objects;
1112
import java.io.ByteArrayInputStream;
1213
import java.io.Closeable;
1314
import java.io.DataInputStream;
@@ -28,7 +29,11 @@ public class AnrProfileManager implements Closeable {
2829
@NotNull private final ObjectQueue<AnrStackTrace> queue;
2930

3031
public AnrProfileManager(final @NotNull SentryOptions options) {
31-
this(options, new File(options.getCacheDirPath(), "anr_profile"));
32+
this(
33+
options,
34+
new File(
35+
Objects.requireNonNull(options.getCacheDirPath(), "cacheDirPath is required"),
36+
"anr_profile"));
3237
}
3338

3439
public AnrProfileManager(final @NotNull SentryOptions options, final @NotNull File file) {

sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.File;
2121
import java.io.IOException;
2222
import java.util.concurrent.atomic.AtomicBoolean;
23+
import java.util.concurrent.atomic.AtomicInteger;
2324
import org.jetbrains.annotations.ApiStatus;
2425
import org.jetbrains.annotations.NotNull;
2526
import org.jetbrains.annotations.Nullable;
@@ -31,6 +32,7 @@ public class AnrProfilingIntegration
3132
public static final long POLLING_INTERVAL_MS = 66;
3233
private static final long THRESHOLD_SUSPICION_MS = 1000;
3334
public static final long THRESHOLD_ANR_MS = 4000;
35+
static final int MAX_NUM_STACKS = (int) (10_000 / POLLING_INTERVAL_MS);
3436

3537
private final AtomicBoolean enabled = new AtomicBoolean(true);
3638
private final Runnable updater = () -> lastMainThreadExecutionTime = SystemClock.uptimeMillis();
@@ -39,6 +41,7 @@ public class AnrProfilingIntegration
3941
new AutoClosableReentrantLock();
4042

4143
private volatile long lastMainThreadExecutionTime = SystemClock.uptimeMillis();
44+
final AtomicInteger numCollectedStacks = new AtomicInteger();
4245
private volatile MainThreadState mainThreadState = MainThreadState.IDLE;
4346
private volatile @Nullable AnrProfileManager profileManager;
4447
private volatile @NotNull ILogger logger = NoOpLogger.getInstance();
@@ -54,6 +57,11 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
5457
"SentryAndroidOptions is required");
5558
this.logger = options.getLogger();
5659

60+
if (options.getCacheDirPath() == null) {
61+
logger.log(SentryLevel.WARNING, "ANR Profiling is enabled but cacheDirPath is not set");
62+
return;
63+
}
64+
5765
if (((SentryAndroidOptions) options).isEnableAnrProfiling()) {
5866
addIntegrationToSdkVersion("AnrProfiling");
5967
AppState.getInstance().addAppStateListener(this);
@@ -71,6 +79,7 @@ public void close() throws IOException {
7179
if (p != null) {
7280
p.close();
7381
}
82+
profileManager = null;
7483
}
7584
}
7685

@@ -138,7 +147,7 @@ public void run() {
138147
}
139148
}
140149
} catch (Throwable t) {
141-
logger.log(SentryLevel.WARNING, "Failed execute AnrStacktraceIntegration", t);
150+
logger.log(SentryLevel.WARNING, "Failed to execute AnrStacktraceIntegration", t);
142151
}
143152
}
144153

@@ -152,29 +161,40 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept
152161
}
153162

154163
if (mainThreadState == MainThreadState.IDLE && diff > THRESHOLD_SUSPICION_MS) {
155-
logger.log(SentryLevel.DEBUG, "ANR: main thread is suspicious");
164+
if (logger.isEnabled(SentryLevel.DEBUG)) {
165+
logger.log(SentryLevel.DEBUG, "ANR: main thread is suspicious");
166+
}
156167
mainThreadState = MainThreadState.SUSPICIOUS;
157168
clearStacks();
158169
}
159170

160171
// if we are suspicious, we need to collect stack traces
161172
if (mainThreadState == MainThreadState.SUSPICIOUS
162173
|| mainThreadState == MainThreadState.ANR_DETECTED) {
163-
final long start = SystemClock.uptimeMillis();
164-
final @NotNull AnrStackTrace trace =
165-
new AnrStackTrace(System.currentTimeMillis(), mainThread.getStackTrace());
166-
final long duration = SystemClock.uptimeMillis() - start;
167-
logger.log(
168-
SentryLevel.DEBUG,
169-
"AnrWatchdog: capturing main thread stacktrace took " + duration + "ms");
170-
171-
addStackTrace(trace);
174+
if (numCollectedStacks.get() < MAX_NUM_STACKS) {
175+
final long start = SystemClock.uptimeMillis();
176+
final @NotNull AnrStackTrace trace =
177+
new AnrStackTrace(System.currentTimeMillis(), mainThread.getStackTrace());
178+
final long duration = SystemClock.uptimeMillis() - start;
179+
if (logger.isEnabled(SentryLevel.DEBUG)) {
180+
logger.log(
181+
SentryLevel.DEBUG,
182+
"AnrWatchdog: capturing main thread stacktrace took " + duration + "ms");
183+
}
184+
addStackTrace(trace);
185+
} else {
186+
if (logger.isEnabled(SentryLevel.DEBUG)) {
187+
logger.log(
188+
SentryLevel.DEBUG,
189+
"ANR: reached maximum number of collected stack traces, skipping further collection");
190+
}
191+
}
172192
}
173193

174-
// TODO is this still required,
175-
// maybe add stop condition
176194
if (mainThreadState == MainThreadState.SUSPICIOUS && diff > THRESHOLD_ANR_MS) {
177-
logger.log(SentryLevel.DEBUG, "ANR: main thread ANR threshold reached");
195+
if (logger.isEnabled(SentryLevel.DEBUG)) {
196+
logger.log(SentryLevel.DEBUG, "ANR: main thread ANR threshold reached");
197+
}
178198
mainThreadState = MainThreadState.ANR_DETECTED;
179199
}
180200
}
@@ -192,8 +212,12 @@ protected AnrProfileManager getProfileManager() {
192212
if (profileManager == null) {
193213
final @NotNull SentryOptions opts =
194214
Objects.requireNonNull(options, "Options can't be null");
215+
final @Nullable String cacheDirPath = opts.getCacheDirPath();
216+
if (cacheDirPath == null) {
217+
throw new IllegalStateException("cacheDirPath is required for ANR profiling");
218+
}
195219
final @NotNull File currentFile =
196-
AnrProfileRotationHelper.getFileForRecording(new File(opts.getCacheDirPath()));
220+
AnrProfileRotationHelper.getFileForRecording(new File(cacheDirPath));
197221
profileManager = new AnrProfileManager(opts, currentFile);
198222
}
199223

@@ -202,10 +226,12 @@ protected AnrProfileManager getProfileManager() {
202226
}
203227

204228
private void clearStacks() throws IOException {
229+
numCollectedStacks.set(0);
205230
getProfileManager().clear();
206231
}
207232

208233
private void addStackTrace(@NotNull final AnrStackTrace trace) throws IOException {
234+
numCollectedStacks.incrementAndGet();
209235
getProfileManager().add(trace);
210236
}
211237

sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrStackTrace.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
@ApiStatus.Internal
1313
public final class AnrStackTrace implements Comparable<AnrStackTrace> {
1414

15+
private static final int MAX_STACK_LENGTH = 1000;
16+
1517
public final StackTraceElement[] stack;
1618
public final long timestampMs;
1719

@@ -47,6 +49,9 @@ public static AnrStackTrace deserialize(final @NotNull DataInputStream dis) thro
4749
if (version == 1) {
4850
final long timestampMs = dis.readLong();
4951
final int stackLength = dis.readInt();
52+
if (stackLength < 0 || stackLength > MAX_STACK_LENGTH) {
53+
return null;
54+
}
5055
final @NotNull StackTraceElement[] stack = new StackTraceElement[stackLength];
5156

5257
for (int i = 0; i < stackLength; i++) {

sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ class AnrProfilingIntegrationTest {
186186
assertEquals(AnrProfilingIntegration.MainThreadState.ANR_DETECTED, integration.state)
187187
assertEquals(2, integration.profileManager.load().stacks.size)
188188

189-
integration.close()
189+
for (i in 0 until AnrProfilingIntegration.MAX_NUM_STACKS + 1) {
190+
integration.checkMainThread(mainThread)
191+
}
192+
assertEquals(AnrProfilingIntegration.MAX_NUM_STACKS, integration.numCollectedStacks.get())
190193
}
191194

192195
@Test

sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrStackTraceTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,34 @@ class AnrStackTraceTest {
9494
assertNull(deserialized.stack[0].fileName)
9595
assertEquals(42, deserialized.stack[1].lineNumber)
9696
}
97+
98+
@Test
99+
fun `deserialize returns null for oversized stack length`() {
100+
val bytes = ByteArrayOutputStream()
101+
val dos = DataOutputStream(bytes)
102+
dos.writeShort(1) // version
103+
dos.writeLong(1234567890L) // timestamp
104+
dos.writeInt(1001) // stackLength exceeds MAX_STACK_LENGTH
105+
dos.flush()
106+
107+
val dis = DataInputStream(ByteArrayInputStream(bytes.toByteArray()))
108+
val deserialized = AnrStackTrace.deserialize(dis)
109+
110+
assertNull(deserialized)
111+
}
112+
113+
@Test
114+
fun `deserialize returns null for negative stack length`() {
115+
val bytes = ByteArrayOutputStream()
116+
val dos = DataOutputStream(bytes)
117+
dos.writeShort(1) // version
118+
dos.writeLong(1234567890L) // timestamp
119+
dos.writeInt(-1) // negative stackLength
120+
dos.flush()
121+
122+
val dis = DataInputStream(ByteArrayInputStream(bytes.toByteArray()))
123+
val deserialized = AnrStackTrace.deserialize(dis)
124+
125+
assertNull(deserialized)
126+
}
97127
}

0 commit comments

Comments
 (0)