use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use std::path::Path;
const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F'];
const EM_X86_64: u16 = 0x3E;
const EM_AARCH64: u16 = 0xB7;
const PT_INTERP: u32 = 3;
const ELF64_PHDR_SIZE: usize = 56;
pub fn validate_guest_bytes(data: &[u8], path: &Path) -> BoxliteResult<()> {
if data.len() < 64 {
return Err(BoxliteError::Internal(format!(
"Guest artifact {} is too small ({} bytes) — not a valid ELF",
path.display(),
data.len()
)));
}
if data[..4] != ELF_MAGIC {
return Err(BoxliteError::Internal(format!(
"Guest artifact {} is not a valid ELF file (bad magic bytes)",
path.display()
)));
}
if data[4] != 2 {
return Err(BoxliteError::Internal(format!(
"Guest artifact {} is not 64-bit ELF (class={})",
path.display(),
data[4]
)));
}
let e_machine = u16::from_le_bytes(data[18..20].try_into().unwrap());
let expected_machine = match std::env::consts::ARCH {
"x86_64" => EM_X86_64,
"aarch64" => EM_AARCH64,
arch => {
tracing::warn!(
arch,
"Cannot validate guest artifact architecture — unknown host arch"
);
return Ok(());
}
};
if e_machine != expected_machine {
let binary_arch = match e_machine {
EM_X86_64 => "x86_64",
EM_AARCH64 => "aarch64",
_ => "unknown",
};
return Err(BoxliteError::Internal(format!(
"Guest artifact {} is compiled for {} but host is {}\n\
Rebuild the guest artifact for the correct target",
path.display(),
binary_arch,
std::env::consts::ARCH,
)));
}
if has_pt_interp(data)? {
return Err(BoxliteError::Internal(format!(
"Guest artifact {} is dynamically linked (has a PT_INTERP program header) — \
the minimal rootfs has no dynamic loader; rebuild the artifact statically",
path.display()
)));
}
Ok(())
}
fn has_pt_interp(data: &[u8]) -> BoxliteResult<bool> {
if data.len() < 64 {
return Ok(false);
}
let e_phoff = u64::from_le_bytes(data[32..40].try_into().unwrap()) as usize;
let e_phentsize = u16::from_le_bytes(data[54..56].try_into().unwrap()) as usize;
let e_phnum = u16::from_le_bytes(data[56..58].try_into().unwrap()) as usize;
if e_phnum != 0 && e_phentsize != ELF64_PHDR_SIZE {
return Err(BoxliteError::Internal(format!(
"guest ELF program header entry size {e_phentsize} is invalid (expected {ELF64_PHDR_SIZE})"
)));
}
for i in 0..e_phnum {
let ph_offset = i
.checked_mul(e_phentsize)
.and_then(|offset| e_phoff.checked_add(offset))
.ok_or_else(|| {
BoxliteError::Internal("guest ELF program header offset overflow".into())
})?;
let ph_end = ph_offset.checked_add(e_phentsize).ok_or_else(|| {
BoxliteError::Internal("guest ELF program header offset overflow".into())
})?;
if ph_end > data.len() {
return Err(BoxliteError::Internal(
"guest ELF program header table runs past end of file".into(),
));
}
let p_type = u32::from_le_bytes(data[ph_offset..ph_offset + 4].try_into().unwrap());
if p_type == PT_INTERP {
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
fn guest_path() -> &'static Path {
Path::new("/runtime/boxlite-guest")
}
fn make_elf_header(machine: u16, add_interp: bool) -> Vec<u8> {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(&ELF_MAGIC);
data[4] = 2; data[5] = 1; data[6] = 1;
data[18..20].copy_from_slice(&machine.to_le_bytes());
if add_interp {
data[32..40].copy_from_slice(&64u64.to_le_bytes());
data[54..56].copy_from_slice(&56u16.to_le_bytes());
data[56..58].copy_from_slice(&1u16.to_le_bytes());
data[64..68].copy_from_slice(&PT_INTERP.to_le_bytes());
}
data
}
#[test]
fn test_valid_binary_matching_arch() {
let machine = match std::env::consts::ARCH {
"x86_64" => EM_X86_64,
"aarch64" => EM_AARCH64,
_ => return,
};
assert!(validate_guest_bytes(&make_elf_header(machine, false), guest_path()).is_ok());
}
#[test]
fn test_wrong_arch() {
let machine = match std::env::consts::ARCH {
"x86_64" => EM_AARCH64,
"aarch64" => EM_X86_64,
_ => return,
};
let err = validate_guest_bytes(&make_elf_header(machine, false), guest_path()).unwrap_err();
assert!(err.to_string().contains("compiled for"));
assert!(err.to_string().contains("but host is"));
}
#[test]
fn test_not_elf() {
let err = validate_guest_bytes(
b"not an elf file at all, but long enough to reach the magic check",
guest_path(),
)
.unwrap_err();
assert!(err.to_string().contains("not a valid ELF"));
}
#[test]
fn test_too_small() {
let err = validate_guest_bytes(b"tiny", guest_path()).unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_32bit_elf() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&ELF_MAGIC);
data[4] = 1;
let err = validate_guest_bytes(&data, guest_path()).unwrap_err();
assert!(err.to_string().contains("not 64-bit"));
}
#[test]
fn test_has_pt_interp_detection() {
assert!(has_pt_interp(&make_elf_header(EM_X86_64, true)).unwrap());
assert!(!has_pt_interp(&make_elf_header(EM_X86_64, false)).unwrap());
}
#[test]
fn test_dynamically_linked_binary_rejected() {
let machine = match std::env::consts::ARCH {
"x86_64" => EM_X86_64,
"aarch64" => EM_AARCH64,
_ => return,
};
let err = validate_guest_bytes(&make_elf_header(machine, true), guest_path()).unwrap_err();
assert!(err.to_string().contains("dynamically linked"));
}
#[test]
fn test_malformed_phoff_returns_error_not_panic() {
let mut data = make_elf_header(EM_X86_64, true);
data[32..40].copy_from_slice(&u64::MAX.to_le_bytes());
let err = has_pt_interp(&data).unwrap_err();
assert!(err.to_string().contains("program header"));
}
#[test]
fn test_zero_phentsize_with_phnum_rejected() {
let mut data = make_elf_header(EM_X86_64, false);
data[32..40].copy_from_slice(&64u64.to_le_bytes()); data[54..56].copy_from_slice(&0u16.to_le_bytes()); data[56..58].copy_from_slice(&1u16.to_le_bytes());
let err = has_pt_interp(&data).unwrap_err();
assert!(err.to_string().contains("program header entry size"));
}
#[test]
fn test_truncated_program_header_rejected() {
let mut data = make_elf_header(EM_X86_64, false);
data[32..40].copy_from_slice(&64u64.to_le_bytes()); data[54..56].copy_from_slice(&56u16.to_le_bytes()); data[56..58].copy_from_slice(&1u16.to_le_bytes()); data.truncate(68);
let err = has_pt_interp(&data).unwrap_err();
assert!(err.to_string().contains("runs past end of file"));
}
}