Enhance error handling in command execution and add diagnostics for missing executables
This commit is contained in:
+227
-12
@@ -1,10 +1,10 @@
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
|
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
use std::io::{ErrorKind, Write};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -2105,16 +2105,8 @@ fn run_target_command(
|
|||||||
cmd.env(k, v);
|
cmd.env(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = cmd.output().with_context(|| {
|
let output = cmd.output().map_err(|err| {
|
||||||
if is_root_fs(root) {
|
command_spawn_error(root, &resolved_program, &resolved_program_display, err)
|
||||||
format!("failed to execute `{}`", resolved_program_display)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"failed to execute `chroot {} {}`",
|
|
||||||
root.display(),
|
|
||||||
resolved_program_display
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
@@ -2153,6 +2145,199 @@ fn run_target_command(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_spawn_error(
|
||||||
|
root: &Path,
|
||||||
|
resolved_program: &Path,
|
||||||
|
resolved_program_display: &str,
|
||||||
|
err: std::io::Error,
|
||||||
|
) -> anyhow::Error {
|
||||||
|
let mut message = if is_root_fs(root) {
|
||||||
|
format!("failed to execute `{resolved_program_display}`: {err}")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"failed to execute `chroot {} {}`: {err}",
|
||||||
|
root.display(),
|
||||||
|
resolved_program_display
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if err.kind() == ErrorKind::NotFound {
|
||||||
|
message.push_str(&exec_not_found_diagnostics(root, resolved_program));
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow!(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exec_not_found_diagnostics(root: &Path, resolved_program: &Path) -> String {
|
||||||
|
let program_host_path = path_in_root(root, resolved_program);
|
||||||
|
let mut details = vec![
|
||||||
|
String::new(),
|
||||||
|
"exec diagnostics for ENOENT:".to_string(),
|
||||||
|
format!(" target program path: {}", resolved_program.display()),
|
||||||
|
format!(" host-visible path: {}", program_host_path.display()),
|
||||||
|
];
|
||||||
|
|
||||||
|
match fs::symlink_metadata(&program_host_path) {
|
||||||
|
Ok(meta) => {
|
||||||
|
details.push(format!(
|
||||||
|
" target exists: yes (mode {:o}, {} bytes)",
|
||||||
|
meta.permissions().mode() & 0o7777,
|
||||||
|
meta.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(stat_err) => {
|
||||||
|
details.push(format!(" target exists: no ({stat_err})"));
|
||||||
|
return details.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::read(&program_host_path) {
|
||||||
|
Ok(bytes) => {
|
||||||
|
if let Some(interpreter) = shebang_interpreter(&bytes) {
|
||||||
|
details.push(format!(" script interpreter: {interpreter}"));
|
||||||
|
details.push(format!(
|
||||||
|
" script interpreter exists: {}",
|
||||||
|
path_existence_description(root, Path::new(&interpreter))
|
||||||
|
));
|
||||||
|
} else if is_elf(&bytes) {
|
||||||
|
details.push(" file format: ELF".to_string());
|
||||||
|
match elf_interpreter(&bytes) {
|
||||||
|
Some(interpreter) => {
|
||||||
|
details.push(format!(" ELF interpreter: {interpreter}"));
|
||||||
|
details.push(format!(
|
||||||
|
" ELF interpreter exists: {}",
|
||||||
|
path_existence_description(root, Path::new(&interpreter))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => details.push(" ELF interpreter: none found".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details.push(" file format: not ELF and no shebang".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(read_err) => details.push(format!(" failed to read target: {read_err}")),
|
||||||
|
}
|
||||||
|
|
||||||
|
details.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_existence_description(root: &Path, target_path: &Path) -> String {
|
||||||
|
if !target_path.is_absolute() {
|
||||||
|
return "not checked (interpreter path is not absolute)".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let host_path = path_in_root(root, target_path);
|
||||||
|
match fs::symlink_metadata(&host_path) {
|
||||||
|
Ok(meta) => format!(
|
||||||
|
"yes at {} (mode {:o}, {} bytes)",
|
||||||
|
host_path.display(),
|
||||||
|
meta.permissions().mode() & 0o7777,
|
||||||
|
meta.len()
|
||||||
|
),
|
||||||
|
Err(err) => format!("no at {} ({err})", host_path.display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shebang_interpreter(bytes: &[u8]) -> Option<String> {
|
||||||
|
if !bytes.starts_with(b"#!") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_end = bytes
|
||||||
|
.iter()
|
||||||
|
.position(|b| *b == b'\n')
|
||||||
|
.unwrap_or(bytes.len());
|
||||||
|
let line = std::str::from_utf8(&bytes[2..line_end]).ok()?.trim();
|
||||||
|
let interpreter = line.split_whitespace().next()?;
|
||||||
|
Some(interpreter.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_elf(bytes: &[u8]) -> bool {
|
||||||
|
bytes.starts_with(b"\x7fELF")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elf_interpreter(bytes: &[u8]) -> Option<String> {
|
||||||
|
if !is_elf(bytes) || bytes.len() < 16 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let class = bytes[4];
|
||||||
|
let data = bytes[5];
|
||||||
|
let little_endian = match data {
|
||||||
|
1 => true,
|
||||||
|
2 => false,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (phoff, phentsize, phnum) = match class {
|
||||||
|
1 => (
|
||||||
|
read_u32(bytes, 28, little_endian)? as usize,
|
||||||
|
read_u16(bytes, 42, little_endian)? as usize,
|
||||||
|
read_u16(bytes, 44, little_endian)? as usize,
|
||||||
|
),
|
||||||
|
2 => (
|
||||||
|
read_u64(bytes, 32, little_endian)? as usize,
|
||||||
|
read_u16(bytes, 54, little_endian)? as usize,
|
||||||
|
read_u16(bytes, 56, little_endian)? as usize,
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
for index in 0..phnum {
|
||||||
|
let offset = phoff.checked_add(index.checked_mul(phentsize)?)?;
|
||||||
|
let header = bytes.get(offset..offset.checked_add(phentsize)?)?;
|
||||||
|
let p_type = read_u32(header, 0, little_endian)?;
|
||||||
|
if p_type != 3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (interp_offset, interp_size) = match class {
|
||||||
|
1 => (
|
||||||
|
read_u32(header, 4, little_endian)? as usize,
|
||||||
|
read_u32(header, 16, little_endian)? as usize,
|
||||||
|
),
|
||||||
|
2 => (
|
||||||
|
read_u64(header, 8, little_endian)? as usize,
|
||||||
|
read_u64(header, 32, little_endian)? as usize,
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let interp_end = interp_offset.checked_add(interp_size)?;
|
||||||
|
let raw = bytes.get(interp_offset..interp_end)?;
|
||||||
|
let nul = raw.iter().position(|b| *b == 0).unwrap_or(raw.len());
|
||||||
|
return std::str::from_utf8(&raw[..nul]).ok().map(ToOwned::to_owned);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u16(bytes: &[u8], offset: usize, little_endian: bool) -> Option<u16> {
|
||||||
|
let raw: [u8; 2] = bytes.get(offset..offset.checked_add(2)?)?.try_into().ok()?;
|
||||||
|
Some(if little_endian {
|
||||||
|
u16::from_le_bytes(raw)
|
||||||
|
} else {
|
||||||
|
u16::from_be_bytes(raw)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(bytes: &[u8], offset: usize, little_endian: bool) -> Option<u32> {
|
||||||
|
let raw: [u8; 4] = bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?;
|
||||||
|
Some(if little_endian {
|
||||||
|
u32::from_le_bytes(raw)
|
||||||
|
} else {
|
||||||
|
u32::from_be_bytes(raw)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u64(bytes: &[u8], offset: usize, little_endian: bool) -> Option<u64> {
|
||||||
|
let raw: [u8; 8] = bytes.get(offset..offset.checked_add(8)?)?.try_into().ok()?;
|
||||||
|
Some(if little_endian {
|
||||||
|
u64::from_le_bytes(raw)
|
||||||
|
} else {
|
||||||
|
u64::from_be_bytes(raw)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_target_program(root: &Path, program: &str) -> PathBuf {
|
fn resolve_target_program(root: &Path, program: &str) -> PathBuf {
|
||||||
let path_env = std::env::var_os("PATH");
|
let path_env = std::env::var_os("PATH");
|
||||||
resolve_target_program_with_path(root, program, path_env.as_deref())
|
resolve_target_program_with_path(root, program, path_env.as_deref())
|
||||||
@@ -2377,6 +2562,36 @@ CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
|
|||||||
assert_eq!(resolved, PathBuf::from("missing-tool"));
|
assert_eq!(resolved, PathBuf::from("missing-tool"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shebang_interpreter_reads_first_command() {
|
||||||
|
let script = b"#!/usr/bin/env sh\nexit 0\n";
|
||||||
|
|
||||||
|
assert_eq!(shebang_interpreter(script).as_deref(), Some("/usr/bin/env"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elf_interpreter_reads_64_bit_pt_interp() {
|
||||||
|
let interpreter = b"/lib/ld-musl-x86_64.so.1\0";
|
||||||
|
let mut elf = vec![0u8; 160];
|
||||||
|
|
||||||
|
elf[0..4].copy_from_slice(b"\x7fELF");
|
||||||
|
elf[4] = 2; // 64-bit
|
||||||
|
elf[5] = 1; // little-endian
|
||||||
|
elf[32..40].copy_from_slice(&64u64.to_le_bytes()); // e_phoff
|
||||||
|
elf[54..56].copy_from_slice(&56u16.to_le_bytes()); // e_phentsize
|
||||||
|
elf[56..58].copy_from_slice(&1u16.to_le_bytes()); // e_phnum
|
||||||
|
|
||||||
|
elf[64..68].copy_from_slice(&3u32.to_le_bytes()); // PT_INTERP
|
||||||
|
elf[72..80].copy_from_slice(&128u64.to_le_bytes()); // p_offset
|
||||||
|
elf[96..104].copy_from_slice(&(interpreter.len() as u64).to_le_bytes()); // p_filesz
|
||||||
|
elf[128..128 + interpreter.len()].copy_from_slice(interpreter);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
elf_interpreter(&elf).as_deref(),
|
||||||
|
Some("/lib/ld-musl-x86_64.so.1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn unique_test_dir(name: &str) -> PathBuf {
|
fn unique_test_dir(name: &str) -> PathBuf {
|
||||||
let nonce = SystemTime::now()
|
let nonce = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
Reference in New Issue
Block a user