Skip to content

Commit a48253c

Browse files
committed
fix(vm): resolve e2fsprogs through augmented path
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent fde5568 commit a48253c

1 file changed

Lines changed: 153 additions & 165 deletions

File tree

‎crates/openshell-driver-vm/src/rootfs.rs‎

Lines changed: 153 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use flate2::read::MultiGzDecoder;
55
use sha2::{Digest, Sha256};
6+
use std::ffi::{OsStr, OsString};
67
use std::fs;
78
use std::fs::File;
89
#[cfg(test)]
@@ -370,36 +371,24 @@ pub fn set_rootfs_image_file_mode(
370371
/// Replay the ext4 journal and repair automatically correctable filesystem
371372
/// state before the driver mutates a preserved guest disk offline.
372373
pub fn recover_rootfs_image(image_path: &Path) -> Result<(), String> {
373-
let mut failures = Vec::new();
374-
let mut unavailable = Vec::new();
375-
376-
for candidate in e2fs_tool_candidates("e2fsck") {
377-
let label = candidate.display().to_string();
378-
match Command::new(&candidate)
379-
.arg("-p")
380-
.arg("-f")
381-
.arg(image_path)
382-
.output()
383-
{
384-
Ok(output) if matches!(output.status.code(), Some(0..=2)) => return Ok(()),
385-
Ok(output) => failures.push(format!(
386-
"{label} failed with status {}\nstdout: {}\nstderr: {}",
387-
output.status,
388-
String::from_utf8_lossy(&output.stdout),
389-
String::from_utf8_lossy(&output.stderr)
390-
)),
391-
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
392-
unavailable.push(format!("{label} not found"));
393-
}
394-
Err(error) => failures.push(format!("run {label}: {error}")),
374+
let output = e2fs_command("e2fsck")?
375+
.arg("-p")
376+
.arg("-f")
377+
.arg(image_path)
378+
.output();
379+
match output {
380+
Ok(output) if matches!(output.status.code(), Some(0..=2)) => Ok(()),
381+
Ok(output) => Err(format!(
382+
"e2fsck failed with status {}\nstdout: {}\nstderr: {}",
383+
output.status,
384+
String::from_utf8_lossy(&output.stdout),
385+
String::from_utf8_lossy(&output.stderr)
386+
)),
387+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
388+
Err("e2fsck not found".to_string())
395389
}
390+
Err(error) => Err(format!("run e2fsck: {error}")),
396391
}
397-
398-
Err(if failures.is_empty() {
399-
unavailable.join("\n")
400-
} else {
401-
failures.join("\n")
402-
})
403392
}
404393

405394
#[cfg(target_os = "macos")]
@@ -675,12 +664,14 @@ enum FormatterAttempt {
675664
}
676665

677666
fn format_ext4_image_from_dir(source: &Path, image_path: &Path) -> Result<(), String> {
678-
let candidates = ["mke2fs", "mkfs.ext4"]
679-
.into_iter()
680-
.flat_map(e2fs_tool_candidates);
667+
let candidates = ["mke2fs", "mkfs.ext4"].map(PathBuf::from);
681668
run_ext4_formatter_candidates(candidates, |candidate| {
682669
let label = candidate.display().to_string();
683-
let output = Command::new(candidate)
670+
let mut command = match e2fs_command(candidate.as_os_str()) {
671+
Ok(command) => command,
672+
Err(error) => return FormatterAttempt::Failed(error),
673+
};
674+
let output = command
684675
.arg("-q")
685676
.arg("-F")
686677
.arg("-t")
@@ -699,10 +690,10 @@ fn format_ext4_image_from_dir(source: &Path, image_path: &Path) -> Result<(), St
699690
String::from_utf8_lossy(&output.stdout),
700691
String::from_utf8_lossy(&output.stderr)
701692
)),
702-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
693+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
703694
FormatterAttempt::Unavailable(format!("{label} not found"))
704695
}
705-
Err(err) => FormatterAttempt::Failed(format!("run {label}: {err}")),
696+
Err(error) => FormatterAttempt::Failed(format!("run {label}: {error}")),
706697
}
707698
})
708699
.map_err(|details| {
@@ -895,45 +886,33 @@ fn sandbox_guest_user_ids_from_image_path(
895886
let quoted_path = debugfs_quote_absolute_path(guest_path)
896887
.expect("the static passwd path is a valid debugfs path");
897888
let command = format!("cat {quoted_path}");
898-
let mut last_error = None;
899-
900-
for candidate in e2fs_tool_candidates("debugfs") {
901-
let label = candidate.display().to_string();
902-
match Command::new(&candidate)
903-
.arg("-R")
904-
.arg(&command)
905-
.arg(image_path)
906-
.output()
907-
{
908-
Ok(output) if output.status.success() => {
909-
let passwd = String::from_utf8(output.stdout).map_err(|error| {
910-
format!(
911-
"read {guest_path} from {} as UTF-8: {error}",
912-
image_path.display()
913-
)
914-
})?;
915-
return parse_sandbox_guest_user_ids(&passwd, &image_path.display().to_string());
916-
}
917-
Ok(output) => {
918-
last_error = Some(format!(
919-
"{label} failed with status {}\nstdout: {}\nstderr: {}",
920-
output.status,
921-
String::from_utf8_lossy(&output.stdout),
922-
String::from_utf8_lossy(&output.stderr)
923-
));
924-
}
925-
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
926-
last_error = Some(format!("{label} not found"));
927-
}
928-
Err(error) => last_error = Some(format!("run {label}: {error}")),
889+
let output = e2fs_command("debugfs")?
890+
.arg("-R")
891+
.arg(&command)
892+
.arg(image_path)
893+
.output();
894+
match output {
895+
Ok(output) if output.status.success() => {
896+
let passwd = String::from_utf8(output.stdout).map_err(|error| {
897+
format!(
898+
"read {guest_path} from {} as UTF-8: {error}",
899+
image_path.display()
900+
)
901+
})?;
902+
parse_sandbox_guest_user_ids(&passwd, &image_path.display().to_string())
929903
}
904+
Ok(output) => Err(format!(
905+
"debugfs command '{command}' failed for {}: debugfs failed with status {}\nstdout: {}\nstderr: {}. Install e2fsprogs (debugfs) and retry",
906+
image_path.display(),
907+
output.status,
908+
String::from_utf8_lossy(&output.stdout),
909+
String::from_utf8_lossy(&output.stderr)
910+
)),
911+
Err(error) => Err(format!(
912+
"debugfs command '{command}' failed for {}: {error}. Install e2fsprogs (debugfs) and retry",
913+
image_path.display()
914+
)),
930915
}
931-
932-
Err(format!(
933-
"debugfs command '{command}' failed for {}: {}. Install e2fsprogs (debugfs) and retry",
934-
image_path.display(),
935-
last_error.unwrap_or_else(|| "debugfs not found".to_string())
936-
))
937916
}
938917

939918
fn sandbox_guest_user_ids(rootfs: &Path) -> Result<Option<(u32, u32)>, String> {
@@ -983,92 +962,82 @@ fn run_debugfs_batch(image_path: &Path, commands: &[String]) -> Result<(), Strin
983962
}
984963

985964
fn run_debugfs_batch_file(image_path: &Path, command_path: &Path) -> Result<(), String> {
986-
let mut last_error = None;
987-
for candidate in e2fs_tool_candidates("debugfs") {
988-
let label = candidate.display().to_string();
989-
let output = Command::new(&candidate)
990-
.arg("-w")
991-
.arg("-f")
992-
.arg(command_path)
993-
.arg(image_path)
994-
.output();
995-
match output {
996-
Ok(output) if output.status.success() => return Ok(()),
997-
Ok(output) => {
998-
last_error = Some(format!(
999-
"{label} failed with status {}\nstdout: {}\nstderr: {}",
1000-
output.status,
1001-
String::from_utf8_lossy(&output.stdout),
1002-
String::from_utf8_lossy(&output.stderr)
1003-
));
1004-
}
1005-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1006-
last_error = Some(format!("{label} not found"));
1007-
}
1008-
Err(err) => {
1009-
last_error = Some(format!("run {label}: {err}"));
1010-
}
1011-
}
965+
let output = e2fs_command("debugfs")?
966+
.arg("-w")
967+
.arg("-f")
968+
.arg(command_path)
969+
.arg(image_path)
970+
.output();
971+
match output {
972+
Ok(output) if output.status.success() => Ok(()),
973+
Ok(output) => Err(format!(
974+
"debugfs batch {} failed for {}: debugfs failed with status {}\nstdout: {}\nstderr: {}. Install e2fsprogs (debugfs) and retry",
975+
command_path.display(),
976+
image_path.display(),
977+
output.status,
978+
String::from_utf8_lossy(&output.stdout),
979+
String::from_utf8_lossy(&output.stderr)
980+
)),
981+
Err(error) => Err(format!(
982+
"debugfs batch {} failed for {}: {error}. Install e2fsprogs (debugfs) and retry",
983+
command_path.display(),
984+
image_path.display()
985+
)),
1012986
}
1013-
Err(format!(
1014-
"debugfs batch {} failed for {}: {}. Install e2fsprogs (debugfs) and retry",
1015-
command_path.display(),
1016-
image_path.display(),
1017-
last_error.unwrap_or_else(|| "debugfs not found".to_string())
1018-
))
1019987
}
1020988

1021989
fn run_debugfs(image_path: &Path, command: &str) -> Result<(), String> {
1022-
let mut last_error = None;
1023-
for candidate in e2fs_tool_candidates("debugfs") {
1024-
let label = candidate.display().to_string();
1025-
let output = Command::new(&candidate)
1026-
.arg("-w")
1027-
.arg("-R")
1028-
.arg(command)
1029-
.arg(image_path)
1030-
.output();
1031-
match output {
1032-
Ok(output) if output.status.success() => return Ok(()),
1033-
Ok(output) => {
1034-
last_error = Some(format!(
1035-
"{label} failed with status {}\nstdout: {}\nstderr: {}",
1036-
output.status,
1037-
String::from_utf8_lossy(&output.stdout),
1038-
String::from_utf8_lossy(&output.stderr)
1039-
));
1040-
}
1041-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1042-
last_error = Some(format!("{label} not found"));
1043-
}
1044-
Err(err) => {
1045-
last_error = Some(format!("run {label}: {err}"));
1046-
}
1047-
}
990+
let output = e2fs_command("debugfs")?
991+
.arg("-w")
992+
.arg("-R")
993+
.arg(command)
994+
.arg(image_path)
995+
.output();
996+
match output {
997+
Ok(output) if output.status.success() => Ok(()),
998+
Ok(output) => Err(format!(
999+
"debugfs command '{command}' failed for {}: debugfs failed with status {}\nstdout: {}\nstderr: {}. Install e2fsprogs (debugfs) and retry",
1000+
image_path.display(),
1001+
output.status,
1002+
String::from_utf8_lossy(&output.stdout),
1003+
String::from_utf8_lossy(&output.stderr)
1004+
)),
1005+
Err(error) => Err(format!(
1006+
"debugfs command '{command}' failed for {}: {error}. Install e2fsprogs (debugfs) and retry",
1007+
image_path.display()
1008+
)),
10481009
}
1049-
Err(format!(
1050-
"debugfs command '{command}' failed for {}: {}. Install e2fsprogs (debugfs) and retry",
1051-
image_path.display(),
1052-
last_error.unwrap_or_else(|| "debugfs not found".to_string())
1053-
))
10541010
}
10551011

1056-
/// Resolve the places an e2fsprogs binary may live, starting with `PATH`.
1057-
///
1058-
/// Distributions install `mke2fs`, `e2fsck`, and `debugfs` under `/usr/sbin`
1059-
/// or `/sbin`, which most Linux hosts leave out of a non-root user's `PATH`,
1060-
/// so those directories are probed explicitly. Homebrew keeps e2fsprogs
1061-
/// keg-only on macOS, so its prefixes are probed as well.
1062-
fn e2fs_tool_candidates(tool: &str) -> Vec<PathBuf> {
1063-
let mut candidates = vec![PathBuf::from(tool)];
1064-
for dir in ["/usr/local/sbin", "/usr/sbin", "/sbin"] {
1065-
candidates.push(Path::new(dir).join(tool));
1066-
}
1067-
for root in ["/opt/homebrew/opt/e2fsprogs", "/usr/local/opt/e2fsprogs"] {
1068-
candidates.push(Path::new(root).join("sbin").join(tool));
1069-
candidates.push(Path::new(root).join("bin").join(tool));
1070-
}
1071-
candidates
1012+
fn e2fs_command(tool: impl AsRef<OsStr>) -> Result<Command, String> {
1013+
let path = e2fs_search_path(std::env::var_os("PATH").as_deref())?;
1014+
let mut command = Command::new(tool);
1015+
command.env("PATH", path);
1016+
Ok(command)
1017+
}
1018+
1019+
fn e2fs_search_path(existing: Option<&OsStr>) -> Result<OsString, String> {
1020+
let mut paths = existing
1021+
.map(std::env::split_paths)
1022+
.into_iter()
1023+
.flatten()
1024+
.collect::<Vec<_>>();
1025+
1026+
#[cfg(target_os = "linux")]
1027+
paths.extend(["/usr/local/sbin", "/usr/sbin", "/sbin"].map(PathBuf::from));
1028+
1029+
#[cfg(target_os = "macos")]
1030+
paths.extend(
1031+
[
1032+
"/opt/homebrew/opt/e2fsprogs/sbin",
1033+
"/opt/homebrew/opt/e2fsprogs/bin",
1034+
"/usr/local/opt/e2fsprogs/sbin",
1035+
"/usr/local/opt/e2fsprogs/bin",
1036+
]
1037+
.map(PathBuf::from),
1038+
);
1039+
1040+
std::env::join_paths(paths).map_err(|error| format!("construct e2fsprogs PATH: {error}"))
10721041
}
10731042

10741043
fn temporary_injection_path(image_path: &Path) -> PathBuf {
@@ -1513,10 +1482,15 @@ mod tests {
15131482

15141483
#[test]
15151484
fn recover_rootfs_image_accepts_clean_ext4_image() {
1516-
if !e2fs_tool_candidates("e2fsck")
1517-
.iter()
1518-
.any(|candidate| Command::new(candidate).arg("-V").output().is_ok())
1519-
{
1485+
let available = e2fs_command("e2fsck")
1486+
.and_then(|mut command| {
1487+
command
1488+
.arg("-V")
1489+
.output()
1490+
.map_err(|error| error.to_string())
1491+
})
1492+
.is_ok();
1493+
if !available {
15201494
return;
15211495
}
15221496

@@ -1595,17 +1569,31 @@ mod tests {
15951569
}
15961570

15971571
#[test]
1598-
fn e2fs_tool_candidates_probe_path_then_system_sbin_directories() {
1599-
let candidates = e2fs_tool_candidates("mke2fs");
1600-
1601-
assert_eq!(candidates.first(), Some(&PathBuf::from("mke2fs")));
1602-
for expected in ["/usr/local/sbin/mke2fs", "/usr/sbin/mke2fs", "/sbin/mke2fs"] {
1603-
assert!(
1604-
candidates.contains(&PathBuf::from(expected)),
1605-
"expected {expected} in {candidates:?}"
1606-
);
1607-
}
1608-
assert!(candidates.contains(&PathBuf::from("/opt/homebrew/opt/e2fsprogs/sbin/mke2fs")));
1572+
fn e2fs_search_path_preserves_path_before_platform_fallbacks() {
1573+
let search_path = e2fs_search_path(Some(OsStr::new("/custom/bin")))
1574+
.expect("construct e2fsprogs search path");
1575+
let paths = std::env::split_paths(&search_path).collect::<Vec<_>>();
1576+
1577+
assert_eq!(paths.first(), Some(&PathBuf::from("/custom/bin")));
1578+
#[cfg(target_os = "linux")]
1579+
assert_eq!(
1580+
&paths[1..],
1581+
[
1582+
PathBuf::from("/usr/local/sbin"),
1583+
PathBuf::from("/usr/sbin"),
1584+
PathBuf::from("/sbin"),
1585+
]
1586+
);
1587+
#[cfg(target_os = "macos")]
1588+
assert_eq!(
1589+
&paths[1..],
1590+
[
1591+
PathBuf::from("/opt/homebrew/opt/e2fsprogs/sbin"),
1592+
PathBuf::from("/opt/homebrew/opt/e2fsprogs/bin"),
1593+
PathBuf::from("/usr/local/opt/e2fsprogs/sbin"),
1594+
PathBuf::from("/usr/local/opt/e2fsprogs/bin"),
1595+
]
1596+
);
16091597
}
16101598

16111599
#[test]

0 commit comments

Comments
 (0)