@@ -181,6 +181,14 @@ pub fn partitions_of(dev: &Utf8Path) -> Result<PartitionTable> {
181
181
182
182
pub struct LoopbackDevice {
183
183
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
+ /// Process ID of the cleanup helper
191
+ helper_pid : u32 ,
184
192
}
185
193
186
194
impl LoopbackDevice {
@@ -208,7 +216,19 @@ impl LoopbackDevice {
208
216
. run_get_string ( ) ?;
209
217
let dev = Utf8PathBuf :: from ( dev. trim ( ) ) ;
210
218
tracing:: debug!( "Allocated loopback {dev}" ) ;
211
- Ok ( Self { dev : Some ( dev) } )
219
+
220
+ // Try to spawn cleanup helper process - if it fails, continue without it
221
+ let cleanup_handle = Self :: spawn_cleanup_helper ( dev. as_str ( ) )
222
+ . map_err ( |e| {
223
+ tracing:: warn!( "Failed to spawn loopback cleanup helper: {}, continuing without signal protection" , e) ;
224
+ e
225
+ } )
226
+ . ok ( ) ;
227
+
228
+ Ok ( Self {
229
+ dev : Some ( dev) ,
230
+ cleanup_handle,
231
+ } )
212
232
}
213
233
214
234
// Access the path to the loopback block device.
@@ -217,13 +237,173 @@ impl LoopbackDevice {
217
237
self . dev . as_deref ( ) . unwrap ( )
218
238
}
219
239
240
+ /// Spawn a cleanup helper process that will clean up the loopback device
241
+ /// if the parent process dies unexpectedly
242
+ fn spawn_cleanup_helper ( device_path : & str ) -> Result < LoopbackCleanupHandle > {
243
+ let device_path = device_path. to_string ( ) ;
244
+
245
+ // Fork the cleanup helper process
246
+ match unsafe { libc:: fork ( ) } {
247
+ -1 => anyhow:: bail!( "Failed to fork cleanup helper process" ) ,
248
+ 0 => {
249
+ // Child process - this will be the cleanup helper
250
+ // This function will not return
251
+ Self :: cleanup_helper_main ( device_path) ;
252
+ }
253
+ child_pid => {
254
+ // Parent process
255
+ Ok ( LoopbackCleanupHandle {
256
+ helper_pid : child_pid as u32 ,
257
+ } )
258
+ }
259
+ }
260
+ }
261
+
262
+ /// Main function for the cleanup helper process
263
+ /// This function does not return - it either exits normally or via exec
264
+ fn cleanup_helper_main ( device_path : String ) -> ! {
265
+ // Close stdin, stdout, stderr and other inherited file descriptors
266
+ unsafe {
267
+ for fd in 0 ..=2 {
268
+ libc:: close ( fd) ;
269
+ }
270
+ // Redirect to /dev/null in case something tries to write
271
+ let null_fd = libc:: open ( b"/dev/null\0 " . as_ptr ( ) as * const i8 , libc:: O_RDWR ) ;
272
+ if null_fd >= 0 {
273
+ libc:: dup2 ( null_fd, 0 ) ;
274
+ libc:: dup2 ( null_fd, 1 ) ;
275
+ libc:: dup2 ( null_fd, 2 ) ;
276
+ if null_fd > 2 {
277
+ libc:: close ( null_fd) ;
278
+ }
279
+ }
280
+ }
281
+
282
+ // Set up death signal notification - we want to be notified when parent dies
283
+ unsafe {
284
+ if libc:: prctl ( libc:: PR_SET_PDEATHSIG , libc:: SIGUSR1 ) != 0 {
285
+ std:: process:: exit ( 1 ) ;
286
+ }
287
+ }
288
+
289
+ // Mask most signals to avoid being killed accidentally
290
+ // But leave SIGUSR1 unmasked so we can receive the death notification
291
+ unsafe {
292
+ let mut sigset: libc:: sigset_t = std:: mem:: zeroed ( ) ;
293
+ libc:: sigfillset ( & mut sigset) ;
294
+ // Don't mask SIGKILL, SIGSTOP (can't be masked anyway), or our death signal
295
+ libc:: sigdelset ( & mut sigset, libc:: SIGKILL ) ;
296
+ libc:: sigdelset ( & mut sigset, libc:: SIGSTOP ) ;
297
+ libc:: sigdelset ( & mut sigset, libc:: SIGUSR1 ) ; // We'll use SIGUSR1 as our death signal
298
+
299
+ if libc:: pthread_sigmask ( libc:: SIG_SETMASK , & sigset, std:: ptr:: null_mut ( ) ) != 0 {
300
+ let err = std:: io:: Error :: last_os_error ( ) ;
301
+ tracing:: error!( "pthread_sigmask failed: {}" , err) ;
302
+ std:: process:: exit ( 1 ) ;
303
+ }
304
+ }
305
+
306
+ // Wait for death signal or normal termination
307
+ let mut siginfo: libc:: siginfo_t = unsafe { std:: mem:: zeroed ( ) } ;
308
+ let sigset = {
309
+ let mut sigset: libc:: sigset_t = unsafe { std:: mem:: zeroed ( ) } ;
310
+ unsafe {
311
+ libc:: sigemptyset ( & mut sigset) ;
312
+ libc:: sigaddset ( & mut sigset, libc:: SIGUSR1 ) ;
313
+ libc:: sigaddset ( & mut sigset, libc:: SIGTERM ) ; // Also listen for SIGTERM (normal cleanup)
314
+ }
315
+ sigset
316
+ } ;
317
+
318
+ // Wait for a signal
319
+ let result = unsafe {
320
+ let result = libc:: sigwaitinfo ( & sigset, & mut siginfo) ;
321
+ if result == -1 {
322
+ let err = std:: io:: Error :: last_os_error ( ) ;
323
+ tracing:: error!( "sigwaitinfo failed: {}" , err) ;
324
+ std:: process:: exit ( 1 ) ;
325
+ }
326
+ result
327
+ } ;
328
+
329
+ if result > 0 {
330
+ if siginfo. si_signo == libc:: SIGUSR1 {
331
+ // Parent died unexpectedly, clean up the loopback device
332
+ let status = std:: process:: Command :: new ( "losetup" )
333
+ . args ( [ "-d" , & device_path] )
334
+ . status ( ) ;
335
+
336
+ match status {
337
+ Ok ( exit_status) if exit_status. success ( ) => {
338
+ // Write to stderr since we closed stdout
339
+ let _ = std:: io:: Write :: write_all (
340
+ & mut std:: io:: stderr ( ) ,
341
+ format ! ( "bootc: cleaned up leaked loopback device {}\n " , device_path)
342
+ . as_bytes ( ) ,
343
+ ) ;
344
+ std:: process:: exit ( 0 ) ;
345
+ }
346
+ Ok ( _) => {
347
+ let _ = std:: io:: Write :: write_all (
348
+ & mut std:: io:: stderr ( ) ,
349
+ format ! (
350
+ "bootc: failed to clean up loopback device {}\n " ,
351
+ device_path
352
+ )
353
+ . as_bytes ( ) ,
354
+ ) ;
355
+ std:: process:: exit ( 1 ) ;
356
+ }
357
+ Err ( e) => {
358
+ let _ = std:: io:: Write :: write_all (
359
+ & mut std:: io:: stderr ( ) ,
360
+ format ! (
361
+ "bootc: error cleaning up loopback device {}: {}\n " ,
362
+ device_path, e
363
+ )
364
+ . as_bytes ( ) ,
365
+ ) ;
366
+ std:: process:: exit ( 1 ) ;
367
+ }
368
+ }
369
+ } else if siginfo. si_signo == libc:: SIGTERM {
370
+ // Normal cleanup signal from parent
371
+ std:: process:: exit ( 0 ) ;
372
+ }
373
+ }
374
+
375
+ // If we get here, something went wrong
376
+ std:: process:: exit ( 1 ) ;
377
+ }
378
+
220
379
// Shared backend for our `close` and `drop` implementations.
221
380
fn impl_close ( & mut self ) -> Result < ( ) > {
222
381
// SAFETY: This is the only place we take the option
223
382
let Some ( dev) = self . dev . take ( ) else {
224
383
tracing:: trace!( "loopback device already deallocated" ) ;
225
384
return Ok ( ( ) ) ;
226
385
} ;
386
+
387
+ // Kill the cleanup helper since we're cleaning up normally
388
+ if let Some ( cleanup_handle) = self . cleanup_handle . take ( ) {
389
+ // Kill the helper process since we're doing normal cleanup
390
+ unsafe {
391
+ if libc:: kill ( cleanup_handle. helper_pid as i32 , libc:: SIGTERM ) != 0 {
392
+ let err = std:: io:: Error :: last_os_error ( ) ;
393
+ tracing:: warn!( "kill failed: {}" , err) ;
394
+ }
395
+ }
396
+ // Wait for it to exit (non-blocking)
397
+ unsafe {
398
+ let mut status = 0 ;
399
+ if libc:: waitpid ( cleanup_handle. helper_pid as i32 , & mut status, libc:: WNOHANG ) == -1
400
+ {
401
+ let err = std:: io:: Error :: last_os_error ( ) ;
402
+ tracing:: warn!( "waitpid failed: {}" , err) ;
403
+ }
404
+ }
405
+ }
406
+
227
407
Command :: new ( "losetup" ) . args ( [ "-d" , dev. as_str ( ) ] ) . run ( )
228
408
}
229
409
@@ -389,4 +569,105 @@ mod test {
389
569
) ;
390
570
Ok ( ( ) )
391
571
}
572
+
573
+ #[ test]
574
+ fn test_loopback_device_with_cleanup_helper ( ) -> Result < ( ) > {
575
+ // Only run this test if we have permissions and losetup is available
576
+ if !std:: path:: Path :: new ( "/usr/bin/losetup" ) . exists ( )
577
+ && !std:: path:: Path :: new ( "/sbin/losetup" ) . exists ( )
578
+ {
579
+ eprintln ! ( "Skipping loopback test: losetup not found" ) ;
580
+ return Ok ( ( ) ) ;
581
+ }
582
+
583
+ // Check if we can run as root or have the necessary capabilities
584
+ if unsafe { libc:: geteuid ( ) } != 0 {
585
+ eprintln ! ( "Skipping loopback test: requires root privileges" ) ;
586
+ return Ok ( ( ) ) ;
587
+ }
588
+
589
+ eprintln ! ( "Running loopback test with root privileges" ) ;
590
+
591
+ // Use /var/tmp which is more likely to persist and be accessible
592
+ let temp_path = format ! ( "/var/tmp/bootc_test_loopback_{}.img" , std:: process:: id( ) ) ;
593
+ let temp_path = std:: path:: Path :: new ( & temp_path) ;
594
+
595
+ eprintln ! ( "Creating temporary file at: {:?}" , temp_path) ;
596
+
597
+ // Create and write the file manually
598
+ {
599
+ use std:: fs:: File ;
600
+ use std:: io:: Write ;
601
+ let mut file = File :: create ( & temp_path) ?;
602
+ // Create a 10MB file
603
+ let data = vec ! [ 0u8 ; 10 * 1024 * 1024 ] ;
604
+ file. write_all ( & data) ?;
605
+ file. sync_all ( ) ?;
606
+ // File is closed here when it goes out of scope
607
+ }
608
+
609
+ // Verify the file exists and has the right size
610
+ let metadata = std:: fs:: metadata ( & temp_path) ?;
611
+ assert_eq ! ( metadata. len( ) , 10 * 1024 * 1024 ) ;
612
+ eprintln ! ( "File created successfully, size: {}" , metadata. len( ) ) ;
613
+
614
+ // Add a small delay to ensure file system operations are complete
615
+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) ;
616
+
617
+ // Double-check the file still exists before calling losetup
618
+ if !temp_path. exists ( ) {
619
+ anyhow:: bail!( "Temporary file disappeared before losetup call" ) ;
620
+ }
621
+
622
+ // Test creating and cleaning up a loopback device
623
+ eprintln ! ( "Creating loopback device..." ) ;
624
+ let loopback_result = LoopbackDevice :: new ( & temp_path) ;
625
+
626
+ // Clean up the temp file before processing the result
627
+ let cleanup_temp_file = || {
628
+ if temp_path. exists ( ) {
629
+ let _ = std:: fs:: remove_file ( & temp_path) ;
630
+ }
631
+ } ;
632
+
633
+ let loopback = match loopback_result {
634
+ Ok ( device) => device,
635
+ Err ( e) => {
636
+ cleanup_temp_file ( ) ;
637
+ return Err ( e) ;
638
+ }
639
+ } ;
640
+
641
+ let device_path = loopback. path ( ) . to_string ( ) ;
642
+ eprintln ! ( "Loopback device created: {}" , device_path) ;
643
+
644
+ // Verify the device was created
645
+ assert ! ( device_path. starts_with( "/dev/loop" ) ) ;
646
+ assert ! ( std:: path:: Path :: new( & device_path) . exists( ) ) ;
647
+
648
+ // Verify we have a cleanup handle
649
+ assert ! (
650
+ loopback. cleanup_handle. is_some( ) ,
651
+ "Cleanup helper should be spawned"
652
+ ) ;
653
+
654
+ eprintln ! ( "Cleanup helper spawned successfully" ) ;
655
+
656
+ // Explicitly close the loopback device to test cleanup
657
+ eprintln ! ( "Closing loopback device..." ) ;
658
+ if let Err ( e) = loopback. close ( ) {
659
+ cleanup_temp_file ( ) ;
660
+ return Err ( e) ;
661
+ }
662
+
663
+ // Give a moment for cleanup to happen
664
+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
665
+
666
+ eprintln ! ( "Test completed successfully" ) ;
667
+
668
+ // Clean up the temporary file
669
+ cleanup_temp_file ( ) ;
670
+
671
+ Ok ( ( ) )
672
+ }
392
673
}
0 commit comments