Skip to content

TypeScript fd://3 socket activation is broken: every valid socket is rejected at startup #44

Description

@abienkowski

Summary

--listen-socket=fd://3 never works in the TypeScript implementation. A correct, listening AF_UNIX socket passed on fd 3 is rejected and the process exits 1:

fd 3 is not a Unix socket: set ListenStream to a filesystem path in the .socket unit

Socket activation is therefore entirely non-functional in TypeScript. Go and Rust accept the same fd and serve on it.

Cause

ts/src/index.ts guards the adopted fd with:

if (typeof server.address() !== "string") {
  console.error(`fd ${fd} is not a Unix socket: ...`);
  process.exit(1);
}

server.address() returns a string for a server that bound a path itself, but for a socket adopted via listen({ fd }) it returns null — so the condition is true for every fd, valid or not.

Measured on Node 22:

fd 3 contents server.address()
listening AF_UNIX socket null
listening TCP socket {"address":"127.0.0.1","family":"IPv4","port":55281}

So null is the success case, and the TCP case is distinguished by being an object with a port — not by being a non-string.

Reproduction

Pass a real listening Unix socket on fd 3:

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(path); s.listen(5)
os.dup2(s.fileno(), 3, inheritable=True)
subprocess.run(["node", "ts/dist/index.js", "--listen-socket=fd://3", ...], pass_fds=(3,))
GO   listening=True  -> serving
RUST listening=True  -> serving
TS   listening=True  -> rc=1  "fd 3 is not a Unix socket"

Affected implementation(s)

  • Go
  • Rust
  • TypeScript

Expected behavior

A listening AF_UNIX socket on fd 3 is adopted and served, matching Go and Rust. A TCP socket on fd 3 is still rejected — that guard is the reason the check exists and must be preserved.

Suggested fix

Invert the test to match what Node actually returns:

const addr = server.address();
if (addr !== null && typeof addr === "object" && "port" in addr) {
  // TCP socket on fd 3 — reject
}

Context

Introduced in #38 (PR #38's content landed on main via a40a07f), where the check was added to stop a unit with ListenStream=127.0.0.1:2375 silently reinstating a TCP listener. The intent was right; the discriminator was wrong, and nothing exercised the fd://3 path end to end — which is precisely the test gap #29 point 3 calls out.

Found while investigating #29.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: P1Added to issues and PRs relating to a high severity bugs.Type: BugAdded to issues and PRs if they are addressing a bug

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions