You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
net.Socket has no prependListener/prependOnceListener: calling them silently does nothing, so the listener never sees data (ioredis/iovalkey hang waiting for replies) #10441
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*asnetfrom'node:net';constserver=net.createServer((conn)=>conn.on('data',(d)=>conn.write(d)));// echoserver.listen(0,'127.0.0.1',async()=>{constport=(server.address()asnet.AddressInfo).port;for(constmethodof['on','prependListener','prependOnceListener']){awaitnewPromise<void>((resolve)=>{constsock: any=net.createConnection({host: '127.0.0.1', port });consttimer=setTimeout(()=>{console.log(method,'-> no data after 1s');sock.destroy();resolve();},1000);sock.on('connect',()=>{constret=sock[method]('data',(d: Buffer)=>{console.log(method,'-> data',JSON.stringify(d.toString()));clearTimeout(timer);sock.destroy();resolve();});console.log(method,'typeof:',typeofsock[method],'returns socket:',ret===sock,'listenerCount:',sock.listenerCount('data'));sock.resume();// prepend*Listener does not start flowing mode by itselfsock.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).
Location: crates/perry-ext-net/src/dispatch.rs:154 (socket_method_name) lists on/addListener/once/off/ removeListener/listenerCount/… but not prependListener/prependOnceListener. The codegen native table for net Socket (crates/perry-codegen/src/lower_call/native_table/net_events.rs:220, :499-590) has no prepend entries either,
although the events and stream tables do (:1468, :1633). Fix is inferred: register the listener at the front, as js_ext_net_socket_on does at the back.
The silent undefined for unknown methods on a native socket handle (instead of a TypeError) is what turned this into a hang (inferred: the dispatch falls through to an undefined property read).
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,prependListenerandprependOnceListenerread asundefined, but calling them does not throw. The call returnsundefined, registers nothing, and the socket's'data'never reaches the listener.Reproduction
No dependencies. A local echo server is part of the script.
main.tsExpected (Node 26.5.1)
Actual (Perry)
Same output with and without
PERRY_NO_AUTO_OPTIMIZE=1:The static form
sock.prependListener('data', fn)behaves the same way. Calling a method that does not exist at all, such assock.noSuchMethod('x'), also returnsundefined. Node throwsTypeError: sock.noSuchMethod is not a function. So a missing socket method shows up as a hang, not as an error.Impact
built/DataHandler.js:25attaches the RESP parser withredis.stream.prependListener("data", (data) => parser.execute(data))and then callsredis.stream.resume(). Under Perry the parsernever gets a byte. The client connects and writes
CLIENT SETINFO(the server receives it), but no reply ever resolves, soreadynever fires and every command promise hangs.built/DataHandler.js:24.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.
prependListeneron a socket is likely affected (inferred).Notes
(1)
import IORedis from 'ioredis'instead ofimport Redis from 'ioredis', becausenew Redis(...)is still routed to the native binding even withcompilePackages;(2)
built/connectors/StandaloneConnector.js:54(0, net_1.createConnection)(…)returnsundefinedwhennetis used as a module value (node:netused as a value is inert:connect()/createConnection()return undefined,newon aSocketvalue is not a socket,isIP()returns undefined #10429);(3) the socket has no
writableproperty, which deadlocks the offline queue (net.Socket has nowritable/readable/_writableState, andreadyState/connecting/pendingare undefined on an untyped receiver #10465).These workarounds were applied as a
globalThiswrapper around a statically importednet.createConnection, with awritablegetter defined on the returned socket.crates/perry-ext-net/src/dispatch.rs:154(socket_method_name) listson/addListener/once/off/removeListener/listenerCount/… but notprependListener/prependOnceListener. The codegen native table fornetSocket(crates/perry-codegen/src/lower_call/native_table/net_events.rs:220,:499-590) has no prepend entries either,although the
eventsandstreamtables do (:1468,:1633). Fix is inferred: register the listener at the front, asjs_ext_net_socket_ondoes at the back.undefinedfor unknown methods on a native socket handle (instead of aTypeError) is what turned this into a hang (inferred: the dispatch falls through to an undefined property read).listenerCount,eventNames,once,prependListeneretc. return wrong values; static helpers broken #850 (closed; same no-op forEventEmitter.prependListenerinnode:events), node:net: Socket and Server methods read as non-callable values #3397 (closed; net Socket methods read as non-callable).