Skip to content

net.Socket has no writable/readable/_writableState, and readyState/connecting/pending are undefined on an untyped receiver #10465

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. A client net.Socket has none of the stream state properties
(writable, readable, writableEnded, readableEnded, _writableState, _readableState). When the socket is
read through an untyped value, which is how every JS driver holds it (this.stream), the #2549 state properties
(readyState, connecting, pending) are undefined too, and destroyed stays false after 'close'.

Reproduction

server.mjs (plain Node echo server, run separately):

import net from "node:net";
net.createServer((s) => { s.on("data", (d) => s.write(d)); s.on("end", () => s.end()); })
  .listen(Number(process.env.PORT), "127.0.0.1", () => console.log("READY"));

main.ts:

import net from "node:net";
const keys = ["writable", "readable", "readyState", "connecting", "pending", "destroyed", "writableEnded", "readableEnded"];
function show(when: string, s: any) {
  console.log(when.padEnd(12), keys.map((k) => `${k}=${s[k]}`).join(" "), `_writableState=${typeof s._writableState}`, `_readableState=${typeof s._readableState}`);
}
const fresh = new net.Socket();
show("new Socket", fresh);
const s = net.connect(Number(process.env.PORT), "127.0.0.1");
show("connecting", s);
s.on("connect", () => {
  show("connected", s);
  s.write("hi");
});
s.on("data", () => { show("data", s); s.end(); show("after end()", s); });
s.on("end", () => show("'end'", s));
s.on("close", () => show("'close'", s));
PORT=18123 node server.mjs &
PORT=18123 node main.ts
perry compile main.ts -o main && PORT=18123 ./main

Expected (Node 26.5.1)

new Socket   writable=true readable=true readyState=open connecting=false pending=true destroyed=false writableEnded=false readableEnded=false _writableState=object _readableState=object
connecting   writable=true readable=true readyState=opening connecting=true pending=true destroyed=false writableEnded=false readableEnded=false _writableState=object _readableState=object
connected    writable=true readable=true readyState=open connecting=false pending=false destroyed=false writableEnded=false readableEnded=false _writableState=object _readableState=object
data         writable=true readable=true readyState=open connecting=false pending=false destroyed=false writableEnded=false readableEnded=false _writableState=object _readableState=object
after end()  writable=false readable=true readyState=readOnly connecting=false pending=false destroyed=false writableEnded=true readableEnded=false _writableState=object _readableState=object
'end'        writable=false readable=false readyState=closed connecting=false pending=false destroyed=false writableEnded=true readableEnded=true _writableState=object _readableState=object
'close'      writable=false readable=false readyState=closed connecting=false pending=true destroyed=true writableEnded=true readableEnded=true _writableState=object _readableState=object

Actual (Perry)

Same output with and without PERRY_NO_AUTO_OPTIMIZE=1:

new Socket   writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined
connecting   writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined
connected    writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined
data         writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined
after end()  writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined
'end'        writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined
'close'      writable=undefined readable=undefined readyState=undefined connecting=undefined pending=undefined destroyed=false writableEnded=undefined readableEnded=undefined _writableState=undefined _readableState=undefined

Impact

Network drivers check these properties before every write, so a falsy undefined makes them drop or queue writes
forever. Nothing throws.

  • pg 8.22.0: Connection._send (lib/connection.js:165) is if (!this.stream.writable) return false. Every
    protocol message, including the startup message, is silently dropped, and the audit's client hung until its
    connection timeout. With _send patched, two other blockers patched and trust auth, the audit's pg run matched
    Node. lib/connection.js:191 and :212 have the same check.
  • ioredis 5.11.1 (built/Redis.js:365) and iovalkey 0.4.0 (built/Redis.js:354): sendCommand treats
    !this.stream.writable as not writable and pushes every command into the offline queue. In the audit the commands
    stayed queued and the client hung after connecting.
  • @redis/client (dist/lib/client/socket.js:183, :298) checks readable/writable/destroyed in the same way
    (inferred, not run).
  • Likely also affects anything that checks socket.writable or socket._writableState before writing: mysql2,
    mongodb, SMTP/AMQP/MQTT clients.

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