44//!
55//! - File descriptor passing
66//! - Changing to a file-descriptor relative directory
7+ //! - Systemd socket activation fd passing
78
89use cap_std:: fs:: Dir ;
910use cap_std:: io_lifetimes;
1011use cap_tempfile:: cap_std;
1112use io_lifetimes:: OwnedFd ;
1213use rustix:: fd:: { AsFd , FromRawFd , IntoRawFd } ;
1314use rustix:: io:: FdFlags ;
15+ use std:: ffi:: CString ;
1416use std:: os:: fd:: AsRawFd ;
1517use std:: os:: unix:: process:: CommandExt ;
1618use std:: sync:: Arc ;
1719
20+ /// The file descriptor number at which systemd passes the first socket.
21+ /// See `sd_listen_fds(3)`.
22+ const SD_LISTEN_FDS_START : i32 = 3 ;
23+
1824/// Extension trait for [`std::process::Command`].
1925///
2026/// [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
@@ -25,6 +31,24 @@ pub trait CapStdExtCommandExt {
2531 /// Use the given directory as the current working directory for the process.
2632 fn cwd_dir ( & mut self , dir : Dir ) -> & mut Self ;
2733
34+ /// Set up the [systemd socket activation][sd_listen_fds] environment for
35+ /// the child process.
36+ ///
37+ /// Each `(fd, name)` pair is placed at consecutive file descriptor numbers
38+ /// starting from `SD_LISTEN_FDS_START` (3), and the environment variables
39+ /// `LISTEN_PID`, `LISTEN_FDS`, and `LISTEN_FDNAMES` are set accordingly
40+ /// inside the child (via `pre_exec`).
41+ ///
42+ /// Note that `LISTEN_PID` must reflect the child's actual PID, so it
43+ /// cannot be set with [`Command::env`] — it is written directly with
44+ /// `setenv(3)` in the forked child before exec.
45+ ///
46+ /// [sd_listen_fds]: https://www.freedesktop.org/software/systemd/man/latest/sd_listen_fds.html
47+ fn pass_systemd_fds < ' a > (
48+ & mut self ,
49+ fds : impl IntoIterator < Item = ( Arc < OwnedFd > , & ' a str ) > ,
50+ ) -> & mut Self ;
51+
2852 /// On Linux, arrange for [`SIGTERM`] to be delivered to the child if the
2953 /// parent *thread* exits. This helps avoid leaking child processes if
3054 /// the parent crashes for example.
@@ -39,6 +63,22 @@ pub trait CapStdExtCommandExt {
3963 fn lifecycle_bind_to_parent_thread ( & mut self ) -> & mut Self ;
4064}
4165
66+ /// Wrapper around `libc::setenv` that checks the return value.
67+ ///
68+ /// # Safety
69+ ///
70+ /// Must only be called in a single-threaded context (e.g. after `fork()`
71+ /// and before `exec()`).
72+ #[ allow( unsafe_code) ]
73+ unsafe fn check_setenv ( key : * const i8 , val : * const i8 ) -> std:: io:: Result < ( ) > {
74+ // SAFETY: Caller guarantees we are in a single-threaded context
75+ // with valid nul-terminated C strings.
76+ if unsafe { libc:: setenv ( key, val, 1 ) } != 0 {
77+ return Err ( std:: io:: Error :: last_os_error ( ) ) ;
78+ }
79+ Ok ( ( ) )
80+ }
81+
4282#[ allow( unsafe_code) ]
4383impl CapStdExtCommandExt for std:: process:: Command {
4484 fn take_fd_n ( & mut self , fd : Arc < OwnedFd > , target : i32 ) -> & mut Self {
@@ -62,6 +102,49 @@ impl CapStdExtCommandExt for std::process::Command {
62102 self
63103 }
64104
105+ fn pass_systemd_fds < ' a > (
106+ & mut self ,
107+ fds : impl IntoIterator < Item = ( Arc < OwnedFd > , & ' a str ) > ,
108+ ) -> & mut Self {
109+ let mut n_fds: i32 = 0 ;
110+ let mut names = Vec :: new ( ) ;
111+ for ( fd, name) in fds {
112+ assert ! (
113+ !name. contains( '\0' ) && !name. contains( ':' ) ,
114+ "systemd fd name must not contain NUL or ':'"
115+ ) ;
116+ let target = SD_LISTEN_FDS_START
117+ . checked_add ( n_fds)
118+ . expect ( "too many fds" ) ;
119+ self . take_fd_n ( fd, target) ;
120+ names. push ( name. to_owned ( ) ) ;
121+ n_fds = n_fds. checked_add ( 1 ) . expect ( "too many fds" ) ;
122+ }
123+
124+ // Build the env values now (owned), then move them into the pre_exec
125+ // closure. We cannot use Command::env() because it causes Rust's
126+ // Command to build an envp array that replaces environ, which would
127+ // clobber the setenv calls we make in pre_exec for LISTEN_PID.
128+ let fd_count = CString :: new ( n_fds. to_string ( ) ) . unwrap ( ) ;
129+ // SAFETY: We validated that no name contains NUL above.
130+ let fd_names = CString :: new ( names. join ( ":" ) ) . unwrap ( ) ;
131+
132+ unsafe {
133+ self . pre_exec ( move || {
134+ let pid = rustix:: process:: getpid ( ) ;
135+ let pid_dec = rustix:: path:: DecInt :: new ( pid. as_raw_nonzero ( ) . get ( ) ) ;
136+ // SAFETY: After fork() and before exec(), the child is
137+ // single-threaded, so setenv (which is not thread-safe) is
138+ // safe to call here.
139+ check_setenv ( c"LISTEN_PID" . as_ptr ( ) , pid_dec. as_c_str ( ) . as_ptr ( ) ) ?;
140+ check_setenv ( c"LISTEN_FDS" . as_ptr ( ) , fd_count. as_ptr ( ) ) ?;
141+ check_setenv ( c"LISTEN_FDNAMES" . as_ptr ( ) , fd_names. as_ptr ( ) ) ?;
142+ Ok ( ( ) )
143+ } ) ;
144+ }
145+ self
146+ }
147+
65148 fn cwd_dir ( & mut self , dir : Dir ) -> & mut Self {
66149 unsafe {
67150 self . pre_exec ( move || {
0 commit comments