Skip to content

net.Socket has no prependListener/prependOnceListener: calling them silently does nothing, so the listener never sees data (ioredis/iovalkey hang waiting for replies) #10441

Closed

Description

@proggeramlug

Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. On a net.Socket, prependListener and prependOnceListener read as undefined, but calling them does not throw. The call returns undefined, registers nothing, and the socket's 'data' never reaches the listener.

Reproduction

No dependencies. A local echo server is part of the script.

main.ts

import * as net from 'node:net';

const server = net.createServer((conn) => conn.on('data', (d) => conn.write(d)));  // echo
server.listen(0, '127.0.0.1', async () => {
  const port = (server.address() as net.AddressInfo).port;
  for (const method of ['on', 'prependListener', 'prependOnceListener']) {
    await new Promise<void>((resolve) => {
      const sock: any = net.createConnection({ host: '127.0.0.1', port });
      const timer = setTimeout(() => { console.log(method, '-> no data after 1s'); sock.destroy(); resolve(); }, 1000);
      sock.on('connect', () => {
        const ret = sock[method]('data', (d: Buffer) => {
          console.log(method, '-> data', JSON.stringify(d.toString()));
          clearTimeout(timer); sock.destroy(); resolve();
        });
        console.log(method, 'typeof:', typeof sock[method], 'returns socket:', ret === sock, 'listenerCount:', sock.listenerCount('data'));
        sock.resume();   // prepend*Listener does not start flowing mode by itself
        sock.write('ping');
      });
    });
  }
  server.close();
});
node main.ts
perry compile main.ts -o out && ./out

Expected (Node 26.5.1)

on typeof: function returns socket: true listenerCount: 1
on -> data "ping"
prependListener typeof: function returns socket: true listenerCount: 1
prependListener -> data "ping"
prependOnceListener typeof: function returns socket: true listenerCount: 1
prependOnceListener -> data "ping"

Actual (Perry)

Same output with and without PERRY_NO_AUTO_OPTIMIZE=1:

on typeof: function returns socket: true listenerCount: 1
on -> data "ping"
prependListener typeof: undefined returns socket: false listenerCount: 0
prependListener -> no data after 1s
prependOnceListener typeof: undefined returns socket: false listenerCount: 0
prependOnceListener -> no data after 1s

The static form sock.prependListener('data', fn) behaves the same way. Calling a method that does not exist at all, such as sock.noSuchMethod('x'), also returns undefined. Node throws TypeError: sock.noSuchMethod is not a function. So a missing socket method shows up as a hang, not as an error.

Impact

  • ioredis 5.11.1: built/DataHandler.js:25 attaches the RESP parser with
    redis.stream.prependListener("data", (data) => parser.execute(data)) and then calls redis.stream.resume(). Under Perry the parser
    never gets a byte. The client connects and writes CLIENT SETINFO (the server receives it), but no reply ever resolves, so
    ready never fires and every command promise hangs.
  • iovalkey 0.4.0: same code at built/DataHandler.js:24.
  • If that line is changed to redis.stream.on("data", …), the audit's full ioredis test matches Node byte for byte. The same is true for iovalkey.
    The test covers strings with EX/ttl, incr/mget, hash, list, set, zset, pipeline, multi, eval, WRONGTYPE error, getBuffer, keyPrefix, pub/sub and quit.
    This was the last Perry defect on that path after the workarounds below.
  • Any other stream consumer that uses prependListener on a socket is likely affected (inferred).

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions