Skip to content

Commit 7074ca6

Browse files
committed
blockdev: fix loopback cleanup helper path resolution
- Add robust binary path resolution with multiple fallback strategies - Use /proc/self/exe, argv[0], common paths, and PATH search - Add graceful fallback when cleanup helper can't be spawned - Improve error handling and logging - Add comprehensive tests for binary finding logic This fixes the 'Failed to spawn loopback cleanup helper' error that was causing issues in packaged distributions where the binary path was not easily discoverable.
1 parent a81de6d commit 7074ca6

File tree

1 file changed

+96
-25
lines changed

1 file changed

+96
-25
lines changed

crates/blockdev/src/blockdev.rs

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use std::collections::HashMap;
22
use std::env;
33
use std::path::Path;
4-
use std::process::Command;
4+
use std::path::PathBuf;
5+
use std::process::{Command, Stdio};
56
use std::sync::OnceLock;
67

78
use anyhow::{anyhow, Context, Result};
8-
use camino::Utf8Path;
9-
use camino::Utf8PathBuf;
9+
use camino::{Utf8Path, Utf8PathBuf};
1010
use fn_error_context::context;
1111
use regex::Regex;
1212
use serde::Deserialize;
@@ -194,6 +194,10 @@ struct LoopbackCleanupHandle {
194194
impl LoopbackDevice {
195195
// Create a new loopback block device targeting the provided file path.
196196
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
197201
let direct_io = match env::var("BOOTC_DIRECT_IO") {
198202
Ok(val) => {
199203
if val == "on" {
@@ -205,25 +209,50 @@ impl LoopbackDevice {
205209
Err(_e) => "off",
206210
};
207211

208-
let dev = Command::new("losetup")
212+
// Create the loopback device
213+
let output = Command::new("losetup")
209214
.args([
210215
"--show",
211216
format!("--direct-io={direct_io}").as_str(),
212217
"-P",
213218
"--find",
219+
path.as_str(),
214220
])
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+
218237
tracing::debug!("Allocated loopback {dev}");
219238

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+
};
223252

224253
Ok(Self {
225254
dev: Some(dev),
226-
cleanup_handle: Some(cleanup_handle),
255+
cleanup_handle,
227256
})
228257
}
229258

@@ -236,14 +265,12 @@ impl LoopbackDevice {
236265
/// Spawn a cleanup helper process that will clean up the loopback device
237266
/// if the parent process dies unexpectedly
238267
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")?;
244271

245272
// Create the helper process
246-
let mut cmd = Command::new(self_exe);
273+
let mut cmd = Command::new(bootc_path);
247274
cmd.args(["loopback-cleanup-helper", "--device", device_path]);
248275

249276
// Set environment variable to indicate this is a cleanup helper
@@ -262,6 +289,40 @@ impl LoopbackDevice {
262289
Ok(LoopbackCleanupHandle { child })
263290
}
264291

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+
265326
// Shared backend for our `close` and `drop` implementations.
266327
fn impl_close(&mut self) -> Result<()> {
267328
// SAFETY: This is the only place we take the option
@@ -272,7 +333,7 @@ impl LoopbackDevice {
272333

273334
// Kill the cleanup helper since we're cleaning up normally
274335
if let Some(mut cleanup_handle) = self.cleanup_handle.take() {
275-
// Send SIGTERM to the child process
336+
// Send SIGKILL to the child process
276337
let _ = cleanup_handle.child.kill();
277338
}
278339

@@ -311,22 +372,32 @@ pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
311372
.await;
312373

313374
// Clean up the loopback device
314-
let status = std::process::Command::new("losetup")
375+
let output = std::process::Command::new("losetup")
315376
.args(["-d", device_path])
316-
.status();
377+
.output();
317378

318-
match status {
319-
Ok(exit_status) if exit_status.success() => {
379+
match output {
380+
Ok(output) if output.status.success() => {
320381
// Log to systemd journal instead of stderr
321382
tracing::info!("Cleaned up leaked loopback device {}", device_path);
322383
std::process::exit(0);
323384
}
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+
);
326393
std::process::exit(1);
327394
}
328395
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+
);
330401
std::process::exit(1);
331402
}
332403
}

0 commit comments

Comments
 (0)