Skip to content

writePhotoToPath crashes the app with an uncaught NPE when the contact has no photo (Android) #834

Description

@air-fuel-ratio

ContactsManagerImpl.writePhotoToPath() hard-crashes the app when called for a
contact that has no photo. ContactsContract.Contacts.openContactPhotoInputStream()
returns null in that case, and the stream is dereferenced without a null check —
and because the exception is thrown on the module's background executor, it is
uncatchable from JS and kills the whole app (the Promise is never rejected, so no
try/catch or .catch() on the JS side can intercept it).

Version: 8.0.10
Platform: Android

Repro

  1. Have a contact with no photo (e.g. a freshly created contact).
  2. Call Contacts.writePhotoToPath(contactId, '/some/path.png').
  3. App crashes within a second.

Crash log

React Native 0.86.3, Android 17 (preview) emulator, x86_64:

java.lang.NullPointerException: Attempt to invoke virtual method 'void java.io.InputStream.close()' on a null object reference
        at com.rt2zz.reactnativecontacts.impl.ContactsManagerImpl.lambda$writePhotoToPath$6(ContactsManagerImpl.java:244)
        at com.rt2zz.reactnativecontacts.impl.ContactsManagerImpl.$r8$lambda$MzTJvhSukvUJEZudjLadLfDpnOQ(ContactsManagerImpl.java:0)
        at com.rt2zz.reactnativecontacts.impl.ContactsManagerImpl$$ExternalSyntheticLambda2.run(D8$$SyntheticClass:0)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:328)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.util.concurrent.Thread.run(Thread.java:1572)

Root cause

ContactsManagerImpl.java, lines 220–248:

InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(cr, uri); // null when no photo
OutputStream outputStream = null;
try {
    outputStream = new FileOutputStream(file);
    BitmapFactory.decodeStream(inputStream).compress(Bitmap.CompressFormat.PNG, 100, outputStream);
    ...
} finally {
    // only guards outputStream
}
try {
    inputStream.close();   // line 244 — NPE
}

Two issues:

  1. inputStream is never null-checked; line 244's inputStream.close() throws
    the NPE. (Line 230's decodeStream(inputStream) is the same hazard, and
    decodeStream can also return a null bitmap, making .compress(...) a
    second NPE path.)
  2. Even when the stream is non-null, inputStream.close() sits outside the
    finally, so it's skipped on any exception in the try — a stream leak.

Suggested fix

InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(cr, uri);
OutputStream outputStream = null;
try {
    if (inputStream == null) {
        promise.resolve(false); // contact has no photo — not an error
        return;
    }
    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
    if (bitmap == null) {
        promise.resolve(false);
        return;
    }
    outputStream = new FileOutputStream(file);
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
    promise.resolve(true);
} catch (FileNotFoundException e) {
    promise.reject(e.toString());
} finally {
    try { if (outputStream != null) outputStream.close(); } catch (IOException e) { e.printStackTrace(); }
    try { if (inputStream != null) inputStream.close(); } catch (IOException e) { e.printStackTrace(); }
}

Note the method also has a misleading name: it exports a contact's existing
photo to a file; it does not set a photo (the only way to set one is
updateContact with thumbnailPath, see #822). Worth a JSDoc clarification on
index.d.ts/index.ts while the function is being touched.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions