Skip to content

Commit 83a416d

Browse files
denrasebuenaflor
andauthored
fix: Avoid stack overflow when deserializing large flat JSON objects (getsentry#5361)
* fix: Avoid stack overflow when deserializing large flat JSON objects Replace JsonObjectDeserializer's recursive token parsing with an iterative loop. The parser already tracks state explicitly, so recursion was only used to advance to the next JSON token and could overflow on large flat maps. Relates to getsentry/sentry-dart#3668 * use larger module count vm might not respect stack size, so increase the frame count (with smaller payload) to make test more robust * add cl entries * fix cl entry * Update CHANGELOG.md --------- Co-authored-by: Giancarlo Buenaflor <giancarlo_buenaflor@yahoo.com>
1 parent e3e78e1 commit 83a416d

3 files changed

Lines changed: 90 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
- [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v950)
3636
- [diff](https://github.com/gradle/gradle/compare/v9.4.1...v9.5.0)
3737

38+
### Fixes
39+
40+
- Avoid stack overflow when deserializing large flat JSON objects ([#5361](https://github.com/getsentry/sentry-java/pull/5361))
41+
3842
## 8.40.0
3943

4044
### Fixes

sentry/src/main/java/io/sentry/JsonObjectDeserializer.java

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -82,49 +82,48 @@ private static final class TokenMap implements Token {
8282

8383
private void parse(@NotNull JsonObjectReader reader) throws IOException {
8484
boolean done = false;
85-
switch (reader.peek()) {
86-
case BEGIN_ARRAY:
87-
reader.beginArray();
88-
pushCurrentToken(new TokenArray());
89-
break;
90-
case END_ARRAY:
91-
reader.endArray();
92-
done = handleArrayOrMapEnd();
93-
break;
94-
case BEGIN_OBJECT:
95-
reader.beginObject();
96-
pushCurrentToken(new TokenMap());
97-
break;
98-
case END_OBJECT:
99-
reader.endObject();
100-
done = handleArrayOrMapEnd();
101-
break;
102-
case NAME:
103-
pushCurrentToken(new TokenName(reader.nextName()));
104-
break;
105-
case STRING:
106-
// avoid method refs on Android due to some issues with older AGP setups
107-
// noinspection Convert2MethodRef
108-
done = handlePrimitive(() -> reader.nextString());
109-
break;
110-
case NUMBER:
111-
done = handlePrimitive(() -> nextNumber(reader));
112-
break;
113-
case BOOLEAN:
114-
// avoid method refs on Android due to some issues with older AGP setups
115-
// noinspection Convert2MethodRef
116-
done = handlePrimitive(() -> reader.nextBoolean());
117-
break;
118-
case NULL:
119-
reader.nextNull();
120-
done = handlePrimitive(() -> null);
121-
break;
122-
case END_DOCUMENT:
123-
done = true;
124-
break;
125-
}
126-
if (!done) {
127-
parse(reader);
85+
while (!done) {
86+
switch (reader.peek()) {
87+
case BEGIN_ARRAY:
88+
reader.beginArray();
89+
pushCurrentToken(new TokenArray());
90+
break;
91+
case END_ARRAY:
92+
reader.endArray();
93+
done = handleArrayOrMapEnd();
94+
break;
95+
case BEGIN_OBJECT:
96+
reader.beginObject();
97+
pushCurrentToken(new TokenMap());
98+
break;
99+
case END_OBJECT:
100+
reader.endObject();
101+
done = handleArrayOrMapEnd();
102+
break;
103+
case NAME:
104+
pushCurrentToken(new TokenName(reader.nextName()));
105+
break;
106+
case STRING:
107+
// avoid method refs on Android due to some issues with older AGP setups
108+
// noinspection Convert2MethodRef
109+
done = handlePrimitive(() -> reader.nextString());
110+
break;
111+
case NUMBER:
112+
done = handlePrimitive(() -> nextNumber(reader));
113+
break;
114+
case BOOLEAN:
115+
// avoid method refs on Android due to some issues with older AGP setups
116+
// noinspection Convert2MethodRef
117+
done = handlePrimitive(() -> reader.nextBoolean());
118+
break;
119+
case NULL:
120+
reader.nextNull();
121+
done = handlePrimitive(() -> null);
122+
break;
123+
case END_DOCUMENT:
124+
done = true;
125+
break;
126+
}
128127
}
129128
}
130129

sentry/src/test/java/io/sentry/SentryEventTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package io.sentry
33
import io.sentry.exception.ExceptionMechanismException
44
import io.sentry.protocol.Mechanism
55
import io.sentry.protocol.SentryId
6+
import java.io.StringReader
67
import java.time.Instant
78
import java.time.temporal.ChronoUnit
89
import java.util.Collections
10+
import java.util.concurrent.atomic.AtomicReference
911
import kotlin.test.Test
1012
import kotlin.test.assertEquals
1113
import kotlin.test.assertFalse
@@ -174,6 +176,48 @@ class SentryEventTest {
174176
}
175177
}
176178

179+
@Test
180+
fun `deserializes event with large flat modules map on a small stack`() {
181+
val moduleCount = 50000
182+
val json = buildString {
183+
append("{\"event_id\":\"00000000000000000000000000000000\",\"modules\":{")
184+
repeat(moduleCount) {
185+
if (it > 0) {
186+
append(',')
187+
}
188+
append("\"m")
189+
append(it)
190+
append("\":\"v\"")
191+
}
192+
append("}}")
193+
}
194+
195+
val error = AtomicReference<Throwable?>()
196+
val event = AtomicReference<SentryEvent?>()
197+
val thread =
198+
Thread(
199+
null,
200+
Runnable {
201+
try {
202+
event.set(
203+
JsonSerializer(SentryOptions())
204+
.deserialize(StringReader(json), SentryEvent::class.java)
205+
)
206+
} catch (throwable: Throwable) {
207+
error.set(throwable)
208+
}
209+
},
210+
"large-flat-modules-repro",
211+
1024L * 1024L,
212+
)
213+
214+
thread.start()
215+
thread.join()
216+
217+
assertNull(error.get())
218+
assertEquals(moduleCount, event.get()?.modules?.size)
219+
}
220+
177221
@Test
178222
fun `null tag does not cause NPE`() {
179223
val event = SentryEvent()

0 commit comments

Comments
 (0)