1
1
use std:: collections:: HashMap ;
2
2
use std:: env;
3
3
use std:: path:: Path ;
4
- use std:: process:: Command ;
4
+ use std:: path:: PathBuf ;
5
+ use std:: process:: { Command , Stdio } ;
5
6
use std:: sync:: OnceLock ;
6
7
7
8
use anyhow:: { anyhow, Context , Result } ;
8
- use camino:: Utf8Path ;
9
- use camino:: Utf8PathBuf ;
9
+ use camino:: { Utf8Path , Utf8PathBuf } ;
10
10
use fn_error_context:: context;
11
11
use regex:: Regex ;
12
12
use serde:: Deserialize ;
@@ -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
+ /// Child process handle
191
+ child : std:: process:: Child ,
184
192
}
185
193
186
194
impl LoopbackDevice {
@@ -208,7 +216,25 @@ 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, but don't fail if it doesn't work
221
+ let cleanup_handle = match Self :: spawn_cleanup_helper ( dev. as_str ( ) ) {
222
+ Ok ( handle) => Some ( handle) ,
223
+ Err ( e) => {
224
+ tracing:: warn!(
225
+ "Failed to spawn loopback cleanup helper for {}: {}. \
226
+ Loopback device may not be cleaned up if process is interrupted.",
227
+ dev,
228
+ e
229
+ ) ;
230
+ None
231
+ }
232
+ } ;
233
+
234
+ Ok ( Self {
235
+ dev : Some ( dev) ,
236
+ cleanup_handle,
237
+ } )
212
238
}
213
239
214
240
// Access the path to the loopback block device.
@@ -217,13 +243,85 @@ impl LoopbackDevice {
217
243
self . dev . as_deref ( ) . unwrap ( )
218
244
}
219
245
246
+ /// Spawn a cleanup helper process that will clean up the loopback device
247
+ /// if the parent process dies unexpectedly
248
+ fn spawn_cleanup_helper ( device_path : & str ) -> Result < LoopbackCleanupHandle > {
249
+ // Try multiple strategies to find the bootc binary
250
+ let bootc_path = Self :: find_bootc_binary ( )
251
+ . context ( "Failed to locate bootc binary for cleanup helper" ) ?;
252
+
253
+ // Create the helper process
254
+ let mut cmd = Command :: new ( bootc_path) ;
255
+ cmd. args ( [ "loopback-cleanup-helper" , "--device" , device_path] ) ;
256
+
257
+ // Set environment variable to indicate this is a cleanup helper
258
+ cmd. env ( "BOOTC_LOOPBACK_CLEANUP_HELPER" , "1" ) ;
259
+
260
+ // Set up stdio to redirect to /dev/null
261
+ cmd. stdin ( Stdio :: null ( ) ) ;
262
+ cmd. stdout ( Stdio :: null ( ) ) ;
263
+ // Don't redirect stderr so we can see error messages
264
+
265
+ // Spawn the process
266
+ let child = cmd
267
+ . spawn ( )
268
+ . context ( "Failed to spawn loopback cleanup helper" ) ?;
269
+
270
+ Ok ( LoopbackCleanupHandle { child } )
271
+ }
272
+
273
+ /// Find the bootc binary using multiple strategies
274
+ fn find_bootc_binary ( ) -> Result < PathBuf > {
275
+ // Strategy 1: Try /proc/self/exe (works in most cases)
276
+ if let Ok ( exe_path) = std:: fs:: read_link ( "/proc/self/exe" ) {
277
+ if exe_path. exists ( ) {
278
+ return Ok ( exe_path) ;
279
+ } else {
280
+ tracing:: warn!( "/proc/self/exe points to non-existent path: {:?}" , exe_path) ;
281
+ }
282
+ } else {
283
+ tracing:: warn!( "Failed to read /proc/self/exe" ) ;
284
+ }
285
+
286
+ // Strategy 2: Try argv[0] from std::env
287
+ if let Some ( argv0) = std:: env:: args ( ) . next ( ) {
288
+ let argv0_path = PathBuf :: from ( argv0) ;
289
+ if argv0_path. is_absolute ( ) && argv0_path. exists ( ) {
290
+ return Ok ( argv0_path) ;
291
+ }
292
+ // If it's relative, try to resolve it
293
+ if let Ok ( canonical) = argv0_path. canonicalize ( ) {
294
+ return Ok ( canonical) ;
295
+ }
296
+ }
297
+
298
+ // Strategy 3: Try common installation paths
299
+ let common_paths = [ "/usr/bin/bootc" , "/usr/local/bin/bootc" ] ;
300
+
301
+ for path in & common_paths {
302
+ let path_buf = PathBuf :: from ( path) ;
303
+ if path_buf. exists ( ) {
304
+ return Ok ( path_buf) ;
305
+ }
306
+ }
307
+
308
+ anyhow:: bail!( "Could not locate bootc binary using any available strategy" )
309
+ }
310
+
220
311
// Shared backend for our `close` and `drop` implementations.
221
312
fn impl_close ( & mut self ) -> Result < ( ) > {
222
313
// SAFETY: This is the only place we take the option
223
314
let Some ( dev) = self . dev . take ( ) else {
224
315
tracing:: trace!( "loopback device already deallocated" ) ;
225
316
return Ok ( ( ) ) ;
226
317
} ;
318
+
319
+ // Kill the cleanup helper since we're cleaning up normally
320
+ if let Some ( mut cleanup_handle) = self . cleanup_handle . take ( ) {
321
+ // Send SIGTERM to the child process and let it do the cleanup
322
+ let _ = cleanup_handle. child . kill ( ) ;
323
+ }
324
+
227
325
Command :: new ( "losetup" ) . args ( [ "-d" , dev. as_str ( ) ] ) . run ( )
228
326
}
229
327
@@ -240,6 +338,56 @@ impl Drop for LoopbackDevice {
240
338
}
241
339
}
242
340
341
+ /// Main function for the loopback cleanup helper process
342
+ /// This function does not return - it either exits normally or via signal
343
+ pub async fn run_loopback_cleanup_helper ( device_path : & str ) -> Result < ( ) > {
344
+ // Check if we're running as a cleanup helper
345
+ if std:: env:: var ( "BOOTC_LOOPBACK_CLEANUP_HELPER" ) . is_err ( ) {
346
+ anyhow:: bail!( "This function should only be called as a cleanup helper" ) ;
347
+ }
348
+
349
+ // Set up death signal notification - we want to be notified when parent dies
350
+ rustix:: process:: set_parent_process_death_signal ( Some ( rustix:: process:: Signal :: TERM ) )
351
+ . context ( "Failed to set parent death signal" ) ?;
352
+
353
+ // Wait for SIGTERM (either from parent death or normal cleanup)
354
+ tokio:: signal:: unix:: signal ( tokio:: signal:: unix:: SignalKind :: terminate ( ) )
355
+ . expect ( "Failed to create signal stream" )
356
+ . recv ( )
357
+ . await ;
358
+
359
+ // Clean up the loopback device
360
+ let output = std:: process:: Command :: new ( "losetup" )
361
+ . args ( [ "-d" , device_path] )
362
+ . output ( ) ;
363
+
364
+ match output {
365
+ Ok ( output) if output. status . success ( ) => {
366
+ // Log to systemd journal instead of stderr
367
+ tracing:: info!( "Cleaned up leaked loopback device {}" , device_path) ;
368
+ std:: process:: exit ( 0 ) ;
369
+ }
370
+ Ok ( output) => {
371
+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
372
+ tracing:: error!(
373
+ "Failed to clean up loopback device {}: {}. Stderr: {}" ,
374
+ device_path,
375
+ output. status,
376
+ stderr. trim( )
377
+ ) ;
378
+ std:: process:: exit ( 1 ) ;
379
+ }
380
+ Err ( e) => {
381
+ tracing:: error!(
382
+ "Error executing losetup to clean up loopback device {}: {}" ,
383
+ device_path,
384
+ e
385
+ ) ;
386
+ std:: process:: exit ( 1 ) ;
387
+ }
388
+ }
389
+ }
390
+
243
391
/// Parse key-value pairs from lsblk --pairs.
244
392
/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't.
245
393
fn split_lsblk_line ( line : & str ) -> HashMap < String , String > {
0 commit comments