@@ -897,18 +897,74 @@ impl Hasher for HashToDigest<'_> {
897897 }
898898}
899899
900- /// Pipe `cmd`'s stdio to `/dev/null`, unless a specific env var is set.
900+ /// Close file descriptors inherited from the client (fd >= 3), releasing the
901+ /// build pipes whose lingering write-ends cause mozilla/sccache#1011.
902+ ///
903+ /// Must only be called immediately after daemonizing (see `daemonize`).
904+ #[ cfg( not( windows) ) ]
905+ fn close_client_inherited_fds ( ) {
906+ use std:: env;
907+
908+ // Escape hatch: allow disabling the fd sweep for debugging.
909+ if env:: var ( "SCCACHE_NO_FD_HYGIENE" ) . ok ( ) . as_deref ( ) == Some ( "1" ) {
910+ return ;
911+ }
912+
913+ // On macOS/BSD close_fds closes each descriptor directly (async-signal-safe);
914+ // we intentionally close rather than set-CLOEXEC because no exec follows and
915+ // the inherited pipe write-ends must actually be released to fix #1011.
916+ //
917+ // SAFETY: we are immediately after daemonix's fork, so exactly one thread
918+ // exists; no live Rust object owns an fd >= 3 here -- daemonix has already
919+ // dup2'd the log (or /dev/null) onto stderr and dropped the original File
920+ // plus closed its /dev/null fd -- and the listening socket and
921+ // SCCACHE_STARTUP_NOTIFY connection are opened later. Closing 3.. only
922+ // releases fds inherited from the client (ninja jobserver / cmake|tee pipes).
923+ unsafe {
924+ close_fds:: close_open_fds ( 3 , & [ ] ) ;
925+ }
926+ }
927+
928+ /// Daemonize the current process, optionally redirecting stderr to `log_file`.
929+ ///
930+ /// If `log_file` is `Some`, the daemon's stderr is redirected to that file;
931+ /// otherwise stderr goes to `/dev/null` (the pre-existing default).
932+ ///
933+ /// When `close_inherited_fds` is true, all file descriptors >= 3 are closed
934+ /// immediately after daemonizing (before the server binds its socket). The
935+ /// cache server is lazily fork+exec'd by a compile-job client and inherits the
936+ /// client's open fds -- e.g. ninja's jobserver pipe or a `cmake ... | tee`
937+ /// stdout pipe. Because the long-lived daemon keeps those write-ends open, the
938+ /// writer never observes EOF, producing a 0%-CPU deadlock
939+ /// (mozilla/sccache#1011). Callers that own live fds at daemonize time (e.g.
940+ /// sccache-dist, which has already built a reqwest client owning runtime/epoll
941+ /// fds) must pass `false`, since blindly closing those fds would be unsound.
942+ ///
943+ /// When `SCCACHE_NO_DAEMON=1` the process does not daemonize and stays a
944+ /// foreground child attached to the client, so inherited fds are deliberately
945+ /// NOT closed (the #1011 hang is then only theoretically reachable in that
946+ /// debug mode). Setting `SCCACHE_NO_FD_HYGIENE=1` is an escape hatch that skips
947+ /// the fd sweep even when daemonizing.
901948#[ cfg( not( windows) ) ]
902- pub fn daemonize ( ) -> Result < ( ) > {
949+ pub fn daemonize ( log_file : Option < File > , close_inherited_fds : bool ) -> Result < ( ) > {
903950 use crate :: jobserver:: discard_inherited_jobserver;
904- use daemonix:: Daemonize ;
951+ use daemonix:: { Daemonize , Stdio } ;
905952 use std:: env;
906953 use std:: mem;
907954
908955 match env:: var ( "SCCACHE_NO_DAEMON" ) {
909956 Ok ( ref val) if val == "1" => { }
910957 _ => {
911- Daemonize :: new ( ) . start ( ) . context ( "failed to daemonize" ) ?;
958+ let stderr = log_file
959+ . map ( |f| Stdio :: from ( f. into_file ( ) ) )
960+ . unwrap_or_else ( Stdio :: devnull) ;
961+ Daemonize :: new ( )
962+ . stderr ( stderr)
963+ . start ( )
964+ . context ( "failed to daemonize" ) ?;
965+ if close_inherited_fds {
966+ close_client_inherited_fds ( ) ;
967+ }
912968 }
913969 }
914970
@@ -984,12 +1040,27 @@ pub fn daemonize() -> Result<()> {
9841040 }
9851041}
9861042
987- /// This is a no-op on Windows.
1043+ /// There is no daemon to fork on Windows, so this only redirects stderr to
1044+ /// `log_file` (if any). No inherited-fd closing is needed on Windows, so
1045+ /// `_close_inherited_fds` is unused.
9881046#[ cfg( windows) ]
989- pub fn daemonize ( ) -> Result < ( ) > {
1047+ pub fn daemonize ( log_file : Option < File > , _close_inherited_fds : bool ) -> Result < ( ) > {
1048+ if let Some ( log_file) = log_file {
1049+ redirect_stderr ( log_file) ;
1050+ }
9901051 Ok ( ( ) )
9911052}
9921053
1054+ #[ cfg( windows) ]
1055+ fn redirect_stderr ( f : File ) {
1056+ use std:: os:: windows:: io:: IntoRawHandle ;
1057+ use windows_sys:: Win32 :: System :: Console :: { STD_ERROR_HANDLE , SetStdHandle } ;
1058+ // Ignore errors here.
1059+ unsafe {
1060+ SetStdHandle ( STD_ERROR_HANDLE , f. into_file ( ) . into_raw_handle ( ) as _ ) ;
1061+ }
1062+ }
1063+
9931064/// Disable connection pool to avoid broken connection between runtime
9941065///
9951066/// # TODO
@@ -1980,4 +2051,91 @@ mod tests {
19802051 . unwrap ( ) ;
19812052 assert_ne ! ( h1, h2) ;
19822053 }
2054+
2055+ /// Regression test for mozilla/sccache#1011: the daemon must close file
2056+ /// descriptors inherited from the client (fd >= 3) so the write-ends of
2057+ /// pipes like ninja's jobserver or `cmake ... | tee` see EOF.
2058+ ///
2059+ /// We fork so the fd-closing sweep does not affect the test harness's own
2060+ /// descriptors. The child performs only async-signal-safe work: the sweep
2061+ /// itself, an `fcntl(F_GETFD)` probe on the inherited write-end (which must
2062+ /// return -1 with errno EBADF once closed), and a probe on fd 2 (stderr,
2063+ /// which must survive the sweep -- guarding an off-by-one in `minfd`).
2064+ #[ cfg( unix) ]
2065+ #[ test]
2066+ fn test_daemonize_closes_inherited_fds ( ) {
2067+ // Create a pipe; both ends are fds >= 3 in the test process.
2068+ let mut fds = [ 0 as libc:: c_int ; 2 ] ;
2069+ // SAFETY: `fds` is a valid two-element array for libc::pipe.
2070+ let rc = unsafe { libc:: pipe ( fds. as_mut_ptr ( ) ) } ;
2071+ assert_eq ! ( rc, 0 , "pipe() failed" ) ;
2072+ let ( read_fd, write_fd) = ( fds[ 0 ] , fds[ 1 ] ) ;
2073+ assert ! (
2074+ read_fd >= 3 && write_fd >= 3 ,
2075+ "expected inherited-style fds"
2076+ ) ;
2077+
2078+ // SAFETY: fork() in a test; the child only does async-signal-safe work.
2079+ let pid = unsafe { libc:: fork ( ) } ;
2080+ assert ! ( pid >= 0 , "fork() failed" ) ;
2081+
2082+ if pid == 0 {
2083+ // Child: close all fds >= 3 (as daemonize does), then verify the
2084+ // inherited write-end is closed (fcntl -> -1 with errno EBADF) and
2085+ // that stderr (fd 2) survives the sweep. Async-signal-safe only:
2086+ // no allocation, no env reads, no Rust runtime.
2087+ // SAFETY: async-signal-safe operations only.
2088+ unsafe {
2089+ close_fds:: close_open_fds ( 3 , & [ ] ) ;
2090+
2091+ // fd 2 (stderr) must survive the sweep (guards a minfd off-by-one).
2092+ if libc:: fcntl ( 2 , libc:: F_GETFD ) < 0 {
2093+ libc:: _exit ( 2 ) ;
2094+ }
2095+
2096+ // The inherited write-end must now be closed: fcntl fails with EBADF.
2097+ let ret = libc:: fcntl ( write_fd, libc:: F_GETFD ) ;
2098+ let errno = {
2099+ #[ cfg( any( target_os = "linux" , target_os = "android" ) ) ]
2100+ {
2101+ * libc:: __errno_location ( )
2102+ }
2103+ #[ cfg( not( any( target_os = "linux" , target_os = "android" ) ) ) ]
2104+ {
2105+ * libc:: __error ( )
2106+ }
2107+ } ;
2108+ libc:: _exit ( if ret == -1 && errno == libc:: EBADF {
2109+ 0
2110+ } else {
2111+ 1
2112+ } ) ;
2113+ }
2114+ }
2115+
2116+ // Parent: reap the child and check its exit status.
2117+ let mut status: libc:: c_int = 0 ;
2118+ // SAFETY: valid pid and status pointer.
2119+ let waited = unsafe { libc:: waitpid ( pid, & mut status, 0 ) } ;
2120+ assert_eq ! ( waited, pid, "waitpid() failed" ) ;
2121+
2122+ // Close our own copies of the pipe fds.
2123+ // SAFETY: fds are valid and owned by this process.
2124+ unsafe {
2125+ libc:: close ( read_fd) ;
2126+ libc:: close ( write_fd) ;
2127+ }
2128+
2129+ assert ! (
2130+ libc:: WIFEXITED ( status) ,
2131+ "child did not exit normally (status={status})"
2132+ ) ;
2133+ // Exit codes: 0 = success; 1 = inherited fd not closed (or wrong errno);
2134+ // 2 = fd 2 (stderr) was incorrectly closed by the sweep.
2135+ assert_eq ! (
2136+ libc:: WEXITSTATUS ( status) ,
2137+ 0 ,
2138+ "fd hygiene sweep behaved incorrectly (child exit code)"
2139+ ) ;
2140+ }
19832141}
0 commit comments