use super::*;
pub(crate) fn parse_needed(path: &Path) -> io::Result<Vec<String>> {
parse_needed_bytes(&fs::read(path)?)
}
pub(crate) fn parse_needed_bytes(data: &[u8]) -> io::Result<Vec<String>> {
let elf = goblin::elf::Elf::parse(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
Ok(elf
.libraries
.iter()
.map(|s| {
if s.starts_with('/') {
Path::new(s)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(s)
.to_string()
} else {
s.to_string()
}
})
.collect())
}
pub(crate) fn parse_needed_dirs_under(path: &Path, root: Option<&Path>) -> Vec<PathBuf> {
let Ok(data) = fs::read(path) else {
return Vec::new();
};
let Ok(elf) = goblin::elf::Elf::parse(&data) else {
return Vec::new();
};
elf.libraries
.iter()
.filter(|s| s.starts_with('/'))
.filter_map(|s| Path::new(s).parent().map(Path::to_path_buf))
.map(|dir| match root {
Some(root) => root.join(dir.strip_prefix("/").unwrap_or(&dir)),
None => dir,
})
.collect()
}
pub(crate) fn parse_interp(path: &Path) -> Option<String> {
let data = fs::read(path).ok()?;
let elf = goblin::elf::Elf::parse(&data).ok()?;
elf.interpreter
.map(|s| s.trim_end_matches('\0').to_string())
}
fn has_soname(path: &Path) -> bool {
fs::read(path)
.ok()
.and_then(|data| {
goblin::elf::Elf::parse(&data)
.ok()
.map(|elf| elf.soname.is_some())
})
.unwrap_or(true)
}
pub(crate) fn program_interp(path: &Path) -> Option<String> {
let data = fs::read(path).ok()?;
let elf = goblin::elf::Elf::parse(&data).ok()?;
match elf.interpreter {
Some(interp) => Some(interp.trim_end_matches('\0').to_string()),
None => injected_interp(&data, &elf),
}
}
pub(crate) fn injected_interp(data: &[u8], elf: &goblin::elf::Elf) -> Option<String> {
let seg = elf
.program_headers
.iter()
.find(|p| p.p_type == goblin::elf::program_header::PT_LOAD && p.p_vaddr == elf.entry)?;
let start = usize::try_from(seg.p_offset).ok()?;
let end = start.checked_add(usize::try_from(seg.p_filesz).ok()?)?;
let seg = data.get(start..end)?;
let (&0, body) = seg.split_last()? else {
return None;
};
(1..=body.len().saturating_sub(2).min(4096)).find_map(|len| {
let at = body.len() - len;
let recorded = u16::from_le_bytes([body[at - 2], body[at - 1]]);
let path = &body[at..];
if usize::from(recorded) != len || path.contains(&0) {
return None;
}
let path = std::str::from_utf8(path).ok()?;
is_dynamic_loader(Path::new(path).file_name()?.to_str()?).then(|| path.to_string())
})
}
pub(crate) fn libc_alias_for(interp_name: &str) -> Option<String> {
interp_name
.strip_prefix("ld-musl-")
.map(|rest| format!("libc.musl-{rest}"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LibcFamily {
Musl,
Glibc,
}
pub(crate) fn libc_family_from_interp(interp: &str) -> Option<LibcFamily> {
let name = Path::new(interp).file_name()?.to_str()?;
if name.starts_with("ld-musl-") {
Some(LibcFamily::Musl)
} else if onelf_format::ldso::is_glibc_loader_name(name) {
Some(LibcFamily::Glibc)
} else {
None
}
}
pub(crate) fn parse_soname(path: &Path) -> Option<String> {
let data = fs::read(path).ok()?;
let elf = goblin::elf::Elf::parse(&data).ok()?;
elf.soname.map(String::from)
}
pub(crate) fn libc_family_of_soname(soname: &str) -> Option<LibcFamily> {
if soname == "libc.so.6" || onelf_format::ldso::is_glibc_loader_name(soname) {
Some(LibcFamily::Glibc)
} else if soname.starts_with("libc.musl-")
|| soname.starts_with("ld-musl-")
|| soname == "libc.so"
{
Some(LibcFamily::Musl)
} else {
None
}
}
pub(crate) fn expand_runpath_entry(entry: &str, origin: &Path) -> PathBuf {
if let Some(rest) = entry.strip_prefix("${ORIGIN}") {
origin.join(rest.trim_start_matches('/'))
} else if let Some(rest) = entry.strip_prefix("$ORIGIN") {
origin.join(rest.trim_start_matches('/'))
} else {
PathBuf::from(entry)
}
}
#[cfg(test)]
pub(crate) fn resolve_runpath_dirs<'a>(
raw: impl Iterator<Item = &'a str>,
origin: &Path,
) -> Vec<PathBuf> {
resolve_runpath_dirs_under(raw, origin, None)
}
pub(crate) fn resolve_runpath_dirs_under<'a>(
raw: impl Iterator<Item = &'a str>,
origin: &Path,
root: Option<&Path>,
) -> Vec<PathBuf> {
raw.flat_map(|s| s.split(':'))
.filter(|s| !s.is_empty())
.map(|entry| expand_runpath_entry(entry, origin))
.filter_map(|dir| match root {
Some(root) if dir.is_absolute() && !dir.starts_with(root) => {
onelf_sysroot::path::in_root(root, &dir).ok()
}
_ => Some(dir),
})
.filter(|p| p.is_dir())
.collect()
}
pub(crate) fn parse_rpaths(path: &Path) -> Vec<PathBuf> {
parse_rpaths_under(path, None)
}
pub(crate) fn parse_rpaths_raw(path: &Path) -> Vec<String> {
let Ok(data) = fs::read(path) else {
return Vec::new();
};
let Ok(elf) = goblin::elf::Elf::parse(&data) else {
return Vec::new();
};
let raw = if !elf.runpaths.is_empty() {
&elf.runpaths
} else {
&elf.rpaths
};
raw.iter()
.flat_map(|entry| entry.split(':'))
.map(String::from)
.collect()
}
pub(crate) fn parse_rpaths_under(path: &Path, root: Option<&Path>) -> Vec<PathBuf> {
let Ok(data) = fs::read(path) else {
return Vec::new();
};
let Ok(elf) = goblin::elf::Elf::parse(&data) else {
return Vec::new();
};
let origin = path.parent().unwrap_or_else(|| Path::new("."));
let raw = if !elf.runpaths.is_empty() {
&elf.runpaths
} else {
&elf.rpaths
};
resolve_runpath_dirs_under(raw.iter().copied(), origin, root)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RunpathOutcome {
Set,
NotNeeded,
Unguaranteed,
SelfExtract,
NeedsPatchelf,
}
pub(crate) fn origin_runpath(depth: usize, lib_rel: &Path) -> String {
let mut s = String::from("$ORIGIN");
for _ in 0..depth {
s.push_str("/..");
}
s.push('/');
s.push_str(&lib_rel.to_string_lossy());
s
}
fn origin_relative(from: &Path, target: &Path, root: &Path) -> Option<String> {
let (from, target) = (
from.strip_prefix(root).ok()?,
target.strip_prefix(root).ok()?,
);
let mut f = from.components();
let mut t = target.components();
let mut shared = 0usize;
loop {
match (f.clone().next(), t.clone().next()) {
(Some(a), Some(b)) if a == b => {
f.next();
t.next();
shared += 1;
}
_ => break,
}
}
let _ = shared;
let up = f.count();
let mut out = String::from("$ORIGIN");
for _ in 0..up {
out.push_str("/..");
}
let rest: PathBuf = t.collect();
if rest.as_os_str().is_empty() {
return Some(out);
}
out.push('/');
out.push_str(&rest.to_string_lossy());
Some(out)
}
pub(crate) fn tree_runpath(root: &Path, object: &Path, lib_rel: &Path) -> String {
let mut parts = vec![origin_runpath_for(root, object, lib_rel)];
let Some(dir) = object.parent() else {
return parts.join(":");
};
for target in run_path_dirs_in_tree(object, root) {
if let Some(entry) = origin_relative(dir, &target, root)
&& !parts.iter().any(|p| p.split(':').any(|e| e == entry))
{
parts.push(entry);
}
}
parts.join(":")
}
pub(crate) fn origin_runpath_for(root: &Path, path: &Path, lib_rel: &Path) -> String {
let dir = path
.parent()
.and_then(|dir| dir.strip_prefix(root).ok())
.unwrap_or(Path::new(""));
let to_lib = origin_runpath(dir.components().count(), lib_rel);
if dir.starts_with(lib_rel) && dir != lib_rel {
format!("$ORIGIN:{to_lib}")
} else {
to_lib
}
}
pub(crate) fn dynstr_range(elf: &goblin::elf::Elf) -> Option<(usize, usize)> {
if let Some(sh) = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".dynstr"))
{
let end = sh.sh_offset.checked_add(sh.sh_size)?;
return Some((
usize::try_from(sh.sh_offset).ok()?,
usize::try_from(end).ok()?,
));
}
let dynamic = elf.dynamic.as_ref()?;
let strtab = dynamic.info.strtab as u64;
let strsz = dynamic.info.strsz as u64;
if strsz == 0 || strtab == 0 {
return None;
}
let end = strtab.checked_add(strsz)?;
Some((usize::try_from(strtab).ok()?, usize::try_from(end).ok()?))
}
fn rewrite_origin_runpath_in(
data: &mut [u8],
path: &Path,
runpath: &str,
origin_only: &str,
) -> io::Result<(RunpathOutcome, bool)> {
let new_bytes = runpath.as_bytes();
let is_self_extract = has_embedded_payload(data);
let elf = match goblin::elf::Elf::parse(data) {
Ok(e) => e,
Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string())),
};
let has_needed = !elf.libraries.is_empty();
let has_soname = elf.soname.is_some();
let is_executable = !has_soname
&& elf
.program_headers
.iter()
.any(|p| p.p_type == goblin::elf::program_header::PT_INTERP);
let dynstr_range = dynstr_range(&elf);
let dynamic_present = elf.dynamic.is_some();
let dyn_offset = elf
.program_headers
.iter()
.find(|p| p.p_type == goblin::elf::program_header::PT_DYNAMIC)
.map(|p| p.p_offset as usize);
let dyn_entry_size = if elf.is_64 { 16 } else { 8 };
let mut slots: Vec<usize> = Vec::new();
let mut runpath_tags: Vec<usize> = Vec::new();
if let (Some((dynstr_offset, _)), Some(dynamic)) = (dynstr_range, &elf.dynamic) {
for (i, d) in dynamic.dyns.iter().enumerate() {
if d.d_tag == goblin::elf::dynamic::DT_RPATH
|| d.d_tag == goblin::elf::dynamic::DT_RUNPATH
{
slots.push(dynstr_offset + d.d_val as usize);
if d.d_tag == goblin::elf::dynamic::DT_RUNPATH
&& let Some(base) = dyn_offset
{
runpath_tags.push(base + i * dyn_entry_size);
}
}
}
}
let is_64 = elf.is_64;
let dynstr_end = dynstr_range.map(|(_, e)| e).unwrap_or(0);
drop(elf);
let mut rewritten = 0usize;
let mut changed = false;
let total = slots.len();
let limit = dynstr_end.min(data.len());
for file_pos in slots {
if file_pos >= limit {
continue;
}
let mut end = file_pos;
while end < limit && data[end] != 0 {
end += 1;
}
while end < limit && data[end] == 0 {
end += 1;
}
let slot_size = end - file_pos;
let write = [runpath, origin_only, "$ORIGIN", ""]
.into_iter()
.find(|s| s.len() < slot_size)
.unwrap_or("");
data[file_pos..file_pos + write.len()].copy_from_slice(write.as_bytes());
for b in &mut data[file_pos + write.len()..file_pos + slot_size] {
*b = 0;
}
changed = true;
if write.len() >= new_bytes.len() {
rewritten += 1;
}
}
for at in runpath_tags {
let width = if is_64 { 8 } else { 4 };
if at + width <= data.len() {
let tag = goblin::elf::dynamic::DT_RPATH;
if is_64 {
data[at..at + 8].copy_from_slice(&tag.to_le_bytes());
} else {
data[at..at + 4].copy_from_slice(&(tag as u32).to_le_bytes());
}
changed = true;
}
}
if total > 0 && rewritten == total {
return Ok((RunpathOutcome::Set, changed));
}
if total > 0 && !is_executable {
let outcome = if runpath == origin_only {
RunpathOutcome::Set
} else {
RunpathOutcome::NeedsPatchelf
};
return Ok((outcome, changed));
}
if !dynamic_present || !has_needed {
return Ok((RunpathOutcome::NotNeeded, changed));
}
if !is_executable {
return Ok((RunpathOutcome::NotNeeded, changed));
}
if is_self_extract {
return Ok((RunpathOutcome::SelfExtract, changed));
}
let _ = path;
Ok((RunpathOutcome::NeedsPatchelf, changed))
}
fn run_patchelf_rpath(path: &Path, runpath: &str, patchelf: Option<&Path>) -> RunpathOutcome {
let Some(patchelf) = patchelf else {
return RunpathOutcome::Unguaranteed;
};
match std::process::Command::new(patchelf)
.arg("--force-rpath")
.arg("--set-rpath")
.arg(runpath)
.arg(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output()
{
Ok(o) if o.status.success() => RunpathOutcome::Set,
Ok(o) => {
eprintln!(
" {} patchelf failed for {}: {}",
color::bold_red("warning:"),
path.display(),
String::from_utf8_lossy(&o.stderr).trim()
);
RunpathOutcome::Unguaranteed
}
Err(e) => {
eprintln!(
" {} could not run patchelf for {}: {e}",
color::bold_red("warning:"),
path.display(),
);
RunpathOutcome::Unguaranteed
}
}
}
fn run_path_dirs_in_tree(object: &Path, root: &Path) -> Vec<PathBuf> {
parse_rpaths_raw(object)
.into_iter()
.filter(|entry| entry.starts_with('/'))
.flat_map(|entry| {
let rel = entry.trim_start_matches('/').to_string();
let flat = rel.strip_prefix("usr/").unwrap_or(&rel).to_string();
[root.join(&rel), root.join(&flat)]
})
.filter(|d| d.is_dir())
.collect()
}
pub(crate) fn audit_unbundled_needs(
directory: &Path,
lib_dir: &Path,
follow: &[PathBuf],
) -> Vec<(PathBuf, Vec<String>)> {
use std::collections::{HashMap, HashSet};
let names_in = |dir: &Path| -> HashSet<String> {
fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().exists())
.filter_map(|e| e.file_name().to_str().map(String::from))
.collect()
};
let lib_root = directory.join(lib_dir);
let shared = names_in(&lib_root);
let mut beside: HashMap<PathBuf, HashSet<String>> = HashMap::new();
let mut findings: Vec<(PathBuf, Vec<String>)> = Vec::new();
for path in find_elf_files(directory)
.into_iter()
.chain(follow.iter().cloned())
{
let Ok(needed) = parse_needed(&path) else {
continue;
};
let dir = path.parent().unwrap_or_else(|| Path::new("."));
let origin_lib = dir.join("../lib");
let local = beside
.entry(origin_lib.clone())
.or_insert_with(|| names_in(&origin_lib));
let mut on_run_path: HashSet<String> = HashSet::new();
for entry in parse_rpaths(&path) {
on_run_path.extend(names_in(&entry));
}
for entry in run_path_dirs_in_tree(&path, directory) {
on_run_path.extend(names_in(&entry));
}
let siblings = if dir.starts_with(&lib_root) && dir != lib_root {
names_in(dir)
} else {
HashSet::new()
};
let mut missing: Vec<String> = needed
.into_iter()
.filter(|soname| {
let bare = Path::new(soname)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(soname);
!shared.contains(bare)
&& !local.contains(bare)
&& !siblings.contains(bare)
&& !on_run_path.contains(bare)
&& !is_dynamic_loader(bare)
})
.collect();
if !missing.is_empty() {
missing.sort();
missing.dedup();
findings.push((path, missing));
}
}
findings
}
pub(crate) fn finalize_tree(
directory: &Path,
lib_rel: &Path,
) -> (usize, usize, Vec<PathBuf>, Vec<PathBuf>) {
let mut rewritten = 0usize;
let mut scrubbed = 0usize;
let mut unguaranteed: Vec<PathBuf> = Vec::new();
let mut self_extract: Vec<PathBuf> = Vec::new();
let patchelf = which_patchelf();
for path in find_elf_files(directory) {
if break_hardlink(&path).is_err() {
continue;
}
let perms = fs::metadata(&path)
.map(|m| m.permissions().mode())
.unwrap_or(0o755);
let needs_chmod = perms & 0o200 == 0;
if needs_chmod {
let _ = fs::set_permissions(&path, PermissionsExt::from_mode(perms | 0o200));
}
let runpath = tree_runpath(directory, &path, lib_rel);
let origin_only = origin_runpath_for(directory, &path, lib_rel);
if let Ok((outcome, did_scrub)) =
finalize_one(&path, &runpath, &origin_only, patchelf.as_deref())
{
match outcome {
RunpathOutcome::Set => rewritten += 1,
RunpathOutcome::Unguaranteed => unguaranteed.push(path.clone()),
RunpathOutcome::SelfExtract => self_extract.push(path.clone()),
RunpathOutcome::NotNeeded | RunpathOutcome::NeedsPatchelf => {}
}
if did_scrub {
scrubbed += 1;
}
}
normalize_mtime(&path);
if needs_chmod {
let _ = fs::set_permissions(&path, PermissionsExt::from_mode(perms));
}
}
(rewritten, scrubbed, unguaranteed, self_extract)
}
pub(crate) fn break_hardlink(path: &Path) -> io::Result<()> {
use std::os::unix::fs::MetadataExt;
let Ok(meta) = fs::symlink_metadata(path) else {
return Ok(());
};
if !meta.is_file() || meta.nlink() < 2 {
return Ok(());
}
let mut tmp = path.as_os_str().to_os_string();
tmp.push(".onelf-unlink.tmp");
let tmp = PathBuf::from(tmp);
let detached = fs::copy(path, &tmp).and_then(|_| fs::rename(&tmp, path));
if let Err(error) = detached {
let _ = fs::remove_file(&tmp);
return Err(io::Error::new(
error.kind(),
format!("{}: {error}", path.display()),
));
}
Ok(())
}
fn finalize_one(
path: &Path,
runpath: &str,
origin_only: &str,
patchelf: Option<&Path>,
) -> io::Result<(RunpathOutcome, bool)> {
let mut data = fs::read(path)?;
let (outcome, rewritten) = rewrite_origin_runpath_in(&mut data, path, runpath, origin_only)?;
let scrubbed = scrub_nix_store_paths_in(&mut data);
let stripped = strip_absolute_needed_in(&mut data);
if rewritten || scrubbed || stripped {
fs::write(path, &data)?;
}
if outcome == RunpathOutcome::NeedsPatchelf {
return Ok((run_patchelf_rpath(path, runpath, patchelf), scrubbed));
}
Ok((outcome, scrubbed))
}
pub(crate) fn report_unbundled_needs(findings: &[(PathBuf, Vec<String>)]) {
if findings.is_empty() {
return;
}
let mut sonames: Vec<&str> = findings
.iter()
.flat_map(|(_, libs)| libs.iter().map(|s| s.as_str()))
.collect();
sonames.sort();
sonames.dedup();
eprintln!(
"{} {} librar(ies) are not in the bundle:",
color::bold_red("warning:"),
sonames.len()
);
for soname in &sonames {
let by: Vec<String> = findings
.iter()
.filter(|(_, libs)| libs.iter().any(|l| l == soname))
.filter_map(|(p, _)| {
p.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
})
.take(3)
.collect();
eprintln!(" - {soname} (needed by {})", by.join(", "));
}
eprintln!(
" These resolve to the host's copies, and load next to the bundled \
libc, only for a package that keeps the host's library directories. \
Otherwise loading them fails where they are first used. Add them \
with --search-path, or leave them if they are meant to come from \
the host (GL, DRI, Vulkan, NSS); `pack --host-libs` decides which \
of the two happens."
);
}
pub(crate) fn report_unguaranteed_runpath(unguaranteed: &[PathBuf], self_extract: &[PathBuf]) {
if !unguaranteed.is_empty() {
eprintln!(
"{} {} file(s) have no baked-in $ORIGIN RUNPATH and rely \
on LD_LIBRARY_PATH:",
color::bold_red("warning:"),
unguaranteed.len()
);
for p in unguaranteed {
eprintln!(" - {}", p.display());
}
eprintln!(
" These break if the app re-execs itself in a sandbox \
(clearenv). Install `patchelf` (or set ONELF_PATCHELF) and \
repack to make them re-exec-safe."
);
}
if !self_extract.is_empty() {
eprintln!(
"{} {} self-extracting executable(s) can't take a baked-in \
RUNPATH (would clobber the embedded payload):",
color::bold_red("warning:"),
self_extract.len()
);
for p in self_extract {
eprintln!(" - {}", p.display());
}
eprintln!(
" These rely on the runtime's LD_LIBRARY_PATH and are not \
sandbox-re-exec-safe."
);
}
}
pub(crate) fn has_self_extract_trailer(data: &[u8]) -> bool {
const BUN_TRAILER: &[u8] = b"\n---- Bun! ----\n";
if data.len() >= BUN_TRAILER.len() && data.ends_with(BUN_TRAILER) {
return true;
}
if data.len() >= BUN_TRAILER.len() + 8
&& &data[data.len() - BUN_TRAILER.len() - 8..data.len() - 8] == BUN_TRAILER
{
return true;
}
false
}
pub(crate) fn has_bun_section(data: &[u8]) -> bool {
let Ok(elf) = goblin::elf::Elf::parse(data) else {
return false;
};
elf.section_headers
.iter()
.any(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".bun"))
}
pub(crate) fn has_embedded_payload(data: &[u8]) -> bool {
has_self_extract_trailer(data) || has_bun_section(data)
}
pub(crate) fn which_patchelf() -> Option<PathBuf> {
if let Ok(p) = std::env::var("ONELF_PATCHELF") {
let p = PathBuf::from(p);
if p.is_file() {
return Some(p);
}
}
let path = std::env::var("PATH").ok()?;
for dir in path.split(':') {
if dir.is_empty() {
continue;
}
let p = PathBuf::from(dir).join("patchelf");
if p.is_file() {
return Some(p);
}
}
None
}
pub(crate) fn strip_absolute_needed_in(modified: &mut [u8]) -> bool {
let Ok(elf) = goblin::elf::Elf::parse(modified) else {
return false;
};
let dynstr_offset = dynstr_range(&elf).map(|(off, _)| off);
let (Some(dynstr_offset), Some(dynamic)) = (dynstr_offset, &elf.dynamic) else {
return false;
};
let mut edits: Vec<(usize, usize, usize)> = Vec::new();
for dyn_entry in &dynamic.dyns {
if dyn_entry.d_tag != goblin::elf::dynamic::DT_NEEDED {
continue;
}
let file_pos = dynstr_offset + dyn_entry.d_val as usize;
if file_pos >= modified.len() || modified[file_pos] != b'/' {
continue;
}
let mut end = file_pos;
while end < modified.len() && modified[end] != 0 {
end += 1;
}
let slot_size = end - file_pos;
let original = &modified[file_pos..end];
let basename_start = match original.iter().rposition(|&b| b == b'/') {
Some(p) => p + 1,
None => 0,
};
let basename_len = original.len() - basename_start;
if basename_len == 0 || basename_len >= slot_size {
continue;
}
edits.push((file_pos, basename_start, slot_size));
}
drop(elf);
let changed = !edits.is_empty();
for (file_pos, basename_start, slot_size) in edits {
let basename: Vec<u8> = modified[file_pos + basename_start..file_pos + slot_size].to_vec();
let basename: Vec<u8> = basename.into_iter().take_while(|&b| b != 0).collect();
let len = basename.len();
modified[file_pos..file_pos + len].copy_from_slice(&basename);
for b in &mut modified[file_pos + len..file_pos + slot_size] {
*b = 0;
}
}
changed
}
pub(crate) fn is_excluded(soname: &str, excludes: &[&str]) -> bool {
excludes.iter().any(|pat| soname.starts_with(pat))
}
pub(crate) fn is_dynamic_loader(soname: &str) -> bool {
onelf_format::ldso::is_glibc_loader_name(soname)
|| soname.starts_with("ld-musl-")
|| soname == "ld.so"
}
const MUSL_LOADER_SCRUB: &[(&[u8], &[u8])] = &[(
b"/lib:/usr/local/lib:/usr/lib",
b"/XXX:/XXX/local/lib:/XXX/lib",
)];
pub(crate) fn scrub_loader_paths(path: &Path, family: LibcFamily) -> io::Result<()> {
let mut data = fs::read(path)?;
let replaced = match family {
LibcFamily::Glibc => {
onelf_format::ldso::scrub_glibc_loader(&mut data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.replaced
}
LibcFamily::Musl => onelf_format::ldso::replace_equal_length(&mut data, MUSL_LOADER_SCRUB),
};
if replaced > 0 {
fs::write(path, &data)?;
}
Ok(())
}
pub(crate) fn scrub_nix_store_paths_in(data: &mut [u8]) -> bool {
let mut changed = false;
let rewrites: &[(&[u8], &[u8])] = &[
(b"/share/zoneinfo", b"/usr/share/zoneinfo"),
(b"/bin/locale", b"/usr/bin/locale"),
];
for (suffix, replacement) in rewrites {
let mut i = 0;
while i + suffix.len() <= data.len() {
if &data[i..i + suffix.len()] != *suffix {
i += 1;
continue;
}
let mut start = i;
while start > 0 && data[start - 1] != 0 {
start -= 1;
}
if start + 11 > data.len() || &data[start..start + 11] != b"/nix/store/" {
i += suffix.len();
continue;
}
let mut end = i + suffix.len();
while end < data.len() && data[end] != 0 {
end += 1;
}
if replacement.len() + 1 > end - start {
i = end;
continue;
}
data[start..start + replacement.len()].copy_from_slice(replacement);
for b in &mut data[start + replacement.len()..end] {
*b = 0;
}
changed = true;
i = end;
}
}
changed
}
pub(crate) fn bootstrap_page_align(is_aarch64: bool) -> u64 {
if is_aarch64 { 0x10000 } else { 0x1000 }
}
pub(crate) fn inject_relative_interp(path: &Path, rel_interp: &str) -> io::Result<bool> {
use crate::payload;
use goblin::elf::program_header::PT_INTERP;
let data = fs::read(path)?;
if has_embedded_payload(&data) {
eprintln!(
" note: {} appears to be a Bun-compiled or self-extracting \
binary; skipping bootstrap injection",
path.display(),
);
return Ok(false);
}
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
if elf.header.e_ident[5] != 1 {
return Ok(false); }
let is64 = match elf.header.e_ident[4] {
1 => false,
2 => true,
_ => return Ok(false),
};
let is_x86_64 = elf.header.e_machine == goblin::elf::header::EM_X86_64;
let is_aarch64 = elf.header.e_machine == goblin::elf::header::EM_AARCH64;
let is_i686 = elf.header.e_machine == goblin::elf::header::EM_386;
if !is_x86_64 && !is_aarch64 && !is_i686 {
return Ok(false);
}
if is_i686 == is64 {
return Ok(false);
}
let phdr_idx = match elf
.program_headers
.iter()
.position(|p| p.p_type == PT_INTERP)
{
Some(i) => i,
None => return Ok(false),
};
let highest_vend: u64 = elf
.program_headers
.iter()
.filter(|p| p.p_type == goblin::elf::program_header::PT_LOAD)
.map(|p| p.p_vaddr + p.p_memsz)
.max()
.unwrap_or(0);
let page_size: u64 = bootstrap_page_align(is_aarch64);
let new_vaddr = (highest_vend + page_size - 1) & !(page_size - 1);
let orig_entry = elf.header.e_entry;
let e_phoff = elf.header.e_phoff as usize;
let e_phentsize = elf.header.e_phentsize as usize;
let e_machine = elf.header.e_machine;
drop(elf);
let Some(code) = payload::bootstrap_blob(e_machine) else {
eprintln!(
" {} onelf was built without the bootstrap payload for this target's \
architecture; skipping relative-interp injection for {}",
color::bold_red("warning:"),
path.display()
);
return Ok(false);
};
let rel_bytes = rel_interp.as_bytes();
let mut blob = Vec::with_capacity(code.len() + 64);
blob.extend_from_slice(code);
while blob.len() % 8 != 0 {
blob.push(0);
}
let metadata_offset = blob.len();
let entry_delta = (orig_entry as i64) - (new_vaddr as i64);
if is64 {
blob.extend_from_slice(&entry_delta.to_le_bytes());
} else {
blob.extend_from_slice(&(entry_delta as i32).to_le_bytes());
}
blob.extend_from_slice(&(rel_bytes.len() as u16).to_le_bytes());
blob.extend_from_slice(rel_bytes);
blob.push(0);
if is_x86_64 {
let disp = (metadata_offset as i32) - (payload::X86_64_METADATA_LEA_RIP as i32);
blob[payload::X86_64_METADATA_LEA_DISP_OFFSET
..payload::X86_64_METADATA_LEA_DISP_OFFSET + 4]
.copy_from_slice(&disp.to_le_bytes());
} else if is_aarch64 {
payload::patch_aarch64_adr(&mut blob, metadata_offset);
} else {
let disp = (metadata_offset as i32) - (payload::I686_METADATA_ADD_PC as i32);
blob[payload::I686_METADATA_ADD_DISP_OFFSET..payload::I686_METADATA_ADD_DISP_OFFSET + 4]
.copy_from_slice(&disp.to_le_bytes());
}
let mut modified = data;
let page = page_size as usize;
while modified.len() % page != 0 {
modified.push(0);
}
let file_offset = modified.len() as u64;
let blob_len = blob.len() as u64;
modified.extend_from_slice(&blob);
let phdr_off = e_phoff + phdr_idx * e_phentsize;
if is64 {
modified[phdr_off..phdr_off + 4].copy_from_slice(&1u32.to_le_bytes()); modified[phdr_off + 4..phdr_off + 8].copy_from_slice(&5u32.to_le_bytes()); modified[phdr_off + 8..phdr_off + 16].copy_from_slice(&file_offset.to_le_bytes());
modified[phdr_off + 16..phdr_off + 24].copy_from_slice(&new_vaddr.to_le_bytes());
modified[phdr_off + 24..phdr_off + 32].copy_from_slice(&new_vaddr.to_le_bytes());
modified[phdr_off + 32..phdr_off + 40].copy_from_slice(&blob_len.to_le_bytes());
modified[phdr_off + 40..phdr_off + 48].copy_from_slice(&blob_len.to_le_bytes());
modified[phdr_off + 48..phdr_off + 56].copy_from_slice(&page_size.to_le_bytes());
} else {
modified[phdr_off..phdr_off + 4].copy_from_slice(&1u32.to_le_bytes()); modified[phdr_off + 4..phdr_off + 8].copy_from_slice(&(file_offset as u32).to_le_bytes());
modified[phdr_off + 8..phdr_off + 12].copy_from_slice(&(new_vaddr as u32).to_le_bytes());
modified[phdr_off + 12..phdr_off + 16].copy_from_slice(&(new_vaddr as u32).to_le_bytes());
modified[phdr_off + 16..phdr_off + 20].copy_from_slice(&(blob_len as u32).to_le_bytes());
modified[phdr_off + 20..phdr_off + 24].copy_from_slice(&(blob_len as u32).to_le_bytes());
modified[phdr_off + 24..phdr_off + 28].copy_from_slice(&5u32.to_le_bytes()); modified[phdr_off + 28..phdr_off + 32].copy_from_slice(&(page_size as u32).to_le_bytes());
}
let e_phnum_off = if is64 { 56 } else { 44 };
let e_phnum =
u16::from_le_bytes(modified[e_phnum_off..e_phnum_off + 2].try_into().unwrap()) as usize;
let last_phdr_off = e_phoff + (e_phnum - 1) * e_phentsize;
if phdr_off != last_phdr_off {
let mut tmp = vec![0u8; e_phentsize];
tmp.copy_from_slice(&modified[phdr_off..phdr_off + e_phentsize]);
modified.copy_within(last_phdr_off..last_phdr_off + e_phentsize, phdr_off);
modified[last_phdr_off..last_phdr_off + e_phentsize].copy_from_slice(&tmp);
}
if is64 {
modified[24..32].copy_from_slice(&new_vaddr.to_le_bytes());
} else {
modified[24..28].copy_from_slice(&(new_vaddr as u32).to_le_bytes());
}
fs::write(path, &modified)?;
Ok(true)
}
pub(crate) enum EnvNeededOutcome {
Added,
AlreadyPresent,
NoBlobForArch,
NoPatchelf,
Skipped,
}
pub(crate) fn add_onelf_env_needed(path: &Path, lib_dest: &Path) -> io::Result<EnvNeededOutcome> {
let data = fs::read(path)?;
if data.len() < 20 || &data[0..4] != b"\x7fELF" || (data[4] != 1 && data[4] != 2) {
return Ok(EnvNeededOutcome::Skipped); }
if has_embedded_payload(&data) {
return Ok(EnvNeededOutcome::Skipped);
}
let e_machine = u16::from_le_bytes([data[18], data[19]]);
let Some(blob) = crate::payload::onelf_env_blob(e_machine) else {
return Ok(EnvNeededOutcome::NoBlobForArch);
};
let soname = crate::payload::onelf_env_soname(e_machine);
if let Ok(elf) = goblin::elf::Elf::parse(&data)
&& elf.libraries.contains(&soname)
{
return Ok(EnvNeededOutcome::AlreadyPresent);
}
let dest = lib_dest.join(soname);
let need_write = match fs::read(&dest) {
Ok(existing) => existing != blob,
Err(_) => true,
};
if need_write {
fs::create_dir_all(lib_dest)?;
fs::write(&dest, blob)?;
let _ = fs::set_permissions(&dest, std::os::unix::fs::PermissionsExt::from_mode(0o755));
normalize_mtime(&dest);
}
let Some(patchelf) = which_patchelf() else {
return Ok(EnvNeededOutcome::NoPatchelf);
};
let out = std::process::Command::new(&patchelf)
.arg("--add-needed")
.arg(soname)
.arg(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();
match out {
Ok(o) if o.status.success() => Ok(EnvNeededOutcome::Added),
Ok(o) => Err(io::Error::other(
String::from_utf8_lossy(&o.stderr).trim().to_string(),
)),
Err(e) => Err(e),
}
}
pub(crate) fn inject_bootstraps(app_dir: &Path, lib_dest: &Path) -> io::Result<usize> {
let rel_lib = lib_dest
.strip_prefix(app_dir)
.unwrap_or(lib_dest)
.to_path_buf();
let mut injected = 0usize;
let mut env_added = 0usize;
let mut env_no_patchelf: Vec<PathBuf> = Vec::new();
let mut env_no_blob = false;
for path in find_elf_files(app_dir) {
let Some(interp) = parse_interp(&path) else {
continue;
};
let Some(basename) = Path::new(&interp).file_name().and_then(|n| n.to_str()) else {
continue;
};
let bundled = lib_dest.join(basename);
if !bundled.exists() {
continue;
}
if path.starts_with(lib_dest) && has_soname(&path) {
continue;
}
let rel_bin = match path.strip_prefix(app_dir) {
Ok(r) => r,
Err(_) => continue,
};
let depth = rel_bin
.parent()
.map(|p| p.components().count())
.unwrap_or(0);
let mut rel = PathBuf::new();
for _ in 0..depth {
rel.push("..");
}
rel.push(&rel_lib);
rel.push(basename);
let rel_interp = rel.to_string_lossy().into_owned();
if break_hardlink(&path).is_err() {
eprintln!(
" {} could not detach {} from its hardlink; no bootstrap injected",
color::bold_red("warning:"),
path.display()
);
continue;
}
let perms = fs::metadata(&path)
.map(|m| m.permissions().mode())
.unwrap_or(0o755);
let needs_chmod = perms & 0o200 == 0;
if needs_chmod {
let _ = fs::set_permissions(
&path,
std::os::unix::fs::PermissionsExt::from_mode(perms | 0o200),
);
}
match add_onelf_env_needed(&path, lib_dest) {
Ok(EnvNeededOutcome::Added) => env_added += 1,
Ok(EnvNeededOutcome::AlreadyPresent) => {}
Ok(EnvNeededOutcome::NoBlobForArch) => env_no_blob = true,
Ok(EnvNeededOutcome::NoPatchelf) => env_no_patchelf.push(path.clone()),
Ok(EnvNeededOutcome::Skipped) => {}
Err(e) => {
eprintln!(
" {} could not add onelf-env to {}: {e}",
color::bold_red("warning:"),
path.display()
);
}
}
match inject_relative_interp(&path, &rel_interp) {
Ok(true) => injected += 1,
Ok(false) => {}
Err(e) => {
eprintln!(
" {} could not inject bootstrap into {}: {e}",
color::bold_red("warning:"),
path.display()
);
}
}
normalize_mtime(&path);
if needs_chmod {
let _ = fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(perms));
}
}
if env_added > 0 {
eprintln!(
"{} onelf-env (re-exec-safe .onelf/env) into {} binaries",
color::bold("Injected"),
env_added
);
}
if !env_no_patchelf.is_empty() {
eprintln!(
"{} patchelf unavailable; {} executable(s) won't re-apply \
.onelf/env after a sandboxed re-exec:",
color::bold_red("warning:"),
env_no_patchelf.len()
);
for p in &env_no_patchelf {
eprintln!(" - {}", p.display());
}
eprintln!(
" Install `patchelf` (or set ONELF_PATCHELF) and repack for \
re-exec-safe env."
);
}
if env_no_blob {
eprintln!(
"{} no onelf-env blob built for this target arch; \
.onelf/env is runtime-only (not sandbox-re-exec-safe). \
Build it via crates/onelf/src/payload/Makefile.",
color::bold_red("warning:"),
);
}
Ok(injected)
}
#[cfg(test)]
mod embedded_payload_tests {
use super::*;
fn elf_with_bun_section() -> Vec<u8> {
let shstrtab = b"\0.bun\0.shstrtab\0";
let mut f = vec![0u8; 64 + shstrtab.len()];
f[0..4].copy_from_slice(b"\x7fELF");
f[4] = 2; f[5] = 1; f[6] = 1; f[16..18].copy_from_slice(&2u16.to_le_bytes()); f[18..20].copy_from_slice(&0x3eu16.to_le_bytes()); f[20..24].copy_from_slice(&1u32.to_le_bytes()); f[52..54].copy_from_slice(&64u16.to_le_bytes()); f[58..60].copy_from_slice(&64u16.to_le_bytes()); f[62..64].copy_from_slice(&2u16.to_le_bytes());
let shstr_off = 64u64;
f[64..64 + shstrtab.len()].copy_from_slice(shstrtab);
while !f.len().is_multiple_of(8) {
f.push(0);
}
let sh_off = f.len() as u64;
f[40..48].copy_from_slice(&sh_off.to_le_bytes()); f[60..62].copy_from_slice(&3u16.to_le_bytes());
let mut sh = |name: u32, typ: u32, off: u64, size: u64| {
let mut e = vec![0u8; 64];
e[0..4].copy_from_slice(&name.to_le_bytes());
e[4..8].copy_from_slice(&typ.to_le_bytes());
e[24..32].copy_from_slice(&off.to_le_bytes());
e[32..40].copy_from_slice(&size.to_le_bytes());
f.extend_from_slice(&e);
};
sh(0, 0, 0, 0); sh(1, 1, 0, 0); sh(6, 3, shstr_off, shstrtab.len() as u64); f
}
#[test]
fn detects_bun_section_binary() {
let f = elf_with_bun_section();
assert!(has_bun_section(&f));
assert!(!has_self_extract_trailer(&f));
assert!(has_embedded_payload(&f));
}
#[test]
fn detects_pre_1_3_12_trailer() {
let mut f = vec![0u8; 64];
f.extend_from_slice(b"\n---- Bun! ----\n");
assert!(has_self_extract_trailer(&f));
assert!(has_embedded_payload(&f));
}
#[test]
fn plain_elf_is_not_embedded_payload() {
let mut f = vec![0u8; 64];
f[0..4].copy_from_slice(b"\x7fELF");
f[4] = 2;
f[5] = 1;
assert!(!has_bun_section(&f));
assert!(!has_self_extract_trailer(&f));
assert!(!has_embedded_payload(&f));
}
}
#[cfg(test)]
mod env_class_tests {
use super::*;
#[test]
fn each_elf_class_gets_its_own_env_constructor() {
let Some(blob32) = crate::payload::onelf_env_blob(goblin::elf::header::EM_386) else {
return;
};
let dir = std::env::temp_dir().join(format!("onelf-envclass-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
let lib = dir.join("lib");
fs::create_dir_all(dir.join("bin")).unwrap();
fs::copy(std::env::current_exe().unwrap(), dir.join("bin/app64")).unwrap();
fs::write(dir.join("bin/app32"), blob32).unwrap();
for bin in ["bin/app64", "bin/app32"] {
let _ = add_onelf_env_needed(&dir.join(bin), &lib);
}
let class = |name: &str| fs::read(lib.join(name)).ok().map(|b| b[4]);
let (class64, class32) = (class("libonelf-env.so"), class("libonelf-env32.so"));
let _ = fs::remove_dir_all(&dir);
assert_eq!(class64, Some(2));
assert_eq!(class32, Some(1));
}
}
#[cfg(test)]
mod runpath_tests {
use super::*;
const GUARD: u8 = 0xAA;
const GUARD_LEN: usize = 32;
fn elf_with_runpath(runpath: &[u8]) -> (Vec<u8>, usize) {
elf_with_runpath_and(runpath, true)
}
fn elf_with_runpath_and(runpath: &[u8], section_headers: bool) -> (Vec<u8>, usize) {
const SHSTRTAB: &[u8] = b"\0.dynstr\0.shstrtab\0";
let mut dynstr = vec![0u8];
dynstr.extend_from_slice(runpath);
dynstr.push(0);
let mut f = vec![0u8; 64];
f[0..4].copy_from_slice(b"\x7fELF");
f[4] = 2; f[5] = 1; f[6] = 1; f[16..18].copy_from_slice(&2u16.to_le_bytes()); f[18..20].copy_from_slice(&0x3eu16.to_le_bytes()); f[20..24].copy_from_slice(&1u32.to_le_bytes()); f[32..40].copy_from_slice(&64u64.to_le_bytes()); f[52..54].copy_from_slice(&64u16.to_le_bytes()); f[54..56].copy_from_slice(&56u16.to_le_bytes()); f[56..58].copy_from_slice(&2u16.to_le_bytes()); f[58..60].copy_from_slice(&64u16.to_le_bytes()); if section_headers {
f[60..62].copy_from_slice(&3u16.to_le_bytes()); f[62..64].copy_from_slice(&2u16.to_le_bytes()); }
let load_off = f.len();
f.extend_from_slice(&[0u8; 56]);
let ph_off = f.len();
f.extend_from_slice(&[0u8; 56]);
let dynstr_off = f.len() as u64;
f.extend_from_slice(&dynstr);
let guard_off = f.len();
f.extend_from_slice(&[GUARD; GUARD_LEN]);
while !f.len().is_multiple_of(8) {
f.push(0);
}
let dyn_off = f.len() as u64;
for (tag, val) in [
(goblin::elf::dynamic::DT_RUNPATH, 1u64),
(goblin::elf::dynamic::DT_STRTAB, dynstr_off),
(goblin::elf::dynamic::DT_STRSZ, dynstr.len() as u64),
(0, 0),
] {
f.extend_from_slice(&tag.to_le_bytes());
f.extend_from_slice(&val.to_le_bytes());
}
let dyn_size = f.len() as u64 - dyn_off;
f[ph_off..ph_off + 4].copy_from_slice(&2u32.to_le_bytes()); f[ph_off + 8..ph_off + 16].copy_from_slice(&dyn_off.to_le_bytes());
f[ph_off + 16..ph_off + 24].copy_from_slice(&dyn_off.to_le_bytes());
f[ph_off + 32..ph_off + 40].copy_from_slice(&dyn_size.to_le_bytes());
f[ph_off + 40..ph_off + 48].copy_from_slice(&dyn_size.to_le_bytes());
let file_len = f.len() as u64;
f[load_off..load_off + 4].copy_from_slice(&1u32.to_le_bytes()); f[load_off + 4..load_off + 8].copy_from_slice(&5u32.to_le_bytes()); f[load_off + 32..load_off + 40].copy_from_slice(&file_len.to_le_bytes());
f[load_off + 40..load_off + 48].copy_from_slice(&file_len.to_le_bytes());
f[load_off + 48..load_off + 56].copy_from_slice(&0x1000u64.to_le_bytes());
if !section_headers {
return (f, guard_off);
}
let shstr_off = f.len() as u64;
f.extend_from_slice(SHSTRTAB);
while !f.len().is_multiple_of(8) {
f.push(0);
}
let sh_off = f.len() as u64;
f[40..48].copy_from_slice(&sh_off.to_le_bytes());
let mut sh = |name: u32, typ: u32, off: u64, size: u64| {
let mut e = vec![0u8; 64];
e[0..4].copy_from_slice(&name.to_le_bytes());
e[4..8].copy_from_slice(&typ.to_le_bytes());
e[24..32].copy_from_slice(&off.to_le_bytes());
e[32..40].copy_from_slice(&size.to_le_bytes());
f.extend_from_slice(&e);
};
sh(0, 0, 0, 0); sh(1, 3, dynstr_off, dynstr.len() as u64); sh(9, 3, shstr_off, SHSTRTAB.len() as u64);
(f, guard_off)
}
fn rewrite(tag: &str, bytes: &[u8]) -> Vec<u8> {
let dir = std::env::temp_dir().join(format!("onelf-runpath-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let path = dir.join("lib.so");
fs::write(&path, bytes).unwrap();
finalize_one(&path, "$ORIGIN/../lib", "$ORIGIN/../lib", None).unwrap();
let out = fs::read(&path).unwrap();
let _ = fs::remove_dir_all(&dir);
out
}
#[test]
fn short_slot_is_scrubbed_without_touching_the_next_section() {
let old = b"/usr/lib/tinysparql-3.0";
let (bytes, guard_off) = elf_with_runpath(old);
let out = rewrite("short", &bytes);
assert_eq!(out.len(), bytes.len());
assert!(
out[guard_off..guard_off + GUARD_LEN]
.iter()
.all(|&b| b == GUARD)
);
let slot = &out[guard_off - old.len() - 1..guard_off];
let written = &slot[..slot.iter().position(|&b| b == 0).unwrap()];
assert!(written.is_empty() || written.starts_with(b"$ORIGIN"));
assert!(!out.windows(old.len()).any(|w| w == old));
}
#[test]
fn a_section_stripped_object_is_still_rewritten() {
let old = b"/usr/lib/tinysparql-3.0";
let (bytes, guard_off) = elf_with_runpath_and(old, false);
let out = rewrite("stripped", &bytes);
assert_eq!(out.len(), bytes.len());
assert!(
out[guard_off..guard_off + GUARD_LEN]
.iter()
.all(|&b| b == GUARD)
);
assert!(!out.windows(old.len()).any(|w| w == old));
}
#[test]
fn a_slot_left_for_patchelf_is_still_scrubbed() {
let old = b"/usr/lib/tinysparql-3.0";
let (bytes, _) = elf_with_runpath(old);
let dir = std::env::temp_dir().join(format!("onelf-runpath-owed-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let path = dir.join("lib.so");
fs::write(&path, &bytes).unwrap();
let (outcome, _) = finalize_one(
&path,
"$ORIGIN/../lib:$ORIGIN/../lib/tinysparql-3.0",
"$ORIGIN/../lib",
None,
)
.unwrap();
assert_eq!(outcome, RunpathOutcome::Unguaranteed);
let out = fs::read(&path).unwrap();
let _ = fs::remove_dir_all(&dir);
assert!(!out.windows(old.len()).any(|w| w == old));
let elf = goblin::elf::Elf::parse(&out).unwrap();
let tags: Vec<u64> = elf.dynamic.unwrap().dyns.iter().map(|d| d.d_tag).collect();
assert!(tags.contains(&goblin::elf::dynamic::DT_RPATH));
assert!(!tags.contains(&goblin::elf::dynamic::DT_RUNPATH));
}
#[test]
fn the_runpath_reaches_lib_from_every_depth() {
let lib = Path::new("lib");
assert_eq!(origin_runpath(0, lib), "$ORIGIN/lib");
assert_eq!(origin_runpath(1, lib), "$ORIGIN/../lib");
assert_eq!(origin_runpath(3, lib), "$ORIGIN/../../../lib");
let root = Path::new("/tree");
assert_eq!(
origin_runpath_for(root, Path::new("/tree/opt/x/bin/app"), lib),
"$ORIGIN/../../../lib"
);
assert_eq!(
origin_runpath_for(root, Path::new("/tree/lib/libfoo.so"), lib),
"$ORIGIN/../lib"
);
assert_eq!(
origin_runpath_for(root, Path::new("/tree/app"), Path::new("usr/lib")),
"$ORIGIN/usr/lib"
);
assert_eq!(
origin_runpath_for(root, Path::new("/tree/lib/gconv/EUC-CN.so"), lib),
"$ORIGIN:$ORIGIN/../../lib"
);
}
#[test]
fn large_enough_slot_is_rewritten_in_place() {
let (bytes, guard_off) = elf_with_runpath(&[b'x'; 80]);
let out = rewrite("large", &bytes);
assert_eq!(out.len(), bytes.len());
assert!(
out[guard_off..guard_off + GUARD_LEN]
.iter()
.all(|&b| b == GUARD)
);
let written = &out[guard_off - 81..guard_off - 81 + "$ORIGIN/../lib".len()];
assert_eq!(written, b"$ORIGIN/../lib");
}
}
#[cfg(test)]
mod scrub_tests {
use super::*;
const MUSL_LIBC: &[u8] =
b"\0/etc/passwd\0/etc/resolv.conf\0/usr/share/zoneinfo/\0/lib:/usr/local/lib:/usr/lib\0";
const GLIBC_LDSO: &[u8] =
b"\x7fELF\0/etc/ld.so.cache\0/etc/ld.so.preload\0/usr/lib64/\0/lib64/\0";
fn scrub(tag: &str, bytes: &[u8], family: LibcFamily) -> Vec<u8> {
let dir = std::env::temp_dir().join(format!("onelf-scrub-{tag}-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let path = dir.join("loader");
fs::write(&path, bytes).unwrap();
scrub_loader_paths(&path, family).unwrap();
let out = fs::read(&path).unwrap();
fs::remove_dir_all(&dir).unwrap();
out
}
#[test]
fn musl_keeps_the_paths_libc_reads_at_runtime() {
let out = scrub("musl", MUSL_LIBC, LibcFamily::Musl);
assert_eq!(out.len(), MUSL_LIBC.len());
assert_eq!(
out,
b"\0/etc/passwd\0/etc/resolv.conf\0/usr/share/zoneinfo/\0/XXX:/XXX/local/lib:/XXX/lib\0"
);
}
#[test]
fn glibc_loses_every_host_prefix() {
let out = scrub("glibc", GLIBC_LDSO, LibcFamily::Glibc);
assert_eq!(out.len(), GLIBC_LDSO.len());
assert_eq!(
out,
b"\x7fELF\0/XXX/ld.so.cache\0/XXX/ld.so.preload\0/XXX/XXX///\0/XXX///\0"
);
}
#[test]
fn a_glibc_loader_that_is_not_elf_is_refused_unchanged() {
let dir = std::env::temp_dir().join(format!("onelf-scrub-notelf-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let path = dir.join("loader");
let bytes = &GLIBC_LDSO[4..];
fs::write(&path, bytes).unwrap();
let err = scrub_loader_paths(&path, LibcFamily::Glibc).unwrap_err();
let out = fs::read(&path).unwrap();
fs::remove_dir_all(&dir).unwrap();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert_eq!(err.to_string(), "the image is not an ELF object");
assert_eq!(out, bytes);
}
}
#[cfg(test)]
mod audit_tests {
use super::*;
#[test]
fn a_dangling_soname_link_satisfies_no_need() {
let tree = std::env::temp_dir().join(format!("onelf-audit-{}", std::process::id()));
let _ = fs::remove_dir_all(&tree);
fs::create_dir_all(tree.join("bin")).unwrap();
fs::create_dir_all(tree.join("lib")).unwrap();
fs::copy(std::env::current_exe().unwrap(), tree.join("bin/app")).unwrap();
std::os::unix::fs::symlink("libc.so.6.pruned", tree.join("lib/libc.so.6")).unwrap();
let findings = audit_unbundled_needs(&tree, Path::new("lib"), &[]);
let _ = fs::remove_dir_all(&tree);
assert!(
findings
.iter()
.any(|(_, libs)| libs.iter().any(|l| l == "libc.so.6")),
"a link to nothing is reported missing: {findings:?}"
);
}
}