@@ -121,58 +121,6 @@ impl ServiceManager {
121121 Self { config }
122122 }
123123
124- /// Remove the com.apple.quarantine extended attribute from the binary.
125- ///
126- /// macOS adds this attribute to files downloaded from the internet, and Gatekeeper
127- /// may block execution of quarantined binaries. We proactively remove it after
128- /// copying the binary to prevent startup failures.
129- ///
130- /// Returns Ok(true) if quarantine was removed, Ok(false) if not quarantined,
131- /// or Err with a warning message if removal failed.
132- #[ cfg( target_os = "macos" ) ]
133- fn remove_quarantine ( & self ) -> Result < bool , String > {
134- use std:: process:: Command ;
135-
136- // First check if the binary is quarantined
137- let check = Command :: new ( "xattr" )
138- . args ( [ "-p" , "com.apple.quarantine" ] )
139- . arg ( & self . config . binary_path )
140- . output ( ) ;
141-
142- match check {
143- Ok ( o) if o. status . success ( ) => {
144- // Binary is quarantined, try to remove it
145- let remove = Command :: new ( "xattr" )
146- . args ( [ "-d" , "com.apple.quarantine" ] )
147- . arg ( & self . config . binary_path )
148- . output ( ) ;
149-
150- match remove {
151- Ok ( o) if o. status . success ( ) => {
152- info ! ( "[service] Removed quarantine attribute from binary" ) ;
153- Ok ( true )
154- }
155- Ok ( o) => {
156- let stderr = String :: from_utf8_lossy ( & o. stderr ) ;
157- Err ( format ! (
158- "Failed to remove quarantine (binary may be blocked by Gatekeeper): {}" ,
159- stderr. trim( )
160- ) )
161- }
162- Err ( e) => Err ( format ! ( "xattr command failed: {}" , e) ) ,
163- }
164- }
165- Ok ( _) => {
166- // Not quarantined (xattr -p returns non-zero when attribute doesn't exist)
167- Ok ( false )
168- }
169- Err ( e) => {
170- // xattr command itself failed - unusual but not fatal
171- Err ( format ! ( "Could not check quarantine status: {}" , e) )
172- }
173- }
174- }
175-
176124 /// Install the daemon as a system service.
177125 ///
178126 /// This copies the binary to a persistent location and creates the
@@ -187,31 +135,66 @@ impl ServiceManager {
187135 std:: fs:: create_dir_all ( parent) ?;
188136 }
189137
190- // Copy binary to persistent location
191- std:: fs:: copy ( source_binary, & self . config . binary_path ) ?;
192- info ! (
193- "[service] Installed binary to {:?}" ,
194- self . config. binary_path
195- ) ;
138+ // Atomically replace the binary (write to temp + rename) to avoid
139+ // corrupting a running daemon's mapped pages on macOS. See the
140+ // doc comment on `atomic_copy_binary` for details.
141+ self . atomic_copy_binary ( source_binary) ?;
142+
143+ // Create service configuration
144+ self . create_service_config ( ) ?;
145+
146+ info ! ( "[service] Service installed successfully" ) ;
147+ Ok ( ( ) )
148+ }
149+
150+ /// Copy a binary to `self.config.binary_path` via a temporary file and
151+ /// atomic `rename`, then set permissions and remove quarantine.
152+ ///
153+ /// A plain `std::fs::copy` truncates and rewrites the *same inode*.
154+ /// On macOS, if a `KeepAlive`-restarted daemon still has the old inode
155+ /// memory-mapped, the in-place write invalidates its code-signature
156+ /// pages. Worse, the *new* daemon inherits the same inode and can
157+ /// crash minutes later when macOS demand-pages an unloaded `__TEXT`
158+ /// page whose hash no longer matches the code directory.
159+ ///
160+ /// Writing to a temp file and then `rename`-ing atomically swaps the
161+ /// directory entry to a **new inode**, so:
162+ /// - any process still mapped to the old inode keeps valid pages,
163+ /// - the new daemon maps a pristine inode with a clean signature.
164+ fn atomic_copy_binary ( & self , source_binary : & PathBuf ) -> ServiceResult < ( ) > {
165+ let tmp_path = self . config . binary_path . with_extension ( "new" ) ;
166+
167+ // Copy to a temp file (creates a new inode)
168+ std:: fs:: copy ( source_binary, & tmp_path) ?;
196169
197- // Make binary executable on Unix
170+ // Set permissions on the temp file before rename
198171 #[ cfg( unix) ]
199172 {
200173 use std:: os:: unix:: fs:: PermissionsExt ;
201174 let perms = std:: fs:: Permissions :: from_mode ( 0o755 ) ;
202- std:: fs:: set_permissions ( & self . config . binary_path , perms) ?;
175+ std:: fs:: set_permissions ( & tmp_path , perms) ?;
203176 }
204177
205- // Remove quarantine attribute on macOS (Gatekeeper may block quarantined binaries)
178+ // Remove quarantine on the temp file before rename
206179 #[ cfg( target_os = "macos" ) ]
207- if let Err ( warning) = self . remove_quarantine ( ) {
208- log:: warn!( "[service] {}" , warning) ;
180+ {
181+ use std:: process:: Command ;
182+ // Best-effort: if quarantine removal fails, the rename still
183+ // proceeds — Gatekeeper may prompt but won't crash.
184+ let _ = Command :: new ( "xattr" )
185+ . args ( [ "-d" , "com.apple.quarantine" ] )
186+ . arg ( & tmp_path)
187+ . output ( ) ;
209188 }
210189
211- // Create service configuration
212- self . create_service_config ( ) ?;
190+ // Atomic swap — old inode stays valid for any mapped process
191+ std:: fs:: rename ( & tmp_path, & self . config . binary_path ) ?;
192+
193+ info ! (
194+ "[service] Installed binary to {:?}" ,
195+ self . config. binary_path
196+ ) ;
213197
214- info ! ( "[service] Service installed successfully" ) ;
215198 Ok ( ( ) )
216199 }
217200
@@ -252,23 +235,9 @@ impl ServiceManager {
252235 // Stop the running daemon (ignore errors - may not be running)
253236 self . stop ( ) . ok ( ) ;
254237
255- // Replace the binary
256- std:: fs:: copy ( source_binary, & self . config . binary_path ) ?;
257- info ! ( "[service] Replaced binary at {:?}" , self . config. binary_path) ;
258-
259- // Make binary executable on Unix
260- #[ cfg( unix) ]
261- {
262- use std:: os:: unix:: fs:: PermissionsExt ;
263- let perms = std:: fs:: Permissions :: from_mode ( 0o755 ) ;
264- std:: fs:: set_permissions ( & self . config . binary_path , perms) ?;
265- }
266-
267- // Remove quarantine attribute on macOS (Gatekeeper may block quarantined binaries)
268- #[ cfg( target_os = "macos" ) ]
269- if let Err ( warning) = self . remove_quarantine ( ) {
270- log:: warn!( "[service] {}" , warning) ;
271- }
238+ // Atomically replace the binary (write to temp + rename) so that
239+ // any daemon still mapped to the old inode keeps valid pages.
240+ self . atomic_copy_binary ( source_binary) ?;
272241
273242 // Recreate service config to apply any template changes (e.g., new env vars)
274243 self . create_service_config ( ) ?;
0 commit comments