use anyhow::{Context, Result};
use std::collections::{BTreeSet, HashMap};
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
#[derive(Debug, Clone)]
pub(crate) struct SharedLibs {
pub found: Vec<(String, PathBuf)>,
pub missing: Vec<MissingLib>,
pub interpreter: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct MissingLib {
pub soname: String,
}
static LD_SO_CACHE: LazyLock<HashMap<String, PathBuf>> =
LazyLock::new(|| parse_ld_so_cache(Path::new("/etc/ld.so.cache")));
const LD_CACHE_MAGIC: &[u8; 20] = b"glibc-ld.so.cache1.1";
const LD_CACHE_HEADER_SIZE: usize = 48;
const LD_CACHE_ENTRY_SIZE: usize = 24;
fn parse_ld_so_cache(path: &Path) -> HashMap<String, PathBuf> {
let mut map = HashMap::new();
let data = match std::fs::read(path) {
Ok(d) => d,
Err(_) => return map,
};
let Some(magic_pos) = data
.windows(LD_CACHE_MAGIC.len())
.position(|w| w == LD_CACHE_MAGIC)
else {
return map;
};
let hdr = magic_pos;
if data.len() < hdr + LD_CACHE_HEADER_SIZE {
return map;
}
let nlibs = u32::from_le_bytes(data[hdr + 20..hdr + 24].try_into().unwrap()) as usize;
let min_size = hdr + LD_CACHE_HEADER_SIZE + nlibs * LD_CACHE_ENTRY_SIZE;
if data.len() < min_size {
return map;
}
for i in 0..nlibs {
let off = hdr + LD_CACHE_HEADER_SIZE + i * LD_CACHE_ENTRY_SIZE;
let key_off = u32::from_le_bytes(data[off + 4..off + 8].try_into().unwrap()) as usize;
let val_off = u32::from_le_bytes(data[off + 8..off + 12].try_into().unwrap()) as usize;
if key_off >= data.len() || val_off >= data.len() {
continue;
}
let soname = match read_cstr(&data, key_off) {
Some(s) => s,
None => continue,
};
let path_str = match read_cstr(&data, val_off) {
Some(s) => s,
None => continue,
};
if path_str.starts_with('/') {
let p = PathBuf::from(path_str);
if p.is_file() {
map.entry(soname.to_string()).or_insert(p);
}
}
}
map
}
fn read_cstr(data: &[u8], offset: usize) -> Option<&str> {
let end = data[offset..].iter().position(|&b| b == 0)?;
std::str::from_utf8(&data[offset..offset + end]).ok()
}
pub(crate) fn resolve_shared_libs(binary: &Path) -> Result<SharedLibs> {
resolve_shared_libs_inner(binary, &[])
}
fn resolve_shared_libs_with_extra_interp_hints(
binary: &Path,
extra_interp_hints: &[PathBuf],
) -> Result<SharedLibs> {
resolve_shared_libs_inner(binary, extra_interp_hints)
}
#[tracing::instrument(skip_all, fields(binary = %binary.display(), extra_hints = extra_interp_hints.len()))]
fn resolve_shared_libs_inner(binary: &Path, extra_interp_hints: &[PathBuf]) -> Result<SharedLibs> {
type LibCache = LazyLock<std::sync::Mutex<HashMap<(PathBuf, Vec<PathBuf>), SharedLibs>>>;
static CACHE: LibCache = LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
let canon = std::fs::canonicalize(binary).unwrap_or_else(|_| binary.to_path_buf());
let cache_key = (canon.clone(), extra_interp_hints.to_vec());
if let Ok(cache) = CACHE.lock()
&& let Some(cached) = cache.get(&cache_key)
{
return Ok(cached.clone());
}
let data =
std::fs::read(binary).with_context(|| format!("read binary: {}", binary.display()))?;
let elf = match goblin::elf::Elf::parse(&data) {
Ok(e) => e,
Err(_) => {
return Ok(SharedLibs {
found: vec![],
missing: vec![],
interpreter: None,
});
}
};
let interpreter = elf.interpreter.map(|s| s.to_string());
if elf.libraries.is_empty() && elf.dynamic.is_none() {
return Ok(SharedLibs {
found: vec![],
missing: vec![],
interpreter,
});
}
let root_needed: Vec<String> = elf.libraries.iter().map(|s| s.to_string()).collect();
let root_search = elf_search_paths(&elf, binary);
let mut interp_search_dirs: Vec<PathBuf> = match interpreter {
Some(ref interp) if !is_standard_interpreter(interp) => {
let interp_path = Path::new(interp);
let mut dirs = Vec::new();
if let Some(parent) = interp_path.parent() {
dirs.push(parent.to_path_buf());
if let Some(grandparent) = parent.parent() {
dirs.push(grandparent.join("lib"));
dirs.push(grandparent.join("lib64"));
}
}
dirs
}
_ => Vec::new(),
};
for hint in extra_interp_hints {
if !interp_search_dirs.contains(hint) {
interp_search_dirs.push(hint.clone());
}
}
use rayon::prelude::*;
let mut found: Vec<(String, PathBuf)> = Vec::new();
let mut missing: Vec<MissingLib> = Vec::new();
let mut visited = std::collections::HashSet::new();
let mut level: Vec<(String, ElfSearchPaths)> = root_needed
.iter()
.map(|s| (s.clone(), root_search.clone()))
.collect();
while !level.is_empty() {
let mut resolved: Vec<(String, PathBuf, PathBuf)> = Vec::new();
for (soname, search_paths) in &level {
if !visited.insert(soname.clone()) {
continue;
}
if let Some(host_path) = resolve_soname(soname, search_paths, &interp_search_dirs) {
let canonical =
std::fs::canonicalize(&host_path).unwrap_or_else(|_| host_path.clone());
let canon_str = canonical.to_string_lossy();
let canon_guest = canon_str
.strip_prefix('/')
.unwrap_or(&canon_str)
.to_string();
found.push((canon_guest.clone(), canonical.clone()));
let host_str = host_path.to_string_lossy();
let host_guest = host_str.strip_prefix('/').unwrap_or(&host_str).to_string();
if host_guest != canon_guest {
found.push((host_guest, canonical.clone()));
}
resolved.push((soname.clone(), host_path, canonical));
} else {
missing.push(MissingLib {
soname: soname.clone(),
});
}
}
let next_deps: Vec<(String, ElfSearchPaths)> = resolved
.par_iter()
.flat_map(|(_, _, canonical)| {
let Ok(lib_data) = std::fs::read(canonical) else {
return Vec::new();
};
let Ok(lib_elf) = goblin::elf::Elf::parse(&lib_data) else {
return Vec::new();
};
let lib_search = elf_search_paths(&lib_elf, canonical);
lib_elf
.libraries
.iter()
.map(|name| (name.to_string(), lib_search.clone()))
.collect::<Vec<_>>()
})
.collect();
level = next_deps
.into_iter()
.filter(|(soname, _)| !visited.contains(soname))
.collect();
}
let result = SharedLibs {
found,
missing,
interpreter,
};
if let Ok(mut cache) = CACHE.lock() {
cache.insert(cache_key, result.clone());
}
Ok(result)
}
#[derive(Debug, Clone, Default)]
struct ElfSearchPaths {
rpath: Vec<PathBuf>,
runpath: Vec<PathBuf>,
}
fn elf_search_paths(elf: &goblin::elf::Elf, binary: &Path) -> ElfSearchPaths {
let origin = binary
.parent()
.and_then(|p| std::fs::canonicalize(p).ok())
.unwrap_or_default();
let origin_str = origin.to_string_lossy();
let lib_str = if elf.is_64 { "lib64" } else { "lib" };
let platform_str = std::env::consts::ARCH;
let expand = |raw: &str| -> Vec<PathBuf> {
raw.split(':')
.filter(|s| !s.is_empty())
.map(|p| {
let expanded = p
.replace("$ORIGIN", &origin_str)
.replace("${ORIGIN}", &origin_str)
.replace("$LIB", lib_str)
.replace("${LIB}", lib_str)
.replace("$PLATFORM", platform_str)
.replace("${PLATFORM}", platform_str);
PathBuf::from(expanded)
})
.collect()
};
if !elf.runpaths.is_empty() {
return ElfSearchPaths {
rpath: Vec::new(),
runpath: expand(&elf.runpaths.join(":")),
};
}
if !elf.rpaths.is_empty() {
return ElfSearchPaths {
rpath: expand(&elf.rpaths.join(":")),
runpath: Vec::new(),
};
}
ElfSearchPaths::default()
}
const STANDARD_INTERPRETERS: &[&str] = &[
"/lib/ld-linux.so.2",
"/lib/ld-linux-aarch64.so.1",
"/lib/ld-linux-armhf.so.3",
"/lib64/ld-linux-x86-64.so.2",
"/lib/ld-musl-x86_64.so.1",
"/lib/ld-musl-aarch64.so.1",
"/libexec/ld-elf.so.1",
];
fn is_standard_interpreter(interp: &str) -> bool {
let interp_path = Path::new(interp);
if STANDARD_INTERPRETERS.contains(&interp) {
return true;
}
let Ok(canon) = std::fs::canonicalize(interp_path) else {
return false;
};
STANDARD_INTERPRETERS.iter().any(|std_interp| {
std::fs::canonicalize(std_interp).is_ok_and(|std_canon| std_canon == canon)
})
}
const DEFAULT_LIB_PATHS: &[&str] = &[
"/lib",
"/usr/lib",
"/lib64",
"/usr/lib64",
"/usr/local/lib",
"/usr/local/lib64",
"/lib/x86_64-linux-gnu",
"/usr/lib/x86_64-linux-gnu",
"/lib/aarch64-linux-gnu",
"/usr/lib/aarch64-linux-gnu",
];
static LD_LIBRARY_PATH_DIRS: LazyLock<Vec<PathBuf>> = LazyLock::new(|| {
std::env::var("LD_LIBRARY_PATH")
.unwrap_or_default()
.split(':')
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect()
});
fn resolve_soname(
soname: &str,
elf_paths: &ElfSearchPaths,
interp_hints: &[PathBuf],
) -> Option<PathBuf> {
for dir in &elf_paths.rpath {
let candidate = dir.join(soname);
if candidate.is_file() {
return Some(candidate);
}
}
for dir in LD_LIBRARY_PATH_DIRS.iter() {
let candidate = dir.join(soname);
if candidate.is_file() {
return Some(candidate);
}
}
for dir in &elf_paths.runpath {
let candidate = dir.join(soname);
if candidate.is_file() {
return Some(candidate);
}
}
for dir in interp_hints {
let candidate = dir.join(soname);
if candidate.is_file() {
return Some(candidate);
}
}
if let Some(cached_path) = LD_SO_CACHE.get(soname) {
return Some(cached_path.clone());
}
for dir in DEFAULT_LIB_PATHS {
let candidate = Path::new(dir).join(soname);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
const ELF_MAGIC: &[u8; 4] = b"\x7fELF";
fn is_elf(path: &Path) -> bool {
std::fs::File::open(path)
.and_then(|mut f| {
use std::io::Read;
let mut magic = [0u8; 4];
f.read_exact(&mut magic)?;
Ok(magic)
})
.is_ok_and(|m| m == *ELF_MAGIC)
}
fn write_entry(archive: &mut Vec<u8>, name: &str, data: &[u8], mode: u32) -> Result<()> {
let builder = cpio::newc::Builder::new(name).mode(mode).nlink(1);
let mut writer = builder.write(archive as &mut dyn Write, data.len() as u32);
writer
.write_all(data)
.with_context(|| format!("write cpio entry '{name}'"))?;
writer.finish().context("finish cpio entry")?;
Ok(())
}
fn write_symlink_entry(archive: &mut Vec<u8>, name: &str, target: &str) -> Result<()> {
let target_bytes = target.as_bytes();
let builder = cpio::newc::Builder::new(name).mode(0o120777).nlink(1);
let mut writer = builder.write(archive as &mut dyn Write, target_bytes.len() as u32);
writer
.write_all(target_bytes)
.with_context(|| format!("write cpio symlink '{name}' -> '{target}'"))?;
writer.finish().context("finish cpio symlink entry")?;
Ok(())
}
const DEBUG_SECTIONS: &[&[u8]] = &[
b".debug_info",
b".debug_abbrev",
b".debug_line",
b".debug_line_str",
b".debug_str",
b".debug_ranges",
b".debug_aranges",
b".debug_frame",
b".debug_loc",
b".debug_loclists",
b".debug_rnglists",
b".debug_str_offsets",
b".debug_addr",
b".debug_pubtypes",
b".debug_pubnames",
b".debug_types",
b".debug_macro",
b".debug_macinfo",
b".comment",
];
fn strip_debug(path: &Path) -> Result<Vec<u8>> {
let paths_to_try: Vec<&Path> = if is_deleted_self(path) {
vec![path, Path::new("/proc/self/exe")]
} else {
vec![path]
};
for src in &paths_to_try {
if let Ok(data) = std::fs::read(src) {
if let Ok(stripped) = strip_debug_sections(&data) {
return Ok(stripped);
}
return Ok(data);
}
}
std::fs::read(path).with_context(|| format!("read binary: {}", path.display()))
}
fn strip_debug_sections(data: &[u8]) -> std::result::Result<Vec<u8>, object::build::Error> {
crate::elf_strip::rewrite(data, |name| DEBUG_SECTIONS.contains(&name))
}
fn is_deleted_self(path: &Path) -> bool {
let proc_exe = Path::new("/proc/self/exe");
let Ok(target) = std::fs::read_link(proc_exe) else {
return false;
};
let target_str = target.to_string_lossy();
target_str.ends_with(" (deleted)")
&& target_str.trim_end_matches(" (deleted)") == path.to_string_lossy().as_ref()
}
fn register_parent_dirs(dirs: &mut BTreeSet<String>, guest_path: &str) {
let Some(parent) = Path::new(guest_path).parent() else {
return;
};
let mut dir = PathBuf::new();
for component in parent.components() {
dir.push(component);
dirs.insert(dir.to_string_lossy().to_string());
}
}
#[tracing::instrument(skip_all, fields(payload = %payload.display(), includes = include_files.len()))]
pub fn build_initramfs_base(
payload: &Path,
extra_binaries: &[(&str, &Path)],
include_files: &[(&str, &Path)],
busybox: bool,
) -> Result<Vec<u8>> {
let mut validated_includes: Vec<(&str, &Path, u32)> = Vec::with_capacity(include_files.len());
for (archive_path, host_path) in include_files {
if Path::new(archive_path)
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
anyhow::bail!("include_files archive path contains '..': {}", archive_path);
}
if archive_path.starts_with(".ktstr_") {
anyhow::bail!(
"include_files archive path must not start with '.ktstr_': {}",
archive_path
);
}
let meta = std::fs::metadata(host_path).with_context(|| {
format!(
"stat include file '{}': {}",
archive_path,
host_path.display()
)
})?;
if !meta.file_type().is_file() {
anyhow::bail!(
"include_files entry '{}' is not a regular file: {}",
archive_path,
host_path.display()
);
}
validated_includes.push((archive_path, host_path, meta.permissions().mode()));
}
let binary = {
let _s = tracing::debug_span!("strip_debug").entered();
strip_debug(payload).with_context(|| format!("strip/read binary: {}", payload.display()))?
};
let mut archive = Vec::new();
let mut dirs = BTreeSet::new();
let mut shared_libs: Vec<(String, PathBuf)> = Vec::new();
let mut all_binaries: Vec<&Path> = std::iter::once(payload)
.chain(extra_binaries.iter().map(|(_, p)| *p))
.collect();
let mut include_elf_paths: Vec<&Path> = Vec::new();
for (_, host_path) in include_files {
if is_elf(host_path) {
include_elf_paths.push(host_path);
all_binaries.push(host_path);
}
}
let _s_resolve = tracing::debug_span!("resolve_all_libs", count = all_binaries.len()).entered();
for path in &all_binaries {
let _s_one =
tracing::debug_span!("resolve_shared_libs", binary = %path.display()).entered();
let result = resolve_shared_libs(path)
.with_context(|| format!("resolve libs for {}", path.display()))?;
drop(_s_one);
if !result.missing.is_empty() && include_elf_paths.contains(path) {
let names: Vec<&str> = result.missing.iter().map(|m| m.soname.as_str()).collect();
anyhow::bail!(
"{}: missing shared libraries: {}",
path.display(),
names.join(", ")
);
}
tracing::debug!(
binary = %path.display(),
interpreter = ?result.interpreter,
is_include = include_elf_paths.contains(path),
"resolved interpreter for binary"
);
if let Some(ref interp) = result.interpreter {
let interp_path = Path::new(interp);
let is_standard = is_standard_interpreter(interp);
tracing::debug!(
interp = %interp_path.display(),
exists = interp_path.is_file(),
is_standard,
"interpreter details"
);
if interp_path.is_file() {
let canonical = std::fs::canonicalize(interp_path)
.unwrap_or_else(|_| interp_path.to_path_buf());
let canon_str = canonical.to_string_lossy();
let guest = canon_str
.strip_prefix('/')
.unwrap_or(&canon_str)
.to_string();
register_parent_dirs(&mut dirs, &guest);
tracing::debug!(
canonical_guest = %guest,
canonical_host = %canonical.display(),
"packing interpreter canonical path"
);
shared_libs.push((guest.clone(), canonical.clone()));
let orig_guest = interp.strip_prefix('/').unwrap_or(interp).to_string();
if orig_guest != guest {
tracing::debug!(
orig_guest = %orig_guest,
canonical_guest = %guest,
"packing interpreter original (non-canonical) path"
);
register_parent_dirs(&mut dirs, &orig_guest);
shared_libs.push((orig_guest, canonical));
} else {
tracing::debug!("interpreter original path matches canonical, no alias needed");
}
if !is_standard_interpreter(interp) {
let mut interp_hints: Vec<PathBuf> = Vec::new();
if let Some(parent) = interp_path.parent() {
interp_hints.push(parent.to_path_buf());
if let Some(grandparent) = parent.parent() {
interp_hints.push(grandparent.join("lib"));
interp_hints.push(grandparent.join("lib64"));
}
}
if let Ok(interp_result) =
resolve_shared_libs_with_extra_interp_hints(interp_path, &interp_hints)
{
for (g, h) in interp_result.found {
register_parent_dirs(&mut dirs, &g);
shared_libs.push((g, h));
}
}
}
}
}
for (guest_path, host_path) in result.found {
register_parent_dirs(&mut dirs, &guest_path);
shared_libs.push((guest_path, host_path));
}
}
let pre_dedup_count = shared_libs.len();
shared_libs.sort_by(|a, b| a.0.cmp(&b.0));
shared_libs.dedup_by(|a, b| a.0 == b.0);
tracing::debug!(
pre_dedup = pre_dedup_count,
post_dedup = shared_libs.len(),
removed = pre_dedup_count - shared_libs.len(),
"shared_libs dedup"
);
if busybox {
dirs.insert("bin".to_string());
}
for (archive_path, _, _) in &validated_includes {
register_parent_dirs(&mut dirs, archive_path);
}
for (name, _) in extra_binaries {
register_parent_dirs(&mut dirs, name);
}
drop(_s_resolve);
tracing::debug!(
shared_libs_count = shared_libs.len(),
dirs_count = dirs.len(),
dirs = ?dirs,
shared_libs_guests = ?shared_libs.iter().map(|(g, _)| g.as_str()).collect::<Vec<_>>(),
"pre-write archive contents"
);
let _s_write = tracing::debug_span!("write_cpio").entered();
for dir in &dirs {
write_entry(&mut archive, dir, &[], 0o40755)?;
}
write_entry(&mut archive, "init", &binary, 0o100755)?;
if busybox {
write_entry(&mut archive, "bin/busybox", crate::BUSYBOX, 0o100755)?;
}
for (name, path) in extra_binaries {
let data = strip_debug(path)
.with_context(|| format!("strip/read extra binary '{}': {}", name, path.display()))?;
write_entry(&mut archive, name, &data, 0o100755)?;
}
for (archive_path, host_path, mode) in &validated_includes {
let data = std::fs::read(host_path).with_context(|| {
format!(
"read include file '{}': {}",
archive_path,
host_path.display()
)
})?;
write_entry(&mut archive, archive_path, &data, *mode)?;
}
{
let mut written_files: HashMap<PathBuf, String> = HashMap::new();
for (guest_path, host_path) in &shared_libs {
let canonical = std::fs::canonicalize(host_path).unwrap_or_else(|_| host_path.clone());
if let Some(first_guest) = written_files.get(&canonical) {
let target = format!("/{first_guest}");
write_symlink_entry(&mut archive, guest_path, &target)?;
} else {
let data = std::fs::read(host_path).with_context(|| {
format!("read shared lib '{}': {}", guest_path, host_path.display())
})?;
write_entry(&mut archive, guest_path, &data, 0o100755)?;
written_files.insert(canonical, guest_path.clone());
}
}
}
write_entry(&mut archive, ".ktstr_init_ok", &[], 0o100644)?;
drop(_s_write);
Ok(archive)
}
#[derive(Default)]
pub struct SuffixParams<'a> {
pub args: &'a [String],
pub sched_args: &'a [String],
pub sched_enable: &'a [String],
pub sched_disable: &'a [String],
pub exec_cmd: Option<&'a str>,
}
pub fn build_suffix(base_len: usize, params: &SuffixParams<'_>) -> Result<Vec<u8>> {
let mut suffix = Vec::new();
let args_data = params.args.join("\n");
write_entry(&mut suffix, "args", args_data.as_bytes(), 0o100644)?;
if !params.sched_args.is_empty() {
let sched_args_data = params.sched_args.join("\n");
write_entry(
&mut suffix,
"sched_args",
sched_args_data.as_bytes(),
0o100644,
)?;
}
if !params.sched_enable.is_empty() {
let data = params.sched_enable.join("\n");
write_entry(&mut suffix, "sched_enable", data.as_bytes(), 0o100755)?;
}
if !params.sched_disable.is_empty() {
let data = params.sched_disable.join("\n");
write_entry(&mut suffix, "sched_disable", data.as_bytes(), 0o100755)?;
}
if let Some(cmd) = params.exec_cmd {
write_entry(&mut suffix, "exec_cmd", cmd.as_bytes(), 0o100644)?;
}
cpio::newc::trailer(&mut suffix as &mut dyn Write).context("write cpio trailer")?;
let total = base_len + suffix.len();
let pad = (512 - (total % 512)) % 512;
suffix.extend(std::iter::repeat_n(0u8, pad));
Ok(suffix)
}
#[cfg(target_arch = "x86_64")]
pub(crate) const SHM_ARCH_TAG: &str = "x86_64";
#[cfg(target_arch = "aarch64")]
pub(crate) const SHM_ARCH_TAG: &str = "aarch64";
pub(crate) fn shm_segment_name(content_hash: u64) -> String {
format!("/ktstr-base-{SHM_ARCH_TAG}-{content_hash:016x}")
}
pub(crate) struct MappedShm {
ptr: *const u8,
len: usize,
fd: std::os::fd::OwnedFd,
}
unsafe impl Send for MappedShm {}
unsafe impl Sync for MappedShm {}
impl AsRef<[u8]> for MappedShm {
fn as_ref(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
}
impl Drop for MappedShm {
fn drop(&mut self) {
unsafe {
libc::munmap(self.ptr as *mut libc::c_void, self.len);
}
let _ = rustix::fs::flock(&self.fd, rustix::fs::FlockOperation::Unlock);
}
}
pub(crate) fn shm_load_base(content_hash: u64) -> Option<MappedShm> {
use std::os::fd::AsRawFd;
let name = shm_segment_name(content_hash);
let fd = rustix::shm::open(
name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
)
.ok()?;
rustix::fs::flock(&fd, rustix::fs::FlockOperation::LockShared).ok()?;
let stat = rustix::fs::fstat(&fd).ok()?;
if stat.st_size <= 0 {
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
return None;
}
let len = stat.st_size as usize;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ,
libc::MAP_SHARED,
fd.as_raw_fd(),
0,
)
};
if ptr == libc::MAP_FAILED {
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
return None;
}
Some(MappedShm {
ptr: ptr as *const u8,
len,
fd,
})
}
fn shm_store(name: &str, data: &[u8]) -> Result<()> {
use std::os::fd::AsRawFd;
let fd = rustix::shm::open(
name,
rustix::shm::OFlags::CREATE | rustix::shm::OFlags::RDWR,
rustix::fs::Mode::from_raw_mode(0o644),
)
.map_err(|e| anyhow::anyhow!("shm_open: {e}"))?;
tracing::info!(
segment = name,
data_len = data.len(),
"shm_store: waiting for LOCK_EX"
);
rustix::fs::flock(&fd, rustix::fs::FlockOperation::LockExclusive)
.map_err(|e| anyhow::anyhow!("flock: {e}"))?;
let raw_fd = fd.as_raw_fd();
unsafe {
if libc::ftruncate(raw_fd, data.len() as libc::off_t) != 0 {
let err = std::io::Error::last_os_error();
if let Err(e) = rustix::shm::unlink(name) {
tracing::warn!(
err = %e,
segment = name,
"shm_unlink failed on ftruncate error path"
);
}
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
anyhow::bail!("ftruncate: {err}");
}
let ptr = libc::mmap(
std::ptr::null_mut(),
data.len(),
libc::PROT_WRITE,
libc::MAP_SHARED,
raw_fd,
0,
);
if ptr == libc::MAP_FAILED {
let err = std::io::Error::last_os_error();
if let Err(e) = rustix::shm::unlink(name) {
tracing::warn!(
err = %e,
segment = name,
"shm_unlink failed on mmap error path"
);
}
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
anyhow::bail!("mmap: {err}");
}
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr as *mut u8, data.len());
libc::munmap(ptr, data.len());
}
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
Ok(())
}
pub(crate) fn shm_store_base(content_hash: u64, data: &[u8]) -> Result<()> {
shm_store(&shm_segment_name(content_hash), data)
}
#[cfg(test)]
pub(crate) fn shm_unlink_base(content_hash: u64) {
let _ = rustix::shm::unlink(shm_segment_name(content_hash).as_str());
}
fn shm_lz4_segment_name(content_hash: u64) -> String {
format!("/ktstr-lz4-{SHM_ARCH_TAG}-{content_hash:016x}")
}
pub(crate) fn shm_open_lz4(content_hash: u64) -> Option<(std::os::fd::OwnedFd, usize)> {
let name = shm_lz4_segment_name(content_hash);
let fd = rustix::shm::open(
name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
)
.ok()?;
rustix::fs::flock(&fd, rustix::fs::FlockOperation::LockShared).ok()?;
let stat = rustix::fs::fstat(&fd).ok()?;
if stat.st_size <= 0 {
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
return None;
}
Some((fd, stat.st_size as usize))
}
pub(crate) fn shm_store_lz4(content_hash: u64, data: &[u8]) -> Result<()> {
shm_store(&shm_lz4_segment_name(content_hash), data)
}
pub(crate) struct CowOverlayGuard {
fd: std::os::fd::OwnedFd,
}
impl CowOverlayGuard {
fn new(fd: std::os::fd::OwnedFd) -> Self {
Self { fd }
}
}
impl Drop for CowOverlayGuard {
fn drop(&mut self) {
let _ = rustix::fs::flock(&self.fd, rustix::fs::FlockOperation::Unlock);
}
}
pub(crate) unsafe fn cow_overlay(
host_addr: *mut u8,
len: usize,
shm_fd: std::os::fd::OwnedFd,
) -> Option<CowOverlayGuard> {
use std::os::fd::AsRawFd;
let ptr = unsafe {
libc::mmap(
host_addr as *mut libc::c_void,
len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_FIXED | libc::MAP_POPULATE,
shm_fd.as_raw_fd(),
0,
)
};
if ptr == libc::MAP_FAILED {
let _ = rustix::fs::flock(&shm_fd, rustix::fs::FlockOperation::Unlock);
return None;
}
Some(CowOverlayGuard::new(shm_fd))
}
pub(crate) fn shm_close_fd(fd: std::os::fd::OwnedFd) {
let _ = rustix::fs::flock(&fd, rustix::fs::FlockOperation::Unlock);
}
pub fn load_initramfs_parts(
guest_mem: &vm_memory::GuestMemoryMmap,
parts: &[&[u8]],
load_addr: u64,
) -> Result<(u64, u32)> {
use vm_memory::{Bytes, GuestAddress};
let mut offset = 0u64;
for part in parts {
guest_mem
.write_slice(part, GuestAddress(load_addr + offset))
.context("write initramfs part to guest memory")?;
offset += part.len() as u64;
}
Ok((load_addr, offset as u32))
}
pub(crate) const LZ4_LEGACY_MAGIC: [u8; 4] = 0x184C2102u32.to_le_bytes();
const LZ4_CHUNK_SIZE: usize = 8 << 20;
pub(crate) fn lz4_legacy_compress(data: &[u8]) -> Vec<u8> {
use rayon::prelude::*;
let compressed_chunks: Vec<Vec<u8>> = data
.par_chunks(LZ4_CHUNK_SIZE)
.map(lz4_flex::block::compress)
.collect();
let total: usize = 4 + compressed_chunks.iter().map(|c| 4 + c.len()).sum::<usize>();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&LZ4_LEGACY_MAGIC);
for chunk in &compressed_chunks {
out.extend_from_slice(&(chunk.len() as u32).to_le_bytes());
out.extend_from_slice(chunk);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn build_suffix_args(
base_len: usize,
args: &[String],
sched_args: &[String],
) -> Result<Vec<u8>> {
build_suffix(
base_len,
&SuffixParams {
args,
sched_args,
..Default::default()
},
)
}
fn build_initramfs(
payload: &Path,
extra_binaries: &[(&str, &Path)],
args: &[String],
) -> Result<Vec<u8>> {
let base = build_initramfs_base(payload, extra_binaries, &[], false)?;
let suffix = build_suffix_args(base.len(), args, &[])?;
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
Ok(archive)
}
fn cpio_entry_names(archive: &[u8]) -> Vec<String> {
let mut names = Vec::new();
let mut remaining: &[u8] = archive;
while let Ok(reader) = cpio::newc::Reader::new(remaining) {
let name = reader.entry().name().to_string();
if reader.entry().is_trailer() {
break;
}
names.push(name);
remaining = reader.finish().unwrap();
}
names
}
fn cpio_entries(archive: &[u8]) -> Vec<(String, u32, u32, u32)> {
let mut entries = Vec::new();
let mut remaining: &[u8] = archive;
while let Ok(reader) = cpio::newc::Reader::new(remaining) {
if reader.entry().is_trailer() {
break;
}
let name = reader.entry().name().to_string();
let size = reader.entry().file_size();
let mode = reader.entry().mode();
let ino = reader.entry().ino();
entries.push((name, size, mode, ino));
remaining = reader.finish().unwrap();
}
entries
}
#[test]
fn cpio_header_format() {
let mut archive = Vec::new();
write_entry(&mut archive, "test", b"hello", 0o100644).unwrap();
assert_eq!(&archive[..6], b"070701");
}
#[test]
fn cpio_trailer() {
let mut archive = Vec::new();
write_entry(&mut archive, "test", b"data", 0o100755).unwrap();
cpio::newc::trailer(&mut archive as &mut dyn std::io::Write).unwrap();
let s = String::from_utf8_lossy(&archive);
assert!(s.contains("TRAILER!!!"));
}
#[test]
fn build_initramfs_has_init() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs(&exe, &[], &[]).unwrap();
let s = String::from_utf8_lossy(&initrd);
assert!(s.contains("init"), "should contain init entry");
assert!(s.contains("TRAILER!!!"));
}
#[test]
fn build_initramfs_base_is_valid_cpio() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs_base(&exe, &[], &[], false).unwrap();
assert_eq!(&initrd[..6], b"070701");
let full = build_initramfs(&exe, &[], &[]).unwrap();
assert!(initrd.len() <= full.len());
}
#[test]
fn build_initramfs_padded() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs(&exe, &[], &[]).unwrap();
assert_eq!(initrd.len() % 512, 0);
}
#[test]
fn initramfs_nonexistent_file() {
let result = build_initramfs(Path::new("/nonexistent"), &[], &[]);
assert!(result.is_err());
}
#[test]
fn initramfs_nonexistent_extra_binary() {
let exe = crate::resolve_current_exe().unwrap();
let result = build_initramfs(&exe, &[("bad", Path::new("/nonexistent"))], &[]);
assert!(result.is_err());
}
#[test]
fn initramfs_with_args() {
let exe = crate::resolve_current_exe().unwrap();
let args = vec!["run".into(), "--json".into(), "scenario".into()];
let initrd = build_initramfs(&exe, &[], &args).unwrap();
let s = String::from_utf8_lossy(&initrd);
assert!(s.contains("args"));
}
#[test]
fn initramfs_empty_args() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs(&exe, &[], &[]).unwrap();
assert_eq!(initrd.len() % 512, 0);
}
#[test]
fn suffix_adds_args_and_trailer() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let args = vec!["run".into(), "--json".into()];
let suffix = build_suffix_args(base.len(), &args, &[]).unwrap();
let s = String::from_utf8_lossy(&suffix);
assert!(s.contains("args"), "suffix should contain args entry");
assert!(s.contains("TRAILER!!!"), "suffix should contain trailer");
assert_eq!(
(base.len() + suffix.len()) % 512,
0,
"base+suffix should be 512-byte aligned"
);
}
#[test]
fn split_matches_monolithic() {
let exe = crate::resolve_current_exe().unwrap();
let args = vec!["run".into(), "--json".into(), "scenario".into()];
let monolithic = build_initramfs(&exe, &[], &args).unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix_args(base.len(), &args, &[]).unwrap();
let mut split = Vec::with_capacity(base.len() + suffix.len());
split.extend_from_slice(&base);
split.extend_from_slice(&suffix);
assert_eq!(
monolithic, split,
"split path should produce identical output"
);
}
#[test]
fn suffix_different_args_differ() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let a = build_suffix_args(base.len(), &["a".into()], &[]).unwrap();
let b = build_suffix_args(base.len(), &["b".into()], &[]).unwrap();
assert_ne!(a, b, "different args should produce different suffixes");
}
#[test]
fn suffix_empty_args() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix_args(base.len(), &[], &[]).unwrap();
assert_eq!((base.len() + suffix.len()) % 512, 0);
let s = String::from_utf8_lossy(&suffix);
assert!(s.contains("TRAILER!!!"));
}
#[test]
fn suffix_with_sched_enable() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let sched_enable = vec!["echo 1 > /sys/kernel/sched_ext/enable".to_string()];
let suffix = build_suffix(
base.len(),
&SuffixParams {
sched_enable: &sched_enable,
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
let entry = entries
.iter()
.find(|(name, ..)| name == "sched_enable")
.expect("sched_enable entry missing");
assert_eq!(
entry.1 as usize,
sched_enable[0].len(),
"sched_enable size should match joined content length",
);
assert_eq!(entry.2, 0o100755, "sched_enable must be executable");
}
#[test]
fn suffix_with_sched_disable() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let sched_disable = vec!["echo 0 > /sys/kernel/sched_ext/enable".to_string()];
let suffix = build_suffix(
base.len(),
&SuffixParams {
sched_disable: &sched_disable,
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
let entry = entries
.iter()
.find(|(name, ..)| name == "sched_disable")
.expect("sched_disable entry missing");
assert_eq!(entry.1 as usize, sched_disable[0].len());
assert_eq!(entry.2, 0o100755, "sched_disable must be executable");
}
#[test]
fn suffix_with_exec_cmd() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let cmd = "/usr/bin/stress-ng --cpu 1 --timeout 5s";
let suffix = build_suffix(
base.len(),
&SuffixParams {
exec_cmd: Some(cmd),
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
let entry = entries
.iter()
.find(|(name, ..)| name == "exec_cmd")
.expect("exec_cmd entry missing");
assert_eq!(entry.1 as usize, cmd.len());
assert_eq!(entry.2, 0o100644, "exec_cmd must be a plain data file");
}
#[test]
fn suffix_omits_empty_optional_entries() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix(base.len(), &SuffixParams::default()).unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let names = cpio_entry_names(&archive);
assert!(!names.iter().any(|n| n == "sched_enable"));
assert!(!names.iter().any(|n| n == "sched_disable"));
assert!(!names.iter().any(|n| n == "exec_cmd"));
}
#[test]
fn try_cow_overlay_rejects_cross_region_span() {
use vm_memory::{GuestAddress, GuestMemory};
let region_a_size: usize = 64 * 1024;
let region_b_size: usize = 64 * 1024;
let region_a_start: u64 = 0;
let region_b_start: u64 = 1 << 20; let mem = vm_memory::GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
assert!(
mem.get_slice(GuestAddress(region_a_start), region_a_size)
.is_ok(),
"full-region slice must succeed"
);
let overrun_start = region_a_start + (region_a_size as u64 / 2);
let overrun_len = region_a_size; assert!(
mem.get_slice(GuestAddress(overrun_start), overrun_len)
.is_err(),
"cross-boundary slice must fail"
);
let gap_addr = (region_a_start + region_a_size as u64) + 0x1000;
assert!(
mem.get_slice(GuestAddress(gap_addr), 4).is_err(),
"gap-start slice must fail"
);
}
#[test]
fn try_cow_overlay_preserves_adjacent_region_bytes() {
use vm_memory::{Bytes, GuestAddress, GuestMemory};
let region_a_size: usize = 64 * 1024;
let region_b_size: usize = 64 * 1024;
let region_a_start: u64 = 0;
let region_b_start: u64 = 1 << 20;
let mem = vm_memory::GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
let marker: Vec<u8> = (0..region_b_size).map(|i| (i & 0xff) as u8).collect();
mem.write_slice(&marker, GuestAddress(region_b_start))
.unwrap();
let overrun_load_addr = region_a_start;
let overrun_len = (region_b_start + region_b_size as u64) as usize;
assert!(
mem.get_slice(GuestAddress(overrun_load_addr), overrun_len)
.is_err(),
"oversized overlay must be rejected before MAP_FIXED"
);
let mut readback = vec![0u8; region_b_size];
mem.read_slice(&mut readback, GuestAddress(region_b_start))
.unwrap();
assert_eq!(
readback, marker,
"region B must be untouched when bounds check rejects cow_overlay"
);
}
#[test]
fn load_initramfs_parts_sequential() {
let part1 = vec![0xAAu8; 4096];
let part2 = vec![0xBBu8; 512];
let mem = vm_memory::GuestMemoryMmap::<()>::from_ranges(&[(
vm_memory::GuestAddress(0),
16 << 20,
)])
.unwrap();
let (addr, size) = load_initramfs_parts(&mem, &[&part1, &part2], 0x200000).unwrap();
assert_eq!(addr, 0x200000);
assert_eq!(size, 4608);
let mut buf = vec![0u8; 4608];
use vm_memory::{Bytes, GuestAddress};
mem.read_slice(&mut buf, GuestAddress(0x200000)).unwrap();
assert_eq!(&buf[..4096], &part1[..]);
assert_eq!(&buf[4096..], &part2[..]);
}
#[test]
fn resolve_shared_libs_nonexistent_returns_error() {
let result = resolve_shared_libs(Path::new("/nonexistent/binary"));
assert!(result.is_err());
}
#[test]
fn resolve_shared_libs_non_elf_returns_empty() {
let tmp = std::env::temp_dir().join("ktstr-test-resolve-nonelf");
std::fs::write(&tmp, b"not an elf").unwrap();
let result = resolve_shared_libs(&tmp).unwrap();
assert!(result.found.is_empty());
assert!(result.missing.is_empty());
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn resolve_shared_libs_dynamic_binary() {
let sh = Path::new("/bin/sh");
if sh.exists() {
let shared = resolve_shared_libs(sh).unwrap();
if !shared.found.is_empty() {
assert!(
shared.found.iter().any(|(g, _)| g.contains("libc")),
"dynamic binary should depend on libc: {:?}",
shared.found
);
for (g, _) in &shared.found {
assert!(!g.starts_with('/'), "guest path should be relative: {g}");
}
}
}
}
#[test]
fn elf_dynamic_needed_extracts_sonames() {
let sh = Path::new("/bin/sh");
if !sh.exists() || !is_elf(sh) {
skip!("/bin/sh not ELF");
}
let data = std::fs::read(sh).unwrap();
let elf = goblin::elf::Elf::parse(&data).unwrap();
let needed: Vec<&str> = elf.libraries.clone();
assert!(
needed.iter().any(|n| n.contains("libc")),
"/bin/sh should need libc: {:?}",
needed
);
}
#[test]
fn resolve_soname_finds_libc() {
let result = resolve_soname("libc.so.6", &ElfSearchPaths::default(), &[]);
assert!(
result.is_some(),
"should resolve libc.so.6 via default paths"
);
assert!(result.unwrap().is_file());
}
#[test]
fn resolve_soname_rpath_beats_runpath_when_both_present() {
let tmp = tempfile::TempDir::new().unwrap();
let rpath_dir = tmp.path().join("rpath");
let runpath_dir = tmp.path().join("runpath");
std::fs::create_dir_all(&rpath_dir).unwrap();
std::fs::create_dir_all(&runpath_dir).unwrap();
let soname = "libktstrfake-rpath-beats-runpath.so.1";
std::fs::write(rpath_dir.join(soname), b"rpath-copy").unwrap();
std::fs::write(runpath_dir.join(soname), b"runpath-copy").unwrap();
let paths = ElfSearchPaths {
rpath: vec![rpath_dir.clone()],
runpath: vec![runpath_dir.clone()],
};
let got = resolve_soname(soname, &paths, &[]).expect("should resolve");
assert_eq!(
got,
rpath_dir.join(soname),
"DT_RPATH must be preferred over DT_RUNPATH when both are \
populated (the LD_LIBRARY_PATH step separates them)"
);
}
#[test]
fn resolve_soname_runpath_beats_interp_hints() {
let tmp = tempfile::TempDir::new().unwrap();
let runpath_dir = tmp.path().join("runpath");
let interp_dir = tmp.path().join("interp");
std::fs::create_dir_all(&runpath_dir).unwrap();
std::fs::create_dir_all(&interp_dir).unwrap();
let soname = "libktstrfake-runpath-beats-interp.so.1";
std::fs::write(runpath_dir.join(soname), b"runpath-copy").unwrap();
std::fs::write(interp_dir.join(soname), b"interp-copy").unwrap();
let paths = ElfSearchPaths {
rpath: Vec::new(),
runpath: vec![runpath_dir.clone()],
};
let got = resolve_soname(soname, &paths, std::slice::from_ref(&interp_dir))
.expect("should resolve");
assert_eq!(
got,
runpath_dir.join(soname),
"DT_RUNPATH must be searched before interp-relative hints"
);
}
#[test]
fn resolve_soname_rpath_only_wins_when_runpath_empty() {
let tmp = tempfile::TempDir::new().unwrap();
let rpath_dir = tmp.path().join("rpath-legacy");
let interp_dir = tmp.path().join("interp");
std::fs::create_dir_all(&rpath_dir).unwrap();
std::fs::create_dir_all(&interp_dir).unwrap();
let soname = "libktstrfake-rpath-legacy.so.1";
std::fs::write(rpath_dir.join(soname), b"rpath-copy").unwrap();
std::fs::write(interp_dir.join(soname), b"interp-copy").unwrap();
let paths = ElfSearchPaths {
rpath: vec![rpath_dir.clone()],
runpath: Vec::new(),
};
let got = resolve_soname(soname, &paths, std::slice::from_ref(&interp_dir))
.expect("should resolve");
assert_eq!(
got,
rpath_dir.join(soname),
"legacy binary with DT_RPATH (no DT_RUNPATH) must resolve \
via DT_RPATH, not interp hints"
);
}
#[test]
fn suffix_with_sched_args() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let sched_args = vec!["--enable-borrow".into(), "--llc".into()];
let suffix = build_suffix_args(base.len(), &[], &sched_args).unwrap();
let s = String::from_utf8_lossy(&suffix);
assert!(
s.contains("sched_args"),
"suffix should contain sched_args entry"
);
assert!(s.contains("TRAILER!!!"));
assert_eq!((base.len() + suffix.len()) % 512, 0);
}
#[test]
fn suffix_without_sched_args_omits_entry() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix_args(base.len(), &[], &[]).unwrap();
let s = String::from_utf8_lossy(&suffix);
assert!(
!s.contains("sched_args"),
"empty sched_args should not produce entry"
);
}
#[test]
fn shm_segment_name_format() {
let name = shm_segment_name(0xDEADBEEF);
assert!(name.starts_with("/ktstr-base-"));
assert!(name.contains("deadbeef"));
}
#[test]
fn is_deleted_self_returns_false_for_nonexistent() {
assert!(!is_deleted_self(Path::new("/nonexistent/binary")));
}
#[test]
fn is_deleted_self_returns_false_for_current() {
let exe = crate::resolve_current_exe().unwrap();
assert!(!is_deleted_self(&exe));
}
#[test]
fn shm_store_load_unlink_roundtrip() {
let hash = 0xABCD_EF01_2345_6789u64;
let data = vec![0x42u8; 1024];
shm_store_base(hash, &data).unwrap();
let loaded = shm_load_base(hash);
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().as_ref(), &data[..]);
shm_unlink_base(hash);
assert!(shm_load_base(hash).is_none());
}
#[test]
fn shm_load_nonexistent_returns_none() {
let hash = 0xFFFF_FFFF_FFFF_FFFFu64;
shm_unlink_base(hash); assert!(shm_load_base(hash).is_none());
}
#[test]
fn shm_store_last_writer_wins_even_with_size_change() {
let hash = 0x1234_5678_9ABC_DEF0u64;
let d1 = vec![0x11u8; 64];
let d2 = vec![0x22u8; 128];
shm_store_base(hash, &d1).unwrap();
shm_store_base(hash, &d2).unwrap();
let loaded = shm_load_base(hash);
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().as_ref(), &d2[..]);
shm_unlink_base(hash);
}
#[test]
fn shm_segment_name_unique_per_hash() {
let n1 = shm_segment_name(0);
let n2 = shm_segment_name(1);
assert_ne!(n1, n2);
assert!(n1.starts_with("/ktstr-base-"));
assert!(n2.starts_with("/ktstr-base-"));
}
#[test]
fn shm_unlink_nonexistent_is_noop() {
shm_unlink_base(0xDEAD_DEAD_DEAD_DEADu64);
}
#[test]
fn mapped_shm_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<MappedShm>();
}
#[test]
fn shm_load_base_holds_lock_until_drop() {
let hash = 0xD0D0_BEEF_F00D_BA5Eu64;
shm_unlink_base(hash); shm_store_base(hash, &vec![0x55u8; 256]).unwrap();
let loaded = shm_load_base(hash).expect("load must succeed");
let name = shm_segment_name(hash);
let fd2 = rustix::shm::open(
name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
)
.expect("second shm_open must succeed");
let err = rustix::fs::flock(&fd2, rustix::fs::FlockOperation::NonBlockingLockExclusive);
assert!(
matches!(err, Err(e) if e == rustix::io::Errno::WOULDBLOCK),
"LOCK_EX|LOCK_NB must be blocked by the live reader's LOCK_SH (got {err:?})",
);
drop(fd2);
drop(loaded);
let fd3 = rustix::shm::open(
name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
)
.expect("third shm_open must succeed");
rustix::fs::flock(&fd3, rustix::fs::FlockOperation::NonBlockingLockExclusive)
.expect("LOCK_EX|LOCK_NB must succeed after the MappedShm is dropped");
rustix::fs::flock(&fd3, rustix::fs::FlockOperation::Unlock).ok();
drop(fd3);
shm_unlink_base(hash);
}
#[test]
fn strip_debug_current_exe() {
let exe = crate::resolve_current_exe().unwrap();
let data = strip_debug(&exe).unwrap();
assert!(!data.is_empty());
assert_eq!(&data[..4], b"\x7fELF");
}
#[test]
fn strip_debug_nonexistent_fails() {
let result = strip_debug(Path::new("/nonexistent/binary"));
assert!(result.is_err());
}
#[test]
fn build_initramfs_base_contains_init() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(s.contains("init"), "base should contain init entry");
}
#[test]
fn build_initramfs_base_includes_extra_shared_libs() {
let exe = crate::resolve_current_exe().unwrap();
let sched = crate::test_support::require_binary("scx-ktstr");
let extras: Vec<(&str, &Path)> = vec![("scheduler", sched.as_path())];
let base = build_initramfs_base(&exe, &extras, &[], false).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(
s.contains("lib64/libelf"),
"initramfs with scx-ktstr extra should contain libelf; \
resolved libs: {:?}",
resolve_shared_libs(sched.as_path()).unwrap().found
);
}
#[test]
fn load_initramfs_to_memory() {
let data = vec![0xAA; 4096];
let mem = vm_memory::GuestMemoryMmap::<()>::from_ranges(&[(
vm_memory::GuestAddress(0),
16 << 20,
)])
.unwrap();
let (addr, size) = load_initramfs_parts(&mem, &[&data], 0x200000).unwrap();
assert_eq!(addr, 0x200000);
assert_eq!(size, 4096);
let mut buf = vec![0u8; 4096];
use vm_memory::{Bytes, GuestAddress};
mem.read_slice(&mut buf, GuestAddress(0x200000)).unwrap();
assert_eq!(buf, data);
}
#[test]
fn busybox_with_include_files() {
let exe = crate::resolve_current_exe().unwrap();
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("included");
std::fs::write(&tmp, b"hello").unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/test.txt", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let names = cpio_entry_names(&base);
assert!(
names.iter().any(|n| n == "bin/busybox"),
"busybox=true should have bin/busybox entry: {:?}",
names
);
}
#[test]
fn include_files_no_busybox_when_empty() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let names = cpio_entry_names(&base);
assert!(
!names.iter().any(|n| n == "bin/busybox"),
"busybox=false should not have bin/busybox entry: {:?}",
names
);
}
#[test]
fn include_files_preserves_mode() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("script");
std::fs::write(&tmp, b"script content").unwrap();
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o100755)).unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/run.sh", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(
s.contains("include-files/run.sh"),
"include path should appear in cpio"
);
}
#[test]
fn include_files_elf_gets_shared_libs() {
let sh = Path::new("/bin/sh");
if !sh.exists() {
skip!("/bin/sh not found");
}
if !is_elf(sh) {
skip!("/bin/sh is not ELF");
}
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/sh", sh)];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
let shared = resolve_shared_libs(sh).unwrap();
if !shared.found.is_empty() {
assert!(
shared.found.iter().any(|(g, _)| s.contains(g.as_str())),
"include ELF shared libs should appear in archive: {:?}",
shared.found
);
}
}
#[test]
fn include_files_non_elf_no_shared_libs() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("hello.sh");
std::fs::write(&tmp, b"#!/bin/sh\necho hello\n").unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/hello.sh", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(s.contains("include-files/hello.sh"));
}
#[test]
fn include_files_adds_directory_entries() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("file.txt");
std::fs::write(&tmp, b"data").unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> =
vec![("include-files/subdir/nested/file.txt", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(s.contains("include-files"), "should have include-files dir");
assert!(
s.contains("include-files/subdir"),
"should have subdir entry"
);
assert!(
s.contains("include-files/subdir/nested"),
"should have nested subdir entry"
);
assert!(s.contains("bin"), "should have bin dir for busybox");
}
#[test]
fn is_elf_detects_elf_binary() {
let exe = crate::resolve_current_exe().unwrap();
assert!(is_elf(&exe), "test binary should be ELF");
}
#[test]
fn is_elf_rejects_non_elf() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("not-elf");
std::fs::write(&tmp, b"not an elf file").unwrap();
assert!(!is_elf(&tmp));
}
#[test]
fn is_elf_rejects_short_file() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("short-elf");
std::fs::write(&tmp, b"ab").unwrap();
assert!(!is_elf(&tmp));
}
#[test]
fn is_elf_nonexistent_returns_false() {
assert!(!is_elf(Path::new("/nonexistent/file")));
}
#[test]
fn include_files_rejects_path_traversal() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("payload");
std::fs::write(&tmp, b"data").unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/../etc/passwd", tmp.as_path())];
let result = build_initramfs_base(&exe, &[], &includes, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains(".."),
"error should mention path traversal: {err}"
);
}
#[test]
fn include_files_rejects_fifo() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let fifo_path = tmp_dir.path().join("fifo");
let c_path = std::ffi::CString::new(fifo_path.to_str().unwrap()).unwrap();
let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
assert_eq!(
rc,
0,
"ktstr: mkfifo({}) failed -- test infrastructure broken",
fifo_path.display(),
);
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/pipe", fifo_path.as_path())];
let result = build_initramfs_base(&exe, &[], &includes, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a regular file"),
"error should reject FIFO: {err}"
);
}
#[test]
fn include_files_rejects_directory() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let dir_path = tmp_dir.path().join("mydir");
std::fs::create_dir(&dir_path).unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/mydir", dir_path.as_path())];
let result = build_initramfs_base(&exe, &[], &includes, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a regular file"),
"error should reject directory: {err}"
);
}
#[test]
fn busybox_independent_of_include_files() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], true).unwrap();
let names = cpio_entry_names(&base);
assert!(
names.iter().any(|n| n == "bin/busybox"),
"busybox=true should have bin/busybox entry even without includes: {:?}",
names
);
}
#[test]
fn parse_ld_so_cache_finds_libc() {
let cache = parse_ld_so_cache(Path::new("/etc/ld.so.cache"));
assert!(
cache.contains_key("libc.so.6"),
"ld.so.cache should contain libc.so.6: found {} entries",
cache.len(),
);
let path = &cache["libc.so.6"];
assert!(
path.is_file(),
"cached libc path should exist: {}",
path.display()
);
}
#[test]
fn parse_ld_so_cache_nonexistent_returns_empty() {
let cache = parse_ld_so_cache(Path::new("/nonexistent/ld.so.cache"));
assert!(cache.is_empty());
}
#[test]
fn parse_ld_so_cache_bad_magic_returns_empty() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("ldcache");
std::fs::write(&tmp, b"not a valid cache file").unwrap();
let cache = parse_ld_so_cache(&tmp);
assert!(cache.is_empty());
}
#[test]
fn parse_ld_so_cache_truncated_returns_empty() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("ldcache");
let mut data = LD_CACHE_MAGIC.to_vec();
data.extend_from_slice(&[0u8; 10]); std::fs::write(&tmp, &data).unwrap();
let cache = parse_ld_so_cache(&tmp);
assert!(cache.is_empty());
}
#[test]
fn ld_so_cache_consistent_with_resolve_soname() {
let result = resolve_soname("libc.so.6", &ElfSearchPaths::default(), &[]);
assert!(
result.is_some(),
"resolve_soname should find libc.so.6 (cache or paths)"
);
assert!(result.unwrap().is_file());
}
#[test]
fn no_duplicate_cpio_entries() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let entries = cpio_entries(&base);
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for (name, size, mode, ino) in &entries {
if !seen.insert(name.clone()) {
duplicates.push((name.clone(), *size, *mode, *ino));
}
}
assert!(
duplicates.is_empty(),
"archive contains duplicate entries: {:?}",
duplicates
);
}
#[test]
fn no_duplicate_entries_with_include_files() {
let exe = crate::resolve_current_exe().unwrap();
let tmp_dir_guard = tempfile::TempDir::new().unwrap();
let tmp_dir = tmp_dir_guard.path();
let lib_data = vec![0xCCu8; 4096];
let f1 = tmp_dir.join("libcustom1.so");
let f2 = tmp_dir.join("libcustom2.so");
let f3 = tmp_dir.join("libcustom3.so");
std::fs::write(&f1, &lib_data).unwrap();
std::fs::write(&f2, &lib_data).unwrap();
std::fs::write(&f3, &lib_data).unwrap();
let includes: Vec<(&str, &Path)> = vec![
("usr/local/custom/platform/lib/libcustom1.so", f1.as_path()),
("usr/local/custom/platform/lib/libcustom2.so", f2.as_path()),
("usr/local/custom/platform/lib/libcustom3.so", f3.as_path()),
];
let base = build_initramfs_base(&exe, &[], &includes, false).unwrap();
let entries = cpio_entries(&base);
let entry_names: Vec<&str> = entries.iter().map(|(n, _, _, _)| n.as_str()).collect();
for (archive_path, _) in &includes {
assert!(
entry_names.contains(archive_path),
"missing include file entry '{}'; archive entries: {:?}",
archive_path,
entry_names
);
}
for (archive_path, _) in &includes {
let entry = entries.iter().find(|(n, _, _, _)| n == archive_path);
assert!(
entry.is_some_and(|(_, size, _, _)| *size == lib_data.len() as u32),
"include file '{}' has wrong size: {:?}",
archive_path,
entry
);
}
assert!(entry_names.contains(&"usr"), "missing 'usr' dir entry");
assert!(
entry_names.contains(&"usr/local"),
"missing 'usr/local' dir entry"
);
assert!(
entry_names.contains(&"usr/local/custom"),
"missing 'usr/local/custom' dir entry"
);
assert!(
entry_names.contains(&"usr/local/custom/platform"),
"missing 'usr/local/custom/platform' dir entry"
);
assert!(
entry_names.contains(&"usr/local/custom/platform/lib"),
"missing 'usr/local/custom/platform/lib' dir entry"
);
let dir_pos = entries
.iter()
.position(|(n, _, _, _)| n == "usr/local/custom/platform/lib")
.unwrap();
for (archive_path, _) in &includes {
let file_pos = entries
.iter()
.position(|(n, _, _, _)| n == *archive_path)
.unwrap();
assert!(
dir_pos < file_pos,
"directory entry must precede file '{}': dir at {}, file at {}",
archive_path,
dir_pos,
file_pos
);
}
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for (name, _, _, _) in &entries {
if !seen.insert(name.clone()) {
duplicates.push(name.clone());
}
}
assert!(
duplicates.is_empty(),
"duplicate entries in archive: {:?}",
duplicates
);
}
#[test]
fn include_elf_shared_libs_all_present_in_archive() {
let sh_path = Path::new("/bin/sh");
let sh_resolved = std::fs::canonicalize(sh_path).unwrap_or_else(|_| sh_path.to_path_buf());
let sh = sh_resolved.as_path();
if !sh.exists() || !is_elf(sh) {
skip!("/bin/sh not available or not ELF");
}
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/sh", sh)];
let base = build_initramfs_base(&exe, &[], &includes, false).unwrap();
let entries = cpio_entries(&base);
let entry_map: std::collections::HashMap<&str, (u32, u32, u32)> = entries
.iter()
.map(|(n, s, m, i)| (n.as_str(), (*s, *m, *i)))
.collect();
let shared = resolve_shared_libs(sh).unwrap();
for (guest_path, _host_path) in &shared.found {
assert!(
entry_map.contains_key(guest_path.as_str()),
"shared lib '{}' missing from archive; entries: {:?}",
guest_path,
entries
.iter()
.map(|(n, _, _, _)| n.as_str())
.collect::<Vec<_>>()
);
let (size, _, _) = entry_map[guest_path.as_str()];
assert!(
size > 0,
"shared lib '{}' has zero size in archive",
guest_path
);
}
assert!(
entry_map.contains_key("include-files/sh"),
"include file itself missing from archive"
);
}
#[test]
fn all_inode_zero_entries_have_nlink_one() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let mut remaining: &[u8] = base.as_slice();
while let Ok(reader) = cpio::newc::Reader::new(remaining) {
if reader.entry().is_trailer() {
break;
}
let name = reader.entry().name().to_string();
let ino = reader.entry().ino();
let nlink = reader.entry().nlink();
assert_eq!(
ino, 0,
"entry '{}' has non-zero inode {}: risk of kernel hardlink confusion",
name, ino
);
assert_eq!(
nlink, 1,
"entry '{}' has nlink {}: kernel only hardlinks when nlink >= 2",
name, nlink
);
remaining = reader.finish().unwrap();
}
}
#[test]
fn lz4_legacy_compress_format() {
let data = vec![0xAAu8; 4096];
let compressed = lz4_legacy_compress(&data);
assert_eq!(
&compressed[..4],
&LZ4_LEGACY_MAGIC,
"output must start with LZ4 legacy magic 0x184C2102"
);
let chunk_size = u32::from_le_bytes(compressed[4..8].try_into().unwrap()) as usize;
assert!(
chunk_size > 0 && chunk_size < data.len(),
"compressed chunk should be non-empty and smaller than input: {}",
chunk_size
);
let decompressed = lz4_flex::block::decompress(&compressed[8..8 + chunk_size], data.len())
.expect("lz4 block decompress failed");
assert_eq!(decompressed, data);
}
#[test]
fn lz4_legacy_compress_large_input_splits_chunks() {
let data = vec![0xBBu8; LZ4_CHUNK_SIZE + 1024];
let compressed = lz4_legacy_compress(&data);
assert_eq!(&compressed[..4], &LZ4_LEGACY_MAGIC);
let mut pos = 4;
let mut chunk_count = 0;
let mut total_decompressed = Vec::new();
while pos + 4 <= compressed.len() {
let chunk_size =
u32::from_le_bytes(compressed[pos..pos + 4].try_into().unwrap()) as usize;
if chunk_size == 0 {
break;
}
pos += 4;
let remaining_uncompressed = data.len() - total_decompressed.len();
let expected_chunk_len = remaining_uncompressed.min(LZ4_CHUNK_SIZE);
let decompressed =
lz4_flex::block::decompress(&compressed[pos..pos + chunk_size], expected_chunk_len)
.expect("lz4 block decompress failed");
total_decompressed.extend_from_slice(&decompressed);
pos += chunk_size;
chunk_count += 1;
}
assert!(
chunk_count >= 2,
"input > 8MB should produce >= 2 chunks, got {}",
chunk_count
);
assert_eq!(total_decompressed, data);
}
#[test]
fn lz4_legacy_compress_empty_input() {
let compressed = lz4_legacy_compress(&[]);
assert_eq!(compressed, LZ4_LEGACY_MAGIC);
}
fn build_synthetic_cpio(total_size: usize) -> Vec<u8> {
let mut archive = Vec::new();
write_entry(&mut archive, "lib", &[], 0o40755).unwrap();
write_entry(&mut archive, "data", &[], 0o40755).unwrap();
let mut rng_state = 0x12345678u64;
let entry_size = 256 * 1024; let mut entry_num = 0;
while archive.len() + entry_size < total_size {
let mut payload = vec![0u8; entry_size];
for byte in &mut payload {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (rng_state >> 33) as u8;
}
let name = format!("lib/test_{entry_num:04}.so");
write_entry(&mut archive, &name, &payload, 0o100755).unwrap();
entry_num += 1;
}
if archive.len() < total_size {
let remaining = total_size - archive.len() - 200; let remaining = remaining.min(total_size);
let mut payload = vec![0u8; remaining];
for byte in &mut payload {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (rng_state >> 33) as u8;
}
write_entry(&mut archive, "data/fill.bin", &payload, 0o100644).unwrap();
}
cpio::newc::trailer(&mut archive as &mut dyn std::io::Write).unwrap();
let pad = (512 - (archive.len() % 512)) % 512;
archive.extend(std::iter::repeat_n(0u8, pad));
archive
}
fn simulate_kernel_unlz4(input: &[u8]) -> Result<Vec<u8>, String> {
const UNCOMP_CHUNK_SIZE: usize = 8 << 20;
if input.len() < 4 {
return Err("input too short for magic".into());
}
let mut inp = 0usize; let mut size = input.len() as isize;
let magic = u32::from_le_bytes(input[inp..inp + 4].try_into().unwrap());
if magic != 0x184C2102 {
return Err(format!("invalid header: 0x{magic:08X}"));
}
inp += 4;
size -= 4;
let mut output = Vec::new();
loop {
if size < 4 {
break;
}
let chunksize = u32::from_le_bytes(input[inp..inp + 4].try_into().unwrap()) as usize;
if chunksize == 0x184C2102 {
inp += 4;
size -= 4;
continue;
}
if chunksize == 0 {
break;
}
inp += 4;
size -= 4;
let chunk_data = &input[inp..inp + chunksize];
let decompressed = lz4_flex::block::decompress(chunk_data, UNCOMP_CHUNK_SIZE)
.map_err(|e| format!("LZ4_decompress_safe failed: {e}"))?;
output.extend_from_slice(&decompressed);
size -= chunksize as isize;
if size == 0 {
break;
} else if size < 0 {
return Err("data corrupted: size went negative".into());
}
inp += chunksize;
}
Ok(output)
}
#[test]
fn lz4_legacy_kernel_unlz4_roundtrip() {
let small = build_synthetic_cpio(1 << 20); let compressed = lz4_legacy_compress(&small);
let decompressed = simulate_kernel_unlz4(&compressed)
.expect("kernel unlz4 simulation failed on small input");
assert_eq!(decompressed, small);
let large = build_synthetic_cpio(10 << 20); let compressed = lz4_legacy_compress(&large);
let decompressed = simulate_kernel_unlz4(&compressed)
.expect("kernel unlz4 simulation failed on multi-chunk input");
assert_eq!(decompressed, large);
}
#[test]
fn lz4_legacy_kernel_unlz4_concatenated() {
let base = build_synthetic_cpio(2 << 20); let suffix_data = b"arg1\narg2\narg3\n";
let lz4_base = lz4_legacy_compress(&base);
let lz4_suffix = lz4_legacy_compress(suffix_data);
let mut combined = Vec::with_capacity(lz4_base.len() + lz4_suffix.len());
combined.extend_from_slice(&lz4_base);
combined.extend_from_slice(&lz4_suffix);
let decompressed = simulate_kernel_unlz4(&combined)
.expect("kernel unlz4 simulation failed on concatenated streams");
let mut expected = Vec::with_capacity(base.len() + suffix_data.len());
expected.extend_from_slice(&base);
expected.extend_from_slice(suffix_data);
assert_eq!(decompressed, expected);
}
#[test]
fn lz4_legacy_compress_c_compat() {
let lz4_check = std::process::Command::new("lz4").arg("--version").output();
if lz4_check.is_err() {
skip!("lz4 CLI not found");
}
let data = build_synthetic_cpio(2 << 20); let compressed = lz4_legacy_compress(&data);
let compressed_path = std::env::temp_dir().join("ktstr-test-lz4-compat.lz4");
let decompressed_path = std::env::temp_dir().join("ktstr-test-lz4-compat.bin");
std::fs::write(&compressed_path, &compressed).unwrap();
let output = std::process::Command::new("lz4")
.args(["-d", "-f", "--no-frame-crc"])
.arg(&compressed_path)
.arg(&decompressed_path)
.output()
.expect("lz4 -d failed to execute");
let _ = std::fs::remove_file(&compressed_path);
assert!(
output.status.success(),
"lz4 -d failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
let result = std::fs::read(&decompressed_path).unwrap();
let _ = std::fs::remove_file(&decompressed_path);
assert_eq!(result.len(), data.len(), "decompressed size mismatch");
assert_eq!(&result[..], &data[..], "decompressed content mismatch");
}
#[test]
fn lz4_legacy_reference_cross_compat() {
let lz4_check = std::process::Command::new("lz4").arg("--version").output();
if lz4_check.is_err() {
skip!("lz4 CLI not found");
}
let data = build_synthetic_cpio(2 << 20);
let input_path = std::env::temp_dir().join("ktstr-test-lz4-ref-input.bin");
let ref_path = std::env::temp_dir().join("ktstr-test-lz4-ref.lz4");
std::fs::write(&input_path, &data).unwrap();
let ref_output = std::process::Command::new("lz4")
.args(["-l", "-f"])
.arg(&input_path)
.arg(&ref_path)
.output()
.expect("lz4 -l failed to execute");
let _ = std::fs::remove_file(&input_path);
assert!(
ref_output.status.success(),
"lz4 -l failed: stderr={}",
String::from_utf8_lossy(&ref_output.stderr),
);
let ref_compressed = std::fs::read(&ref_path).unwrap();
let _ = std::fs::remove_file(&ref_path);
let ref_decompressed = simulate_kernel_unlz4(&ref_compressed)
.expect("kernel unlz4 simulation failed on lz4 -l output");
assert_eq!(
ref_decompressed, data,
"reference lz4 -l roundtrip mismatch"
);
let our_compressed = lz4_legacy_compress(&data);
let our_lz4_path = std::env::temp_dir().join("ktstr-test-lz4-ref-ours.lz4");
let our_decompressed_path = std::env::temp_dir().join("ktstr-test-lz4-ref-ours.bin");
std::fs::write(&our_lz4_path, &our_compressed).unwrap();
let our_output = std::process::Command::new("lz4")
.args(["-d", "-f", "--no-frame-crc"])
.arg(&our_lz4_path)
.arg(&our_decompressed_path)
.output()
.expect("lz4 -d on our output failed to execute");
let _ = std::fs::remove_file(&our_lz4_path);
assert!(
our_output.status.success(),
"lz4 -d on our output failed: stderr={}",
String::from_utf8_lossy(&our_output.stderr),
);
let our_result = std::fs::read(&our_decompressed_path).unwrap();
let _ = std::fs::remove_file(&our_decompressed_path);
assert_eq!(our_result, data, "our lz4 output cross-compat mismatch");
}
}