From 80505436243895cba0a290cd81f356706028b7ba Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 14 Jul 2026 13:30:03 +0200 Subject: [PATCH 1/2] fix potential infinite loops on cyclic Throwable.getCause() --- ...DuplicateEventDetectionEventProcessor.java | 12 +++++--- .../java/io/sentry/util/ExceptionUtils.java | 10 +++++-- ...plicateEventDetectionEventProcessorTest.kt | 28 +++++++++++++++++++ .../java/io/sentry/util/ExceptionUtilsTest.kt | 26 +++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java b/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java index fc178d789f3..b46eb91746b 100644 --- a/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java +++ b/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java @@ -2,8 +2,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -54,10 +56,12 @@ private static boolean containsAnyKey( private static @NotNull List allCauses(final @NotNull Throwable throwable) { final List causes = new ArrayList<>(); - Throwable ex = throwable; - while (ex.getCause() != null) { - causes.add(ex.getCause()); - ex = ex.getCause(); + final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visited.add(throwable); + Throwable cause = throwable.getCause(); + while (cause != null && visited.add(cause)) { + causes.add(cause); + cause = cause.getCause(); } return causes; } diff --git a/sentry/src/main/java/io/sentry/util/ExceptionUtils.java b/sentry/src/main/java/io/sentry/util/ExceptionUtils.java index 9d6033a96c3..4f47fda72db 100644 --- a/sentry/src/main/java/io/sentry/util/ExceptionUtils.java +++ b/sentry/src/main/java/io/sentry/util/ExceptionUtils.java @@ -1,5 +1,7 @@ package io.sentry.util; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -16,8 +18,12 @@ public final class ExceptionUtils { public static @NotNull Throwable findRootCause(final @NotNull Throwable throwable) { Objects.requireNonNull(throwable, "throwable cannot be null"); Throwable rootCause = throwable; - while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { - rootCause = rootCause.getCause(); + final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visited.add(rootCause); + Throwable cause = rootCause.getCause(); + while (cause != null && visited.add(cause)) { + rootCause = cause; + cause = rootCause.getCause(); } return rootCause; } diff --git a/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt b/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt index 3e4141dfeee..8fb929224ea 100644 --- a/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt @@ -8,6 +8,21 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class DuplicateEventDetectionEventProcessorTest { + private class CircularCauseThrowable : RuntimeException() { + private var nextCause: Throwable? = null + private var causeReads = 0 + + fun linkTo(cause: Throwable) { + nextCause = cause + } + + override val cause: Throwable? + get() { + check(causeReads++ < 10) { "Throwable cause cycle was not detected" } + return nextCause + } + } + class Fixture { fun getSut(enableDeduplication: Boolean? = null): DuplicateEventDetectionEventProcessor { val options = @@ -99,6 +114,19 @@ class DuplicateEventDetectionEventProcessorTest { assertNull(result) } + @Test + fun `does not loop indefinitely for cyclic cause chain`() { + val processor = fixture.getSut() + val first = CircularCauseThrowable() + val second = CircularCauseThrowable() + first.linkTo(second) + second.linkTo(first) + + val result = processor.process(SentryEvent(first), Hint()) + + assertNotNull(result) + } + @Test fun `does not deduplicate is deduplication is disabled`() { val processor = fixture.getSut(enableDeduplication = false) diff --git a/sentry/src/test/java/io/sentry/util/ExceptionUtilsTest.kt b/sentry/src/test/java/io/sentry/util/ExceptionUtilsTest.kt index 7517c243497..6d77e5d2f96 100644 --- a/sentry/src/test/java/io/sentry/util/ExceptionUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/ExceptionUtilsTest.kt @@ -3,8 +3,24 @@ package io.sentry.util import java.lang.RuntimeException import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertSame class ExceptionUtilsTest { + private class CircularCauseThrowable : RuntimeException() { + private var nextCause: Throwable? = null + private var causeReads = 0 + + fun linkTo(cause: Throwable) { + nextCause = cause + } + + override val cause: Throwable? + get() { + check(causeReads++ < 10) { "Throwable cause cycle was not detected" } + return nextCause + } + } + @Test fun `returns same exception when there is no cause`() { val ex = RuntimeException() @@ -18,4 +34,14 @@ class ExceptionUtilsTest { val ex = RuntimeException(cause) assertEquals(rootCause, ExceptionUtils.findRootCause(ex)) } + + @Test + fun `does not loop indefinitely for cyclic cause chain`() { + val first = CircularCauseThrowable() + val second = CircularCauseThrowable() + first.linkTo(second) + second.linkTo(first) + + assertSame(second, ExceptionUtils.findRootCause(first)) + } } From 585da7f770cc1c6481216a15e5112574cf63c1fc Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 8 Sep 2026 14:34:45 +0200 Subject: [PATCH 2/2] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ef4c0bac8..f6fe53e86e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `SentryTraced` now checks for its owning transaction dynamically rather than once per app process. The latter caused `SentryTraced` spans to be dropped process-wide once the original transaction finished ([#6057](https://github.com/getsentry/sentry-java/pull/6057)) - Fix typos in Spring GraphQL integration names (`GrahQL` to `GraphQL`) ([#6061](https://github.com/getsentry/sentry-java/pull/6061)) +- Prevent infinite loops when capturing exceptions with cyclic cause chains ([#6073](https://github.com/getsentry/sentry-java/pull/6073)) ## 8.55.0