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
- Have a contact with no photo (e.g. a freshly created contact).
- Call
Contacts.writePhotoToPath(contactId, '/some/path.png').
- 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:
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.)
- 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.
ContactsManagerImpl.writePhotoToPath()hard-crashes the app when called for acontact that has no photo.
ContactsContract.Contacts.openContactPhotoInputStream()returns
nullin 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/catchor.catch()on the JS side can intercept it).Version: 8.0.10
Platform: Android
Repro
Contacts.writePhotoToPath(contactId, '/some/path.png').Crash log
React Native 0.86.3, Android 17 (preview) emulator, x86_64:
Root cause
ContactsManagerImpl.java, lines 220–248:Two issues:
inputStreamis never null-checked; line 244'sinputStream.close()throwsthe NPE. (Line 230's
decodeStream(inputStream)is the same hazard, anddecodeStreamcan also return anullbitmap, making.compress(...)asecond NPE path.)
inputStream.close()sits outside thefinally, so it's skipped on any exception in thetry— a stream leak.Suggested fix
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
updateContactwiththumbnailPath, see #822). Worth a JSDoc clarification onindex.d.ts/index.tswhile the function is being touched.