getsentry / getsentry/sentry-java

Disk-persisted data can race during SDK re-init

Aperta
#5,959 1 commento 0 reazioni 0 assegnatari Vedi su GitHub
Android Improvement Platform: Java Type: Bug
Lingua principale
Kotlin
Stelle
1.4k
Fork
478
Merge medio
2g 23h
PR unite (30g)
67

Descrizione

During SDK re-init (`Sentry.init` while already enabled), previous and new SDK lifecycles can concurrently read/write the same on-disk paths under `cacheDirPath`. On restart, `Scopes.close(true)` shuts the old executor down asynchronously rather than blocking, so old flush/session/envelope work can still touch files while the new init writes the same locations.

Impact: corrupted or lost scope/options/session state used for ANR/exit enrichment and crash recovery; possible lost breadcrumbs.

## Why this races

- Re-init path closes previous scopes with `isRestarting=true`, then continues init immediately: [`Sentry.init`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Sentry.java) → [`Scopes.close(true)`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Scopes.java)
- On restart the old executor is closed via a submitted task (non-blocking), so pending disk work can still run briefly: [`Scopes.close(boolean)`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Scopes.java)
- New init then enqueues session move/finalize, options observers, and scope-cache reset on the **new** executor against the same cache paths: [`notifyOptionsObservers` / `movePreviousSession` / `finalizePreviousSession`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Sentry.java)
- Fixed filenames are not isolated per SDK lifecycle / run id

## Disk-backed surfaces that share paths across re-init

**Scope cache** (`.scope-cache/`, via [`PersistingScopeObserver`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java) + [`CacheUtils`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/CacheUtils.java)):
- `breadcrumbs.json` — single `QueueFile`; old executor may still add/sync while new init `resetCache()` clears/syncs the same file
- `user.json`, `tags.json`, `extras.json`, `contexts.json`, `request.json`, `level.json`, `fingerprint.json`, `transaction.json`, `trace.json`, `replay.json` — plain overwrite/`delete` of fixed names; no atomic replace
- New init explicitly `resetCache()` after options observers run, which collides with any late old-lifecycle flushes

**Options cache** (`.options-cache/`, via [`PersistingOptionsObserver`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/PersistingOptionsObserver.java) and Android [`PersistingOptionsCacheGenerationObserver`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-core/src/main/java/io/sentry/android/core/PersistingOptionsCacheGenerationObserver.java)):
- `release.json`, `proguard-uuid.json`, `sdk-version.json`, `environment.json`, `dist.json`, `tags.json`, `replay-error-sample-rate.json`, `app-last-update-time.json`
- Written on every init via options observers; same fixed paths, non-atomic `FileOutputStream` overwrite

**Session / envelope cache** ([`EnvelopeCache`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java), Android [`AndroidEnvelopeCache`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java)):
- `session.json` / `previous_session.json` — move + rewrite on session start; previous-session finalize also reads/deletes these
- `*.envelope` offline cache — old client flush vs new client store/send against same dir
- crash markers: `last_crash`, `.sentry-native/last_crash`, `startup_crash` (outbox)
- Android ANR last-reported markers under cache dir

**Other fixed-path / shared-dir writers** (same `cacheDirPath`, lifecycle-overlapping on re-init):
- App-start profiling config file rewritten on init ([`Sentry.handleAppStartProfilingConfig`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Sentry.java))
- Profiling traces dir cleanup on init vs in-flight profile finalization
- Session Replay dirs (`replay_/`) under cache dir ([`ReplayCache`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt)); cleanup walks cache dir on integration start
- ANR profile `QueueFile`s under cache dir ([`AnrProfileManager`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java) / rotation helper already documents concurrent `QueueFile` risk)

## Highest-confidence race today

Scope persistence breadcrumbs: single shared `breadcrumbs.json` `QueueFile`, old executor still draining adds while new init clears it in `resetCache()`, with only executor ordering (not cross-lifecycle isolation) as the safety net.

## Related

- Closed perf work on scope persistence writes: https://github.com/getsentry/sentry-java/issues/5714
- Closed re-init executor timeout work: https://github.com/getsentry/sentry-java/issues/5715
- Closed “don’t wait when SDK is reinitialized”: https://github.com/getsentry/sentry-java/issues/3162

## Unknowns

- Whether this has been observed in production beyond code inspection / reasoning about re-init
- How often customer/integration re-init hits the overlapping-executor window in practice

Raised by Markus Hintersteiner during SDK re-init / disk-persistence discussion.

Requested by **markus.hintersteiner**.

--

[View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3ACUCRT2B70%3A1786606836.172319) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3ACUCRT2B70%3A1786606836.172319/?project=4510944073809921)

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

Trace re-initialization from Sentry.init through Scopes.close(boolean), then inspect PersistingScopeObserver, CacheUtils, PersistingOptionsObserver, and EnvelopeCache. Focus first on the shared breadcrumbs.json QueueFile and the old executor's shutdown behavior; done should demonstrate that re-init cannot overlap disk operations or lose persisted scope, options, session, or envelope data.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
java, kotlin
Ambito
mobile, tooling
Tipo di issue
Bug
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Tranquilla
Chiarezza
Abbastanza chiara
Idoneità per principianti
35/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.