SDK Version
3.6.5
Platform
Android
Description
We are seeing a production crash (NullPointerException) inside EmbeddedSessionManager.updateDisplayCountAndDuration() at line 114. The crash occurs when endSession() or pauseImpression() is called from a background thread (e.g. a Dispatchers.Default coroutine worker), and another thread concurrently modifies the impression state.
This is a TOCTOU (time-of-check/time-of-use) race condition in EmbeddedImpressionData. The field start: Date? is a plain Kotlin var with no @Volatile annotation and no synchronisation. The sequence that causes the crash is:
- Thread A calls
updateDisplayCountAndDuration() and passes the if (start != null) null check.
- Thread B sets
start = null (e.g. via a concurrent endAllImpressions() reset).
- Thread A dereferences
start!! → NPE.
Stack Traces
Crash 1 — via endSession():
Fatal Exception: java.lang.NullPointerException
at com.iterable.iterableapi.EmbeddedSessionManager.updateDisplayCountAndDuration(EmbeddedSessionManager.kt:114)
at com.iterable.iterableapi.EmbeddedSessionManager.endAllImpressions(EmbeddedSessionManager.kt:91)
at com.iterable.iterableapi.EmbeddedSessionManager.endSession(EmbeddedSessionManager.kt:41)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Crash 2 — via pauseImpression():
Fatal Exception: java.lang.NullPointerException
at com.iterable.iterableapi.EmbeddedSessionManager.updateDisplayCountAndDuration(EmbeddedSessionManager.kt:114)
at com.iterable.iterableapi.EmbeddedSessionManager.pauseImpression(EmbeddedSessionManager.kt:86)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Both crashes occur on CoroutineScheduler$Worker — i.e. Dispatchers.Default (thread pool), not the main thread.
Root Cause
In EmbeddedImpressionData, the start field is declared as a plain mutable property:
In EmbeddedSessionManager.updateDisplayCountAndDuration() (line ~110–115):
if (start != null) {
// ... Thread B sets start = null here ...
val duration = Date().time - start!!.time // ← NPE
}
Because start is neither @Volatile nor guarded by a lock, the JVM is free to cache the null-check result in a register. A concurrent write from another thread makes the dereference unsafe.
Suggested Fix
The simplest fix is to capture start in a local variable before the null-check, which is the standard safe-access pattern in concurrent Kotlin/Java:
val startSnapshot = start
if (startSnapshot != null) {
val duration = Date().time - startSnapshot.time // safe — local val is thread-confined
}
Alternatively, marking the field as @Volatile would ensure visibility, though a local snapshot is still needed to prevent the race window.
A broader fix would be to add @MainThread annotations to EmbeddedSessionManager's public API to document that it is not thread-safe and must be called on the main thread.
Workaround (client-side)
We are currently working around this by dispatching all EmbeddedSessionManager calls onto Dispatchers.Main, which serialises access to a single thread. However, this is a client-side workaround for a thread-safety issue in the SDK itself.
SDK Version
3.6.5Platform
Android
Description
We are seeing a production crash (
NullPointerException) insideEmbeddedSessionManager.updateDisplayCountAndDuration()at line 114. The crash occurs whenendSession()orpauseImpression()is called from a background thread (e.g. aDispatchers.Defaultcoroutine worker), and another thread concurrently modifies the impression state.This is a TOCTOU (time-of-check/time-of-use) race condition in
EmbeddedImpressionData. The fieldstart: Date?is a plain Kotlinvarwith no@Volatileannotation and no synchronisation. The sequence that causes the crash is:updateDisplayCountAndDuration()and passes theif (start != null)null check.start = null(e.g. via a concurrentendAllImpressions()reset).start!!→ NPE.Stack Traces
Crash 1 — via
endSession():Crash 2 — via
pauseImpression():Both crashes occur on
CoroutineScheduler$Worker— i.e.Dispatchers.Default(thread pool), not the main thread.Root Cause
In
EmbeddedImpressionData, thestartfield is declared as a plain mutable property:In
EmbeddedSessionManager.updateDisplayCountAndDuration()(line ~110–115):Because
startis neither@Volatilenor guarded by a lock, the JVM is free to cache the null-check result in a register. A concurrent write from another thread makes the dereference unsafe.Suggested Fix
The simplest fix is to capture
startin a local variable before the null-check, which is the standard safe-access pattern in concurrent Kotlin/Java:Alternatively, marking the field as
@Volatilewould ensure visibility, though a local snapshot is still needed to prevent the race window.A broader fix would be to add
@MainThreadannotations toEmbeddedSessionManager's public API to document that it is not thread-safe and must be called on the main thread.Workaround (client-side)
We are currently working around this by dispatching all
EmbeddedSessionManagercalls ontoDispatchers.Main, which serialises access to a single thread. However, this is a client-side workaround for a thread-safety issue in the SDK itself.