fix(android): Break the app start extension lock-ordering deadlock - #6007
Open
runningcode wants to merge 3 commits into
Open
fix(android): Break the app start extension lock-ordering deadlock#6007runningcode wants to merge 3 commits into
runningcode wants to merge 3 commits into
Conversation
AppStartExtension held its own lock while calling finish() on the extended span and on the standalone app.start transaction. Finishing captures the transaction synchronously, which runs PerformanceAndroidEventProcessor, which acquires the processor lock and then calls back into isExtended() and getExtendedEndTime() -- taking the extension lock in the opposite order. Two threads is all it takes: the app calls Sentry.finishExtendedAppStart() (extension lock held, waiting for the processor lock) just as the app start deadline fires on the Sentry timer thread (processor lock held, waiting for the extension lock). Both threads hang; if the app finishes the extended app start on the main thread, that is an ANR. Read the fields under the lock and finish outside it, so the extension never holds its lock across a call that reenters the SDK. Span and transaction finishing are already idempotent, so racing callers are safe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📲 Install BuildsAndroid
|
runningcode
marked this pull request as ready for review
August 26, 2026 14:44
runningcode
requested review from
0xadam-brown,
adinauer,
markushi and
romtsn
as code owners
August 26, 2026 14:44
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e328f08. Configure here.
buenaflor
approved these changes
Aug 26, 2026
buenaflor
left a comment
Contributor
There was a problem hiding this comment.
lgtm 👍 (there are some open bugbot reviews still)
do you think it's worth adding some small concurrency section in AGENTS.md or whatever skill we have in the repo
9 tasks
Moving finish() out of the extension lock let finishTransaction and finishExtendedAppStart interleave. finishTransaction reads the extended span's finish date to clamp the transaction end, so a span finishing in the gap between that read and transaction.finish() captures the transaction with an end earlier than its own child, and both paths could enter SentryTracer.finish concurrently. Serialize the two with a dedicated finishLock that the re-entrant capture path never acquires, so the clamp and the finish are atomic again without putting the processor lock back into a cycle with the extension lock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
markushi
approved these changes
Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

📜 Description
AppStartExtensionheld its own lock while callingfinish()on the extended span (finishExtendedAppStart) and on the standaloneapp.starttransaction (finishTransaction). This reads the fields under that lock and performs the finish outside it, and serializes the two finish paths with a separatefinishLockthat the re-entrant capture path never acquires.💡 Motivation and Context
Finishing captures the transaction synchronously —
SentryTracer.finish()callsscopes.captureTransaction(...)on the calling thread, which runsPerformanceAndroidEventProcessor.process(). That processor takes its own lock and then calls back intoAppStartExtension. So the two locks are acquired in opposite orders:cc59f486daAppStartExtension.finishExtendedAppStart():111andfinishTransaction():148hold the lock acrossspan.finish()/transaction.finish(), which capture re-entrantlyPerformanceAndroidEventProcessor.process()acquires its lock at :82, then callsextension.isExtended()at :112 andgetExtendedEndTime()at :113 inside itBoth halves were introduced together in #5604, first released in 8.48.0.
The collision window is the one the feature is built around: the app calls
Sentry.finishExtendedAppStart()just as the app start deadline fires on the Sentry timer thread. If the app finishes the extended app start on the main thread, this is an ANR.Why a second lock
Moving the finish out of
lockon its own would letfinishTransactionandfinishExtendedAppStartinterleave, which the bugbot reviews correctly caught.finishTransactionreads the extended span's finish date to clamp the transaction end, so a span finishing between that read andtransaction.finish()captures the transaction with an end earlier than its own child — and both paths could enterSentryTracer.finishconcurrently, which is not guarded (hasAllChildrenFinished()treats a span with a finish date butisFinished() == falseas finished, so two callers can pass the same guard).finishLockrestores that mutual exclusion. It is never acquired by the re-entrant capture path, so it cannot participate in the cycle. Order when both are held:finishLock, thenlock.This restores parity with the original code rather than perfection — a caller that finishes the span directly via
getExtendedAppStartSpan()bypasses both locks, exactly as it did before.📊 Evidence in production
sdk-crashes-java, last 90 days — ANR events withAppStartExtensionon the stack:The series starts at 8.48.0 — the release that shipped #5604.
Blocked waiting for the lock — SDK-CRASHES-JAVA-5MBP, main thread, foreground app start, SDK 8.49.0:
In the
8.49.0tag,AppStartExtension.java:148is thelock.acquire()this PR moves work out of.Holding the lock — SDK-CRASHES-JAVA-5MEQ, 87 occurrences:
These events contain only the main thread, so the full cycle is not directly visible. What is confirmed is the mechanism this PR removes: the extension lock held across
transaction.finish(), with real users' main threads blocking on it until the OS files an ANR.💚 How did you test it?
Three regression tests in
AppStartExtensionTest:finish()to re-enter the extension from another thread and require that call to complete while the finish is in flight — both fail onmain, where the lock is held across the finishfinishTransactionandfinishExtendedAppStartnot to interleave — fails against the first commit of this PR, which is the regression the bugbot reviews foundFull
:sentry-android-core:testReleaseUnitTestpasses.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
extendAppStart()still calls the listener under the lock. That path does not capture, so it is not part of this cycle.