diff --git a/CHANGELOG.md b/CHANGELOG.md index cbf192eeb3..cf8fe55f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Avoid `StackOverflowError` in `Scopes.isAncestorOf` on deep `Scopes` chains ([#6249](https://github.com/getsentry/sentry-java/pull/6249)) + ## 8.60.0 ### Features diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index d3926d5699..7eccbabd8b 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -96,18 +96,13 @@ private Scopes( @Override @ApiStatus.Internal public boolean isAncestorOf(final @Nullable IScopes otherScopes) { - if (otherScopes == null) { - return false; - } - - if (this == otherScopes) { - return true; - } - - if (otherScopes.getParentScopes() != null) { - return isAncestorOf(otherScopes.getParentScopes()); + @Nullable IScopes candidate = otherScopes; + while (candidate != null) { + if (this == candidate) { + return true; + } + candidate = candidate.getParentScopes(); } - return false; } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 13a57ebe70..7e4a71d580 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -169,6 +169,38 @@ class ScopesTest { assertEquals("b", cloneScope!!.tags["scopes"]) } + @Test + fun `isAncestorOf does not overflow the stack on a deep chain`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val root = createScopes(options) + var deepest: IScopes = root + repeat(20_000) { deepest = deepest.forkedCurrentScope("deep") } + + val result = AtomicReference() + val thread = + Thread( + null, + { + result.set( + try { + root.isAncestorOf(deepest) + } catch (e: StackOverflowError) { + e + } + ) + }, + "small-stack", + 256 * 1024, + ) + thread.start() + thread.join() + + assertEquals(true, result.get()) + } + @Test fun `when scopes is initialized, breadcrumbs are capped as per options`() { val options = SentryOptions()