fix(javascript): emit no socket for a literal out-of-range port (#8137)

* 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.
This commit is contained in:
吴佳浩Alben authored and GitHub committed 2026-10-31 01:23:50 +08:00
1 parent 96606f5114
commit e5c46dff03
2 files changed
+116 -1

No files matched your search

@@ -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 }]);
});
@@ -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<AcquisitionReference, "specifier">,
): boolean => candidate.specifier === "net" || candidate.specifier === "tls";