diff --git a/src/main.rs b/src/main.rs index 9fe46c1..61a60d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result, anyhow, bail}; use base64::Engine; use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; use std::collections::{BTreeMap, HashMap}; use std::ffi::OsStr; use std::fs; -use std::io::Write; +use std::io::{ErrorKind, Write}; #[cfg(unix)] use std::os::unix::fs::{PermissionsExt, symlink}; use std::path::{Path, PathBuf}; @@ -2105,16 +2105,8 @@ fn run_target_command( cmd.env(k, v); } - let output = cmd.output().with_context(|| { - if is_root_fs(root) { - format!("failed to execute `{}`", resolved_program_display) - } else { - format!( - "failed to execute `chroot {} {}`", - root.display(), - resolved_program_display - ) - } + let output = cmd.output().map_err(|err| { + command_spawn_error(root, &resolved_program, &resolved_program_display, err) })?; 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 { + 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 { + 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 { + 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 { + 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 { + 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 { let path_env = std::env::var_os("PATH"); 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")); } + #[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 { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH)