Harden fd inheritance, socket lifecycle, and control-plane I/O - #4
Merged
Merged
Conversation
Validate inherited listener descriptors (LISTEN_PID, socket-ness, family, listening state) and normalize FD_CLOEXEC/O_NONBLOCK on adoption; keep the accept loop alive across transient accept(2) failures; bind control and trigger sockets 0600 via bind-then-rename with peer-uid authentication; make control reads resumable across receive timeouts and writes bounded and SIGPIPE-safe; treat EPERM from kill(pid,0) as alive; validate sun_path length. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…upported macOS fails getsockopt(SO_ACCEPTCONN) for these sockets, so every inherited listener was rejected there. Also bumps anyhow/spin to clear cargo-deny advisories. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the fd/socket findings from the Linux/unix scan. Four of them are the interesting ones; the rest are small and readable from the diff.
Inherited descriptors were adopted on faith.
LISTEN_FDSis systemd's variable, so FDs 3..3+n can arrive from something that is not our supervisor, andFromRawFdvalidates nothing — a name collision meant wrapping (and, on drop, closing) an unrelated descriptor.role.rsnow honorsLISTEN_PIDwhen present,fstats every slot, and family/listening-checks it at the typed accessor:We can't set
LISTEN_PIDon our own children (the pid isn't known until afterfork, andCommand's env is materialized beforepre_exec), so its absence is not treated as suspicious; successor identity is still checked via theHellopid. Adopted FDs also getFD_CLOEXECre-armed (the parent'sdup2cleared it — a leaked control-socket dup holds the supervisor's EOF open and hides the primitive's death) andO_NONBLOCKcleared (it lives on the open file description, so an async-runtime parent would hand the child a listener whose firstaccept()isEAGAIN).The accept loop exited on transient errors.
ECONNABORTED(peer went away before we accepted) or a moment ofEMFILEpermanently stopped the incumbent from answering the control socket — it keeps serving traffic but can never be handed off again, which is exactly the outage this crate exists to prevent. Now: retryEINTR/ECONNABORTED/EPROTO/EPERM, back off 5 ms → 1 s onEMFILE/ENFILE/ENOBUFS/ENOMEM/EAGAIN, fatal only on listener-invalidating errors.Anyone local could drive a handoff. Control + trigger sockets got the ambient umask and no peer check, so a drain+seal was available to any uid on the box. New
sock.rsbinds a staging name, chmods0600, thenrename(2)s it over the path — no world-writable window, and no missing-path window either (the old unlink-then-bind gave connecting clientsENOENTand let a racer steal the name). Every accepted peer's uid comes fromSO_PEERCRED/getpeereidand must be our euid, root, or configured. The reference supervisor additionally replaces "exec whatever path the client sent" with anallowed_binariesallowlist, and bounds trigger clients (10 s I/O timeout, 4 KiB cap, per-client error isolation so one stalled client can't kill the loop).Frame reads desynchronized on timeout. Every protocol read carries
SO_RCVTIMEO, andread_exactdiscards what it consumed on failure — so a frame straddling the timeout left the stream mid-frame and every subsequent frame misparsed.FrameAccumulatorowns the partial bytes; a timeout is a suspension, not a loss:Writes go out via
MSG_NOSIGNAL/SO_NOSIGPIPE(a dead peer must yieldEPIPE, not kill the process — Rust's defaultSIGPIPEmasking is a property of the embedding binary, not of this library) and carrySO_SNDTIMEO, since a peer that stops reading was the one place in the handoff bounded by neither the liveness clock nor the deadline.Smaller ones:
kill(pid, 0)returningEPERMmeans the pid exists under another uid, and was being read as dead (onlyESRCHcounts now); pidfile temp names are writer-specific;sun_pathlength is validated up front instead of surfacing an opaqueEINVALmid-handoff;lock.rsdocuments thatflockneeds a local filesystem.One portability wrinkle surfaced in CI: macOS fails
getsockopt(SO_ACCEPTCONN)outright, which rejected every inherited listener there, so off-Linuxfd_is_listeningfalls back to a listener's observable shape (aSOCK_STREAMwith no peer pergetpeername). Lockfile also bumpsanyhow/spinto clear unrelatedcargo-denyadvisories.Dropped nothing — every finding held up.
ARCHITECTURE.mdand the README config example are updated in the same commit.Testing
CI is green on all 8 checks (including macOS and the FreeBSD cross-compile). Locally:
cargo fmt --check,cargo clippy --workspace --all-targets -D warnings, debug + release builds, and the full workspace suite (crash matrix, wire races, stress) are green. 15 new unit tests cover the added behavior: foreignLISTEN_PID, non-socket/wrong-family/connected-socket rejection, flag normalization idempotency, transient-accept classification,0600bind + rename with no missing-path window, overlongsun_path, peer-uid lookup,EPERM-is-alive, and frame resumption across timeout boundaries.No end-to-end swap has been exercised against a real daemon yet — happy to run one.
Link to Devin session: https://app.devin.ai/sessions/288b845379894277bddeaa82ba171e95
Requested by: @jaredLunde