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
fd://3 socket activation: not verified to be listening, and sd_listen_fds env contract unchecked in all three #29
Partially resolved (2026-09-28) by a40a07f (#41, carrying #35's work). The socket family is now checked; "is it listening" and the sd_listen_fds env contract are not. Scope narrowed below.
Summary
With --listen-socket=fd://3, the Rust implementation adopts fd 3 via from_raw_fd without verifying it is actually a listening AF_UNIX socket. Go's net.FileListener (go/main.go:78) rejects non-socket fds; Rust only surfaces the problem later as io::Error from set_nonblocking/accept (mitigated by the accept-error backoff added in #27 — no busy loop, but also no clear early failure).
Neither implementation checks the LISTEN_PID/LISTEN_FDS environment variables from the sd_listen_fds convention, so this is parity-plus hardening.
Affected implementation(s)
Rust — family check done; SO_ACCEPTCONN and LISTEN_FDS outstanding
Go — family check done; LISTEN_PID/LISTEN_FDS outstanding
TypeScript — family check done; LISTEN_PID/LISTEN_FDS outstanding
What has landed
unix_listener_from_raw_fd (rs/src/main.rs) now calls local_addr() immediately after from_raw_fd and rejects the fd if it is not AF_UNIX:
fd 3 is not a Unix socket (...): set ListenStream to a filesystem path in the .socket unit
Covered by test_unix_listener_from_raw_fd_rejects_tcp_socket. Go got the equivalent guard (listenerFromFile rejects a non-*net.UnixListener), and TypeScript checks server.address() is a string. So the original cross-language gap — a unit with ListenStream=127.0.0.1:2375 silently reinstating a TCP listener — is closed in all three.
What remains
Not verified to be listening. An AF_UNIX socket that was never listen(2)ed still passes. getsockopt(SOL_SOCKET, SO_ACCEPTCONN) would catch it. Currently degrades to accept errors at runtime (rate-limited by the backoff from fix: bind Unix socket listener in Rust for --listen-socket #27, so no busy loop, but no clear startup failure either).
sd_listen_fds contract unchecked in all three. No implementation validates LISTEN_PID == getpid() or LISTEN_FDS >= 1. Without the LISTEN_PID check the process will happily adopt an inherited fd 3 that systemd never intended for it.
No end-to-end test of the fd://3 dispatch path. Only the fd-wrapping helper is unit-tested; a subprocess test that dup2s a real listener onto fd 3 would cover the actual branch.
Expected behavior
--listen-socket=fd://3 with an fd that is not a listening AF_UNIX socket should fail fast at startup with a clear error, not degrade into accept errors at runtime.
Suggested fix
The fstat/S_IFSOCK step is effectively covered by the family check already in place. Remaining:
getsockopt(SOL_SOCKET, SO_ACCEPTCONN) to confirm the fd is listening
validate LISTEN_PID == getpid() and LISTEN_FDS >= 1 per the sd_listen_fds convention — in all three, for parity
a subprocess test that dup2s a real listener onto fd 3 and exercises the fd://3 dispatch path end-to-end
Context
Split out from the review follow-ups on PR #27. Related: #25, #28 (both now closed).
Summary
With
--listen-socket=fd://3, the Rust implementation adopts fd 3 viafrom_raw_fdwithout verifying it is actually a listening AF_UNIX socket. Go'snet.FileListener(go/main.go:78) rejects non-socket fds; Rust only surfaces the problem later asio::Errorfromset_nonblocking/accept(mitigated by the accept-error backoff added in #27 — no busy loop, but also no clear early failure).Neither implementation checks the
LISTEN_PID/LISTEN_FDSenvironment variables from thesd_listen_fdsconvention, so this is parity-plus hardening.Affected implementation(s)
SO_ACCEPTCONNandLISTEN_FDSoutstandingLISTEN_PID/LISTEN_FDSoutstandingLISTEN_PID/LISTEN_FDSoutstandingWhat has landed
unix_listener_from_raw_fd(rs/src/main.rs) now callslocal_addr()immediately afterfrom_raw_fdand rejects the fd if it is not AF_UNIX:Covered by
test_unix_listener_from_raw_fd_rejects_tcp_socket. Go got the equivalent guard (listenerFromFilerejects a non-*net.UnixListener), and TypeScript checksserver.address()is a string. So the original cross-language gap — a unit withListenStream=127.0.0.1:2375silently reinstating a TCP listener — is closed in all three.What remains
listen(2)ed still passes.getsockopt(SOL_SOCKET, SO_ACCEPTCONN)would catch it. Currently degrades to accept errors at runtime (rate-limited by the backoff from fix: bind Unix socket listener in Rust for --listen-socket #27, so no busy loop, but no clear startup failure either).sd_listen_fdscontract unchecked in all three. No implementation validatesLISTEN_PID == getpid()orLISTEN_FDS >= 1. Without theLISTEN_PIDcheck the process will happily adopt an inherited fd 3 that systemd never intended for it.fd://3dispatch path. Only the fd-wrapping helper is unit-tested; a subprocess test thatdup2s a real listener onto fd 3 would cover the actual branch.Expected behavior
--listen-socket=fd://3with an fd that is not a listening AF_UNIX socket should fail fast at startup with a clear error, not degrade into accept errors at runtime.Suggested fix
The
fstat/S_IFSOCKstep is effectively covered by the family check already in place. Remaining:getsockopt(SOL_SOCKET, SO_ACCEPTCONN)to confirm the fd is listeningLISTEN_PID == getpid()andLISTEN_FDS >= 1per thesd_listen_fdsconvention — in all three, for paritydup2s a real listener onto fd 3 and exercises thefd://3dispatch path end-to-endContext
Split out from the review follow-ups on PR #27. Related: #25, #28 (both now closed).