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 ;
@@ -194,6 +194,10 @@ struct LoopbackCleanupHandle {
194
194
impl LoopbackDevice {
195
195
// Create a new loopback block device targeting the provided file path.
196
196
pub fn new ( path : & Path ) -> Result < Self > {
197
+ let path = Utf8PathBuf :: from_path_buf ( path. to_path_buf ( ) )
198
+ . map_err ( |_| anyhow ! ( "Path is not valid UTF-8" ) ) ?;
199
+
200
+ // Check for direct I/O setting
197
201
let direct_io = match env:: var ( "BOOTC_DIRECT_IO" ) {
198
202
Ok ( val) => {
199
203
if val == "on" {
@@ -205,25 +209,50 @@ impl LoopbackDevice {
205
209
Err ( _e) => "off" ,
206
210
} ;
207
211
208
- let dev = Command :: new ( "losetup" )
212
+ // Create the loopback device
213
+ let output = Command :: new ( "losetup" )
209
214
. args ( [
210
215
"--show" ,
211
216
format ! ( "--direct-io={direct_io}" ) . as_str ( ) ,
212
217
"-P" ,
213
218
"--find" ,
219
+ path. as_str ( ) ,
214
220
] )
215
- . arg ( path)
216
- . run_get_string ( ) ?;
217
- let dev = Utf8PathBuf :: from ( dev. trim ( ) ) ;
221
+ . output ( )
222
+ . context ( "Failed to create loopback device" ) ?;
223
+
224
+ if !output. status . success ( ) {
225
+ anyhow:: bail!(
226
+ "losetup failed: {}" ,
227
+ String :: from_utf8_lossy( & output. stderr)
228
+ ) ;
229
+ }
230
+
231
+ let dev = Utf8PathBuf :: from (
232
+ String :: from_utf8 ( output. stdout )
233
+ . context ( "losetup output is not valid UTF-8" ) ?
234
+ . trim ( ) ,
235
+ ) ;
236
+
218
237
tracing:: debug!( "Allocated loopback {dev}" ) ;
219
238
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" ) ?;
239
+ // Try to spawn cleanup helper, but don't fail if it doesn't work
240
+ let cleanup_handle = match Self :: spawn_cleanup_helper ( dev. as_str ( ) ) {
241
+ Ok ( handle) => Some ( handle) ,
242
+ Err ( e) => {
243
+ tracing:: warn!(
244
+ "Failed to spawn loopback cleanup helper for {}: {}. \
245
+ Loopback device may not be cleaned up if process is interrupted.",
246
+ dev,
247
+ e
248
+ ) ;
249
+ None
250
+ }
251
+ } ;
223
252
224
253
Ok ( Self {
225
254
dev : Some ( dev) ,
226
- cleanup_handle : Some ( cleanup_handle ) ,
255
+ cleanup_handle,
227
256
} )
228
257
}
229
258
@@ -236,14 +265,12 @@ impl LoopbackDevice {
236
265
/// Spawn a cleanup helper process that will clean up the loopback device
237
266
/// if the parent process dies unexpectedly
238
267
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" ) ?;
268
+ // Try multiple strategies to find the bootc binary
269
+ let bootc_path = Self :: find_bootc_binary ( )
270
+ . context ( "Failed to locate bootc binary for cleanup helper" ) ?;
244
271
245
272
// Create the helper process
246
- let mut cmd = Command :: new ( self_exe ) ;
273
+ let mut cmd = Command :: new ( bootc_path ) ;
247
274
cmd. args ( [ "loopback-cleanup-helper" , "--device" , device_path] ) ;
248
275
249
276
// Set environment variable to indicate this is a cleanup helper
@@ -262,6 +289,40 @@ impl LoopbackDevice {
262
289
Ok ( LoopbackCleanupHandle { child } )
263
290
}
264
291
292
+ /// Find the bootc binary using multiple strategies
293
+ fn find_bootc_binary ( ) -> Result < PathBuf > {
294
+ // Strategy 1: Try /proc/self/exe (works in most cases)
295
+ if let Ok ( exe_path) = std:: fs:: read_link ( "/proc/self/exe" ) {
296
+ if exe_path. exists ( ) {
297
+ return Ok ( exe_path) ;
298
+ }
299
+ }
300
+
301
+ // Strategy 2: Try argv[0] from std::env
302
+ if let Some ( argv0) = std:: env:: args ( ) . next ( ) {
303
+ let argv0_path = PathBuf :: from ( argv0) ;
304
+ if argv0_path. is_absolute ( ) && argv0_path. exists ( ) {
305
+ return Ok ( argv0_path) ;
306
+ }
307
+ // If it's relative, try to resolve it
308
+ if let Ok ( canonical) = argv0_path. canonicalize ( ) {
309
+ return Ok ( canonical) ;
310
+ }
311
+ }
312
+
313
+ // Strategy 3: Try common installation paths
314
+ let common_paths = [ "/usr/bin/bootc" , "/usr/local/bin/bootc" ] ;
315
+
316
+ for path in & common_paths {
317
+ let path_buf = PathBuf :: from ( path) ;
318
+ if path_buf. exists ( ) {
319
+ return Ok ( path_buf) ;
320
+ }
321
+ }
322
+
323
+ anyhow:: bail!( "Could not locate bootc binary using any available strategy" )
324
+ }
325
+
265
326
// Shared backend for our `close` and `drop` implementations.
266
327
fn impl_close ( & mut self ) -> Result < ( ) > {
267
328
// SAFETY: This is the only place we take the option
@@ -272,7 +333,7 @@ impl LoopbackDevice {
272
333
273
334
// Kill the cleanup helper since we're cleaning up normally
274
335
if let Some ( mut cleanup_handle) = self . cleanup_handle . take ( ) {
275
- // Send SIGTERM to the child process
336
+ // Send SIGKILL to the child process
276
337
let _ = cleanup_handle. child . kill ( ) ;
277
338
}
278
339
@@ -311,22 +372,32 @@ pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
311
372
. await ;
312
373
313
374
// Clean up the loopback device
314
- let status = std:: process:: Command :: new ( "losetup" )
375
+ let output = std:: process:: Command :: new ( "losetup" )
315
376
. args ( [ "-d" , device_path] )
316
- . status ( ) ;
377
+ . output ( ) ;
317
378
318
- match status {
319
- Ok ( exit_status ) if exit_status . success ( ) => {
379
+ match output {
380
+ Ok ( output ) if output . status . success ( ) => {
320
381
// Log to systemd journal instead of stderr
321
382
tracing:: info!( "Cleaned up leaked loopback device {}" , device_path) ;
322
383
std:: process:: exit ( 0 ) ;
323
384
}
324
- Ok ( _) => {
325
- tracing:: error!( "Failed to clean up loopback device {}" , device_path) ;
385
+ Ok ( output) => {
386
+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
387
+ tracing:: error!(
388
+ "Failed to clean up loopback device {}: {}. Stderr: {}" ,
389
+ device_path,
390
+ output. status,
391
+ stderr. trim( )
392
+ ) ;
326
393
std:: process:: exit ( 1 ) ;
327
394
}
328
395
Err ( e) => {
329
- tracing:: error!( "Error cleaning up loopback device {}: {}" , device_path, e) ;
396
+ tracing:: error!(
397
+ "Error executing losetup to clean up loopback device {}: {}" ,
398
+ device_path,
399
+ e
400
+ ) ;
330
401
std:: process:: exit ( 1 ) ;
331
402
}
332
403
}
0 commit comments