Skip to content

Negative retry_in can crash Android SDK via Thread.sleep(millis < 0) #662

@liwei-domusnext

Description

@liwei-domusnext
## Affected Version

`com.adjust.sdk:adjust-android:5.4.6`

I also checked the latest Maven Central release, `5.7.0`, and the related source code appears unchanged.

## Crash

We observed the following fatal crash in production:

```text
Fatal Exception: java.lang.IllegalArgumentException: millis < 0: -9000
    at java.lang.Thread.sleep(Thread.java:399)
    at java.lang.Thread.sleep(Thread.java:355)
    at java.lang.Thread.run(Thread.java:1012)

Problem

If the backend response contains a negative retry_in value, the SDK passes it directly to Thread.sleep(long) through SingleThreadCachedScheduler.schedule().

On Android, Thread.sleep(long) throws IllegalArgumentException when millis is negative. Since the SDK only catches InterruptedException, this can crash the app.

Source Code

Repository:

https://github.com/adjust/android_sdk

ActivityPackageSender

responseData.retryIn = UtilNetworking.extractJsonLong(jsonResponse, "retry_in");
responseData.continueIn = UtilNetworking.extractJsonLong(jsonResponse, "continue_in");

PackageHandler

if (responseData.retryIn != null) {
    long retryIn = responseData.retryIn;
    scheduler.schedule(runnable, retryIn);
    return;
}

SingleThreadCachedScheduler

try {
    Thread.sleep(millisecondsDelay);
} catch (InterruptedException e) {
    AdjustFactory.getLogger().warn("Sleep delay exception: %s", e.getMessage());
}

IllegalArgumentException is not caught here.

Expected Behavior

The SDK should not crash when retry_in is negative.

It should either:

  • clamp negative values to 0
  • ignore invalid negative retry_in
  • treat negative retry_in as immediate retry
  • or catch IllegalArgumentException in the scheduler

Suggested Fix

One possible fix is to validate retry_in before scheduling:

if (responseData.retryIn != null && responseData.retryIn > 0) {
    long retryIn = responseData.retryIn;
    scheduler.schedule(runnable, retryIn);
    return;
}

Alternatively, the scheduler could guard the delay value:

Thread.sleep(Math.max(0, millisecondsDelay));

Additional Context

continue_in already has a positive-value guard:

if (previousResponseContinueIn != null && previousResponseContinueIn > 0) {
    scheduler.schedule(runnable, previousResponseContinueIn);
}

But retry_in does not have the same guard.

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