Skip to content

Commit b803ea7

Browse files
authored
Merge pull request #1449 from gursewak1997/fix-loopback-cleanup-helper
blockdev: implement signal-safe loopback device cleanup helper
2 parents f4b01ab + ff004c9 commit b803ea7

File tree

5 files changed

+198
-4
lines changed

5 files changed

+198
-4
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/blockdev/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ anyhow = { workspace = true }
1111
bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" }
1212
camino = { workspace = true, features = ["serde1"] }
1313
fn-error-context = { workspace = true }
14+
libc = { workspace = true }
1415
regex = "1.10.4"
16+
rustix = { workspace = true }
1517
serde = { workspace = true, features = ["derive"] }
1618
serde_json = { workspace = true }
19+
tempfile = { workspace = true }
20+
tokio = { workspace = true, features = ["signal"] }
1721
tracing = { workspace = true }
1822

1923
[dev-dependencies]

crates/blockdev/src/blockdev.rs

Lines changed: 152 additions & 4 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;
@@ -181,6 +181,14 @@ pub fn partitions_of(dev: &Utf8Path) -> Result<PartitionTable> {
181181

182182
pub 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

186194
impl LoopbackDevice {
@@ -208,7 +216,25 @@ 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, 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+
})
212238
}
213239

214240
// Access the path to the loopback block device.
@@ -217,13 +243,85 @@ impl LoopbackDevice {
217243
self.dev.as_deref().unwrap()
218244
}
219245

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+
220311
// Shared backend for our `close` and `drop` implementations.
221312
fn impl_close(&mut self) -> Result<()> {
222313
// SAFETY: This is the only place we take the option
223314
let Some(dev) = self.dev.take() else {
224315
tracing::trace!("loopback device already deallocated");
225316
return Ok(());
226317
};
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+
227325
Command::new("losetup").args(["-d", dev.as_str()]).run()
228326
}
229327

@@ -240,6 +338,56 @@ impl Drop for LoopbackDevice {
240338
}
241339
}
242340

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+
243391
/// Parse key-value pairs from lsblk --pairs.
244392
/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't.
245393
fn split_lsblk_line(line: &str) -> HashMap<String, String> {

crates/lib/src/cli.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,18 @@ pub(crate) enum InternalsOpts {
462462
#[clap(allow_hyphen_values = true)]
463463
args: Vec<OsString>,
464464
},
465+
/// Loopback device cleanup helper (internal use only)
466+
LoopbackCleanupHelper {
467+
/// Device path to clean up
468+
#[clap(long)]
469+
device: String,
470+
},
471+
/// Test loopback device allocation and cleanup (internal use only)
472+
AllocateCleanupLoopback {
473+
/// File path to create loopback device for
474+
#[clap(long)]
475+
file_path: Utf8PathBuf,
476+
},
465477
/// Invoked from ostree-ext to complete an installation.
466478
BootcInstallCompletion {
467479
/// Path to the sysroot
@@ -1264,6 +1276,29 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
12641276
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
12651277
crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
12661278
}
1279+
InternalsOpts::LoopbackCleanupHelper { device } => {
1280+
crate::blockdev::run_loopback_cleanup_helper(&device).await
1281+
}
1282+
InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
1283+
// Create a temporary file for testing
1284+
let temp_file =
1285+
tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
1286+
let temp_path = temp_file.path();
1287+
1288+
// Create a loopback device
1289+
let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
1290+
.context("Failed to create loopback device")?;
1291+
1292+
println!("Created loopback device: {}", loopback.path());
1293+
1294+
// Close the device to test cleanup
1295+
loopback
1296+
.close()
1297+
.context("Failed to close loopback device")?;
1298+
1299+
println!("Successfully closed loopback device");
1300+
Ok(())
1301+
}
12671302
#[cfg(feature = "rhsm")]
12681303
InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
12691304
},

crates/lib/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ mod kernel;
3838

3939
#[cfg(feature = "rhsm")]
4040
mod rhsm;
41+
42+
// Re-export blockdev crate for internal use
43+
pub(crate) use bootc_blockdev as blockdev;

0 commit comments

Comments
 (0)