Skip to content

NullPointerException in EmbeddedSessionManager.updateDisplayCountAndDuration() — thread-safety race condition #1052

@Shamyyoun

Description

@Shamyyoun

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:

  1. Thread A calls updateDisplayCountAndDuration() and passes the if (start != null) null check.
  2. Thread B sets start = null (e.g. via a concurrent endAllImpressions() reset).
  3. 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:

var start: Date? = null

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions