Skip to content

net: tryReadStart destroys socket on UV_EALREADY instead of ignoring it #62400

@dirkwa

Description

@dirkwa

Version

v26.0.0-pre (main branch, also affects v24.12.0)

Platform

Linux 6.12.73+deb13-amd64 x86_64

Subsystem

net

What steps will reproduce the bug?

tryReadStart() in lib/net.js destroys the socket when readStart() returns UV_EALREADY. This can be reproduced by calling readStart when reading is already active at the libuv level:

const net = require('net');
const { internalBinding } = require('internal/test/binding');
const { UV_EALREADY } = internalBinding('uv');

const server = net.createServer((conn) => { conn.on('error', () => {}); });
server.listen('/tmp/test-ealready.sock', () => {
  const socket = net.createConnection({ path: '/tmp/test-ealready.sock' });
  socket.on('connect', () => {
    // Simulate readStart returning UV_EALREADY (happens with sockets
    // opened via uv_pipe_open where reading is already active)
    socket._handle.readStart = () => UV_EALREADY;
    socket._handle.reading = false;
    socket._read(16384);

    // Socket is now destroyed even though reading was already active
    console.log('destroyed:', socket.destroyed); // true
    socket.destroy();
  });
  socket.on('error', (e) => console.log('error:', e.code)); // EALREADY
  socket.on('close', () => server.close());
});

In practice, this happens when socket types with no async connect phase (synchronous bind, 'connect' emitted via nextTick) cause multiple readStart calls in the same microtask batch. Discovered while implementing AF_CAN socket support (#62398), but the bug is in tryReadStart itself.

How often does it reproduce? Is there a required condition?

100% reproducible. The condition is readStart() returning UV_EALREADY, which occurs when:

  • A socket fd is opened via uv_pipe_open() and readStart is called more than once
  • The deferred _read callback (queued while connecting=true) and the stream resume() from a 'data' listener both fire in the same tick

What is the expected behavior? Why is that the expected behavior?

UV_EALREADY means "reading is already in progress." The socket should continue operating normally. tryReadStart should be idempotent — if reading is already active, there's nothing to do.

What do you see instead?

The socket is destroyed with Error: read EALREADY. The 'error' event fires and the socket becomes unusable

Additional information

Fix with regression test: https://github.com/dirkwa/node/tree/fix-tryreadstart-ealready

The fix adds a guard for UV_EALREADY in tryReadStart, making it ignore the "already reading" case instead of destroying the socket. This matches the intent of the existing JS-side reading flag guard in Socket._read.

Refs: #62398

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