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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions