Description
When calling destroy()(which sets socket = null) concurrently with write()on a different thread, a NullPointerException occurs because write()does not synchronize access to the socketfield. The destroy()method closes the socket and sets it to null, but the write()method checks if (socket == null)only after obtaining the reference – yet between that check and the actual socket.getOutputStream()call, another thread may have already set socket = null. This is a classic Time-of-Check Time-of-Use (TOCTOU) race condition.
The crash stack trace confirms the issue:
java.lang.RuntimeException: java.lang.NullPointerException: Attempt to invoke virtual method 'java.io.OutputStream java.net.Socket.getOutputStream()' on a null object reference
at com.asterinet.react.tcpsocket.TcpSocketClient$1.run(TcpSocketClient.java:166)
...
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.io.OutputStream java.net.Socket.getOutputStream()' on a null object reference
at com.asterinet.react.tcpsocket.TcpSocketClient$1.run(TcpSocketClient.java:163)
Steps to reproduce
- Establish a TCP connection using react-native-tcp-socket.
- Call write()frequently on the socket (e.g., in a loop or via rapid user interaction).
- Simultaneously call destroy()on the same socket (e.g., on component unmount or timeout).
Current behavior
Application crashes with NullPointerException: Attempt to invoke virtual method 'java.io.OutputStream java.net.Socket.getOutputStream()' on a null object reference.
Expected behavior
uses a local variable snapshot to ensure the reference is read only once, avoiding the TOCTOU race condition.Specifically, change:
if (socket == null) {
receiverListener.onError(getId(), new IOException("Attempted to write to closed socket"));
return;
}
try {
socket.getOutputStream().write(data);
receiverListener.onWritten(getId(), msgId, null);
} catch (IOException e) {
receiverListener.onWritten(getId(), msgId, e);
receiverListener.onError(getId(), e);
}
to:
final Socket s = socket;
if (s == null) {
receiverListener.onError(getId(), new IOException("Attempted to write to closed socket"));
return;
}
try {
s.getOutputStream().write(data);
receiverListener.onWritten(getId(), msgId, null);
} catch (IOException e) {
receiverListener.onWritten(getId(), msgId, e);
receiverListener.onError(getId(), e);
}
This ensures that even if destroy() sets the socket field to null after taking a snapshot, the local reference will still not be null, thus preventing NullPointerException.
Screenshots
N/A (crash log provided above).
Relevant information
|
|
| OS |
Android (any version) |
| react-native |
react-native(any version) |
| react-native-tcp-socket |
6.4.1 |
Description
When calling destroy()(which sets socket = null) concurrently with write()on a different thread, a NullPointerException occurs because write()does not synchronize access to the socketfield. The destroy()method closes the socket and sets it to null, but the write()method checks if (socket == null)only after obtaining the reference – yet between that check and the actual socket.getOutputStream()call, another thread may have already set socket = null. This is a classic Time-of-Check Time-of-Use (TOCTOU) race condition.
The crash stack trace confirms the issue:
Steps to reproduce
Current behavior
Application crashes with NullPointerException: Attempt to invoke virtual method 'java.io.OutputStream java.net.Socket.getOutputStream()' on a null object reference.
Expected behavior
uses a local variable snapshot to ensure the reference is read only once, avoiding the TOCTOU race condition.Specifically, change:
to:
This ensures that even if destroy() sets the socket field to null after taking a snapshot, the local reference will still not be null, thus preventing NullPointerException.
Screenshots
N/A (crash log provided above).
Relevant information