Skip to content

write()crashes with NullPointerException due to TOCTOU race condition between destroy()and write()on Android #233

Description

@maoweipeng

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

  1. Establish a TCP connection using react-native-tcp-socket.
  2. Call write()frequently on the socket (e.g., in a loop or via rapid user interaction).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions