Repository navigation
fd://3 socket activation: not verified to be listening, and sd_listen_fds env contract unchecked in all three #29
Description
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_fdsenv 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_ACCEPTCONNandLISTEN_FDSoutstanding - Go — family check done;
LISTEN_PID/LISTEN_FDSoutstanding - TypeScript — family check done;
LISTEN_PID/LISTEN_FDSoutstanding
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_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.- No end-to-end test of the
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://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()andLISTEN_FDS >= 1per thesd_listen_fdsconvention — in all three, for parity - a subprocess test that
dup2s a real listener onto fd 3 and exercises thefd://3dispatch path end-to-end
Context
Split out from the review follow-ups on PR #27. Related: #25, #28 (both now closed).
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
[-]Rust fd://3 socket activation adopts the fd blindly — no validation it is a listening socket[/-][+]fd://3 socket activation: not verified to be listening, and sd_listen_fds env contract unchecked in all three[/+]