@@ -181,6 +181,14 @@ pub fn partitions_of(dev: &Utf8Path) -> Result<PartitionTable> {
181181
182182pub struct LoopbackDevice {
183183 pub dev : Option < Utf8PathBuf > ,
184+ // Handle to the cleanup helper process
185+ cleanup_handle : Option < LoopbackCleanupHandle > ,
186+ }
187+
188+ /// Handle to manage the cleanup helper process for loopback devices
189+ struct LoopbackCleanupHandle {
190+ /// Child process handle
191+ child : std:: process:: Child ,
184192}
185193
186194impl LoopbackDevice {
@@ -208,7 +216,15 @@ impl LoopbackDevice {
208216 . run_get_string ( ) ?;
209217 let dev = Utf8PathBuf :: from ( dev. trim ( ) ) ;
210218 tracing:: debug!( "Allocated loopback {dev}" ) ;
211- Ok ( Self { dev : Some ( dev) } )
219+
220+ // Try to spawn cleanup helper process - if it fails, make it fatal
221+ let cleanup_handle = Self :: spawn_cleanup_helper ( dev. as_str ( ) )
222+ . context ( "Failed to spawn loopback cleanup helper" ) ?;
223+
224+ Ok ( Self {
225+ dev : Some ( dev) ,
226+ cleanup_handle : Some ( cleanup_handle) ,
227+ } )
212228 }
213229
214230 // Access the path to the loopback block device.
@@ -217,13 +233,49 @@ impl LoopbackDevice {
217233 self . dev . as_deref ( ) . unwrap ( )
218234 }
219235
236+ /// Spawn a cleanup helper process that will clean up the loopback device
237+ /// if the parent process dies unexpectedly
238+ fn spawn_cleanup_helper ( device_path : & str ) -> Result < LoopbackCleanupHandle > {
239+ use std:: process:: { Command , Stdio } ;
240+
241+ // Get the path to our own executable
242+ let self_exe =
243+ std:: fs:: read_link ( "/proc/self/exe" ) . context ( "Failed to read /proc/self/exe" ) ?;
244+
245+ // Create the helper process
246+ let mut cmd = Command :: new ( self_exe) ;
247+ cmd. args ( [ "loopback-cleanup-helper" , "--device" , device_path] ) ;
248+
249+ // Set environment variable to indicate this is a cleanup helper
250+ cmd. env ( "BOOTC_LOOPBACK_CLEANUP_HELPER" , "1" ) ;
251+
252+ // Set up stdio to redirect to /dev/null
253+ cmd. stdin ( Stdio :: null ( ) ) ;
254+ cmd. stdout ( Stdio :: null ( ) ) ;
255+ cmd. stderr ( Stdio :: null ( ) ) ;
256+
257+ // Spawn the process
258+ let child = cmd
259+ . spawn ( )
260+ . context ( "Failed to spawn loopback cleanup helper" ) ?;
261+
262+ Ok ( LoopbackCleanupHandle { child } )
263+ }
264+
220265 // Shared backend for our `close` and `drop` implementations.
221266 fn impl_close ( & mut self ) -> Result < ( ) > {
222267 // SAFETY: This is the only place we take the option
223268 let Some ( dev) = self . dev . take ( ) else {
224269 tracing:: trace!( "loopback device already deallocated" ) ;
225270 return Ok ( ( ) ) ;
226271 } ;
272+
273+ // Kill the cleanup helper since we're cleaning up normally
274+ if let Some ( mut cleanup_handle) = self . cleanup_handle . take ( ) {
275+ // Send SIGTERM to the child process
276+ let _ = cleanup_handle. child . kill ( ) ;
277+ }
278+
227279 Command :: new ( "losetup" ) . args ( [ "-d" , dev. as_str ( ) ] ) . run ( )
228280 }
229281
@@ -240,6 +292,46 @@ impl Drop for LoopbackDevice {
240292 }
241293}
242294
295+ /// Main function for the loopback cleanup helper process
296+ /// This function does not return - it either exits normally or via signal
297+ pub async fn run_loopback_cleanup_helper ( device_path : & str ) -> Result < ( ) > {
298+ // Check if we're running as a cleanup helper
299+ if std:: env:: var ( "BOOTC_LOOPBACK_CLEANUP_HELPER" ) . is_err ( ) {
300+ anyhow:: bail!( "This function should only be called as a cleanup helper" ) ;
301+ }
302+
303+ // Set up death signal notification - we want to be notified when parent dies
304+ rustix:: process:: set_parent_process_death_signal ( Some ( rustix:: process:: Signal :: TERM ) )
305+ . context ( "Failed to set parent death signal" ) ?;
306+
307+ // Wait for SIGTERM (either from parent death or normal cleanup)
308+ tokio:: signal:: unix:: signal ( tokio:: signal:: unix:: SignalKind :: terminate ( ) )
309+ . expect ( "Failed to create signal stream" )
310+ . recv ( )
311+ . await ;
312+
313+ // Clean up the loopback device
314+ let status = std:: process:: Command :: new ( "losetup" )
315+ . args ( [ "-d" , device_path] )
316+ . status ( ) ;
317+
318+ match status {
319+ Ok ( exit_status) if exit_status. success ( ) => {
320+ // Log to systemd journal instead of stderr
321+ tracing:: info!( "Cleaned up leaked loopback device {}" , device_path) ;
322+ std:: process:: exit ( 0 ) ;
323+ }
324+ Ok ( _) => {
325+ tracing:: error!( "Failed to clean up loopback device {}" , device_path) ;
326+ std:: process:: exit ( 1 ) ;
327+ }
328+ Err ( e) => {
329+ tracing:: error!( "Error cleaning up loopback device {}: {}" , device_path, e) ;
330+ std:: process:: exit ( 1 ) ;
331+ }
332+ }
333+ }
334+
243335/// Parse key-value pairs from lsblk --pairs.
244336/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't.
245337fn split_lsblk_line ( line : & str ) -> HashMap < String , String > {
0 commit comments