From e5c46dff034fef432e772f669bc309f512a6b186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=BD=B3=E6=B5=A9Alben?= <35291372+XiaoBinGan@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:23:50 +0800 Subject: [PATCH] fix(javascript): emit no socket for a literal out-of-range port (#8137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(javascript): emit no socket for a literal out-of-range port `net` and `tls` validate a numeric port before creating a socket, so a literal port Node rejects throws synchronously and no socket exists — but the analysis reported an `acquire` operation. Measured on Node 24.22.1: net.connect(0) / 80 / 65535 -> Socket created net.connect(65536) -> THREW ERR_SOCKET_BAD_PORT net.connect(-1) / 1.5 -> THREW ERR_SOCKET_BAD_PORT net.connect(NaN) / Infinity -> THREW ERR_SOCKET_BAD_PORT net.connect(1e400) -> THREW ERR_SOCKET_BAD_PORT net.connect(0n) / true / false -> THREW ERR_INVALID_ARG_TYPE net.connect(null) -> THREW ERR_INVALID_ARG_TYPE net.connect("") -> THREW ERR_SOCKET_BAD_PORT net.connect("/tmp/x.sock") -> Socket created (Unix socket path) net.connect({ port: 80 }) -> Socket created net.connect(undefined) / {} / [] -> THREW ERR_MISSING_ARGS (undecided) --- tls mirrors net --- tls.connect(80) -> Socket created tls.connect(65536) / -1 / NaN -> THREW ERR_SOCKET_BAD_PORT A string is a Unix socket path and an object may carry a path, so only an exactly readable non-port literal is decided. `undefined` and an empty object throw `ERR_MISSING_ARGS`, which is a missing option rather than a bad port, so they stay undecided. `NaN` and `Infinity` are identifiers rather than literals and are decided only through `isUnmodifiedGlobalReference`. * chore: retrigger title and readiness checks after the title was corrected * fix(javascript): place the port check away from the fs flag check `#8125` adds an fs flag guard at the top of `collectAcquisitions` and this branch added a socket port guard in the same spot, so the two conflicted on every rebase. The port check now filters the completed candidate list from `collectJavaScriptSemanticResources` and its helpers sit at the end of the file, which cannot collide with the fs flag guard. * fix(javascript): drop the redundant in-loop port guard The caller now filters the completed candidate list, so the loop guard became dead code. Removing it also keeps this branch from editing the same lines as the fs flag guard in #8125. --- ...manticAnalysis.socket_port_literal.test.ts | 58 ++++++++++++++++++ .../javascript/javascriptSemanticResources.ts | 59 ++++++++++++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/domain/javascript/javascriptSemanticAnalysis.socket_port_literal.test.ts diff --git a/src/domain/javascript/javascriptSemanticAnalysis.socket_port_literal.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.socket_port_literal.test.ts new file mode 100644 index 0000000000..a2b1d004ec --- /dev/null +++ b/src/domain/javascript/javascriptSemanticAnalysis.socket_port_literal.test.ts @@ -0,0 +1,58 @@ +import { expect, it } from "vitest"; + +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; + +/** + * `net` and `tls` validate a numeric port before creating a socket, so a literal + * port outside `[0, 65535]`, a fractional or non-finite number, or a BigInt, + * boolean or `null` first argument throws synchronously and no socket exists. + * + * Measured on Node 24.22.1: + * + * net.connect(0) / 80 / 65535 -> Socket created + * net.connect(65536) / -1 / 1.5 -> THREW ERR_SOCKET_BAD_PORT + * net.connect(NaN) / Infinity -> THREW ERR_SOCKET_BAD_PORT + * net.connect(0n) / true / null -> THREW ERR_INVALID_ARG_TYPE + * net.connect("") -> THREW ERR_SOCKET_BAD_PORT + * net.connect("/tmp/x.sock") -> Socket created (Unix socket path) + * net.connect({ port: 80 }) -> Socket created + * net.connect(undefined) / {} / [] -> THREW ERR_MISSING_ARGS (undecided here) + */ +const socketAcquisitionsFor = (call: string) => + analyzeJavaScriptSemantics( + `import net from "node:net";\nimport tls from "node:tls";\nfunction g(p) { ${call}; }`, + ).resourceOperations.map(({ kind, method }) => ({ kind, method })); + +it.each([ + ["a port above the range", `net.connect(65536)`], + ["a negative port", `net.connect(-1)`], + ["a fractional port", `net.connect(1.5)`], + ["a NaN port", `net.connect(NaN)`], + ["an Infinity port", `net.connect(Infinity)`], + ["an overflowing numeric port", `net.connect(1e400)`], + ["a BigInt port", `net.connect(0n)`], + ["a boolean port", `net.connect(true)`], + ["a null port", `net.connect(null)`], + ["an empty path", `net.connect("")`], + ["a tls port above the range", `tls.connect(65536)`], + ["a tls negative port", `tls.connect(-1)`], + ["a tls NaN port", `tls.connect(NaN)`], + ["a createConnection port above the range", `net.createConnection(65536)`], + ["a createConnection empty path", `net.createConnection("")`], +] as const)("emits no socket acquisition for %s", (_name, call) => { + expect(socketAcquisitionsFor(call)).toEqual([]); +}); + +it.each([ + ["a zero port", `net.connect(0)`, "connect"], + ["a low port", `net.connect(80)`, "connect"], + ["the highest port", `net.connect(65535)`, "connect"], + ["a Unix socket path", `net.connect("/tmp/x.sock")`, "connect"], + ["an options object", `net.connect({ port: 80 })`, "connect"], + ["an omitted argument", `net.connect()`, "connect"], + ["an explicit undefined port", `net.connect(undefined)`, "connect"], + ["a tls port", `tls.connect(80)`, "connect"], + ["a createConnection port", `net.createConnection(80)`, "createConnection"], +] as const)("keeps the socket acquisition for %s", (_name, call, method) => { + expect(socketAcquisitionsFor(call)).toEqual([{ kind: "acquire", method }]); +}); diff --git a/src/domain/javascript/javascriptSemanticResources.ts b/src/domain/javascript/javascriptSemanticResources.ts index ec5e5252f4..d441c70d1c 100644 --- a/src/domain/javascript/javascriptSemanticResources.ts +++ b/src/domain/javascript/javascriptSemanticResources.ts @@ -64,7 +64,11 @@ export const collectJavaScriptSemanticResources = ( callableStack: [], ancestors: [], }; - const acquisitions = collectAcquisitions(program, context); + const acquisitions = collectAcquisitions(program, context).filter( + (candidate) => + !socketPortInFirstArgument(candidate) || + !rejectsSocketPortLiteral(candidate.node, context), + ); return [ ...acquisitions.map(immutableAcquisition), ...collectReleases(program, context, acquisitions), @@ -220,3 +224,56 @@ const acquisitionMethod = ( }; const resourceCandidateId = (candidate: ResourceCandidate): string => `resource:acquire:${String(candidate.node.start ?? -1)}:${String(candidate.node.end ?? -1)}`; + +/** + * `net` and `tls` validate a numeric port before creating a socket, so a + * literal port outside `[0, 65535]`, a fractional, non-finite, BigInt, boolean + * or `null` first argument throws synchronously and no socket exists. A string + * is a Unix socket path and an object may carry a path, so only an exactly + * readable non-port literal is decided. `""` is a path with no length, which + * Node also rejects. + * + * `undefined` and an empty object throw `ERR_MISSING_ARGS`, but those are a + * missing option rather than a bad port, so they stay undecided here. + */ +const rejectsSocketPortLiteral = ( + node: t.CallExpression | t.OptionalCallExpression, + context: DataEffectTraversalContext, +): boolean => { + const port = node.arguments[0]; + if (port === undefined || t.isSpreadElement(port)) return false; + const value = unwrapJavaScriptExpression(port).node; + if (t.isStringLiteral(value)) return value.value.length === 0; + if ( + t.isNullLiteral(value) || + t.isBigIntLiteral(value) || + t.isBooleanLiteral(value) + ) + return true; + if (t.isNumericLiteral(value)) + return ( + !Number.isInteger(value.value) || value.value < 0 || value.value > 65535 + ); + // `NaN` and `Infinity` are unshadowed globals, and neither is a port. + if ( + t.isIdentifier(value) && + (value.name === "NaN" || value.name === "Infinity") && + isUnmodifiedGlobalReference(value, context.state, value.name) + ) + return true; + // `-1` and `-1.5` are unary expressions over numeric literals: always out of + // range because a port is never negative. + if (t.isUnaryExpression(value)) { + const operand = unwrapJavaScriptExpression(value.argument).node; + return ( + t.isNumericLiteral(operand) && + (value.operator === "-" || !Number.isInteger(operand.value)) + ); + } + return false; +}; + +/** Only `net`/`tls` sockets take a port in argument 0. */ +const socketPortInFirstArgument = ( + candidate: Pick, +): boolean => candidate.specifier === "net" || candidate.specifier === "tls";