Skip to content

[Android] Fatal NPE in revopush-diff FileUtils.copyDirectoryContents (local asset diff) escapes downloadUpdate → crash loop on every launch #55

Description

@yugun-ilevit-com

Summary

On Android, downloadUpdate crashes the whole app with an uncaught NullPointerException thrown inside revopush-diff (org.revopush.ota.utils.FileUtils.copyDirectoryContents, "Attempt to get length of null array"). Because the exception escapes CodePushNativeModule$3.doInBackground, AsyncTask rethrows it as a fatal RuntimeException, and since the failed package is never recorded, the same diff download is retried and crashes again on every app launch. Affected devices are stuck in a crash loop until the app data is cleared.

We see it in production (Sentry, ~1,050 events / 30 users over 30 days, up to ~110 events per device per week) across all our Android store builds since 2026-05.

Environment

  • @revopush/react-native-code-push: 2.5.0-rc.7 (pins org.revopush:revopush-diff:0.0.7)
  • react-native 0.79.6, New Architecture (fabric + bridgeless), Hermes
  • Android 12–16, various Samsung devices (no device/OS skew)
  • Server: Revopush cloud; releases created with revopush release-expo (CLI 0.0.13)

Stack trace

java.lang.RuntimeException: An error occurred while executing doInBackground()
    at android.os.AsyncTask$4.done(AsyncTask.java:415)
    ...
Caused by: java.lang.NullPointerException: Attempt to get length of null array
    at org.revopush.ota.utils.FileUtils.copyDirectoryContents(SourceFile:12)
    at org.revopush.ota.BundleManager.a(SourceFile:51)
    at org.revopush.ota.BundleManager.downloadPackage(SourceFile:18)
    at com.microsoft.codepush.react.CodePushUpdateManager.downloadPackage(CodePushUpdateManager.java:171)
    at com.microsoft.codepush.react.CodePushNativeModule$3.doInBackground(CodePushNativeModule.java:431)
    at com.microsoft.codepush.react.CodePushNativeModule$3.doInBackground(CodePushNativeModule.java:425)
    at android.os.AsyncTask$3.call(AsyncTask.java:394)

Analysis

revopush-diff has no public source, so this is from the AAR bytecode (javap), checked on both 0.0.7 and the latest 0.0.9 (Maven, 2026-09-03):

  1. BundleManager.downloadPackage takes the diff path when a current package exists and the update has a bundleDiffBlobUrl. Inside the asset-diff step (the private a(UpdatePackage, BiFunction, JSONObject, File, String, String, String, String) overload), the "local asset diff" branch calls FileUtils.copyDirectoryContents(currentPackageFolderPath, newPackageFolderPath, predicate) guarded only by a null check on the path string, not by File.exists().
  2. FileUtils.copyDirectoryContents(String, String, Predicate) does sourceDir.listFiles() and iterates with arraylength directly. listFiles() returns null when the directory does not exist, hence the NPE.
  3. copyNecessaryFilesFromCurrentPackage in the same AAR does have the exists() guard (the one Microsoft added in Fix NPE when invoking copyNecessaryFilesFromCurrentPackage() method on Android microsoft/react-native-code-push#2566 for the identical crash, see also NullPointerException in FileUtils.copyDirectoryContents microsoft/react-native-code-push#1584), but it was not carried over to the new local-asset-diff path.
  4. CodePushNativeModule.downloadUpdate only catches CodePushInvalidUpdateException and IOException | CodePushUnknownException, so the NPE is fatal, saveFailedUpdate is never called, and the device retries the same diff on the next launch.

0.0.9 (as pinned by SDK 2.6.1 / 2.5.2) has the same bytecode for both the call site and copyDirectoryContents, so upgrading does not fix it.

We have not yet identified what removes the current package folder while codepush.json still points at it (candidates: OS storage cleanup on low-space devices, as in the Microsoft issue).

Suggested fix

  • In revopush-diff, guard the local asset diff copy with currentPackageFolderPath == null || !new File(currentPackageFolderPath).exists() → skip the copy and fall through to the existing full-bundle fallback (same as Fix NPE when invoking copyNecessaryFilesFromCurrentPackage() method on Android microsoft/react-native-code-push#2566 upstream), and/or make copyDirectoryContents treat a null listFiles() as empty.
  • In CodePushNativeModule.downloadUpdate, catch RuntimeException as well, so a failure inside the native diff code rejects the promise instead of killing the process. We are shipping this as a patch-package in the meantime:
} catch (RuntimeException e) {
    CodePushUtils.log(e);
    mCodePush.clearUpdates(); // drop the stale package state so the next sync gets a full bundle
    promise.reject(e);
}

Happy to provide Sentry event JSON or test a build with a fixed AAR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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