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_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())
}
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 name.starts_with("ld-linux") {
Some(LibcFamily::Glibc)
} else {
None
}
}
pub(crate) fn libc_family_of_soname(soname: &str) -> Option<LibcFamily> {
if soname == "libc.so.6" || soname.starts_with("ld-linux") {
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)
}
}
pub(crate) fn resolve_runpath_dirs<'a>(
raw: impl Iterator<Item = &'a str>,
origin: &Path,
) -> Vec<PathBuf> {
raw.flat_map(|s| s.split(':'))
.filter(|s| !s.is_empty())
.map(|entry| expand_runpath_entry(entry, origin))
.filter(|p| p.is_dir())
.collect()
}
pub(crate) fn parse_rpaths(path: &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(raw.iter().copied(), origin)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RunpathOutcome {
Set,
NotNeeded,
Unguaranteed,
SelfExtract,
NeedsPatchelf,
}
const ORIGIN_RUNPATH: &str = "$ORIGIN/lib:$ORIGIN/../lib:$ORIGIN/../../lib:$ORIGIN/../../../lib";
fn rewrite_origin_runpath_in(data: &mut [u8], path: &Path) -> io::Result<RunpathOutcome> {
let new_bytes = ORIGIN_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 = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".dynstr"))
.map(|sh| (sh.sh_offset as usize, (sh.sh_offset + sh.sh_size) as usize));
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 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;
if new_bytes.len() + 1 > slot_size {
continue;
}
data[file_pos..file_pos + new_bytes.len()].copy_from_slice(new_bytes);
for b in &mut data[file_pos + new_bytes.len()..file_pos + slot_size] {
*b = 0;
}
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());
}
}
}
if total > 0 && rewritten == total {
return Ok(RunpathOutcome::Set);
}
if total > 0 && !is_executable {
return Ok(RunpathOutcome::Set);
}
if !dynamic_present || !has_needed {
return Ok(RunpathOutcome::NotNeeded);
}
if !is_executable {
return Ok(RunpathOutcome::NotNeeded);
}
if is_self_extract {
return Ok(RunpathOutcome::SelfExtract);
}
let _ = path;
Ok(RunpathOutcome::NeedsPatchelf)
}
fn run_patchelf_rpath(path: &Path, 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(ORIGIN_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
}
}
}
pub(crate) fn set_origin_runpath(path: &Path) -> io::Result<RunpathOutcome> {
let new_bytes = ORIGIN_RUNPATH.as_bytes();
let data = fs::read(path)?;
let is_self_extract = has_embedded_payload(&data);
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| 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 = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".dynstr"))
.map(|sh| (sh.sh_offset as usize, (sh.sh_offset + sh.sh_size) as usize));
let dynamic_present = elf.dynamic.is_some();
let mut slots_total = 0usize;
let mut slots_rewritten = 0usize;
if let (Some((dynstr_offset, dynstr_end)), Some(dynamic)) = (dynstr_range, &elf.dynamic) {
let mut modified = data.clone();
let limit = dynstr_end.min(modified.len());
for dyn_entry in &dynamic.dyns {
if dyn_entry.d_tag == goblin::elf::dynamic::DT_RPATH
|| dyn_entry.d_tag == goblin::elf::dynamic::DT_RUNPATH
{
slots_total += 1;
let file_pos = dynstr_offset + dyn_entry.d_val as usize;
if file_pos >= limit {
continue;
}
let mut end = file_pos;
while end < limit && modified[end] != 0 {
end += 1;
}
while end < limit && modified[end] == 0 {
end += 1;
}
let slot_size = end - file_pos;
if new_bytes.len() + 1 > slot_size {
continue;
}
modified[file_pos..file_pos + new_bytes.len()].copy_from_slice(new_bytes);
for i in new_bytes.len()..slot_size {
modified[file_pos + i] = 0;
}
slots_rewritten += 1;
}
}
if slots_total > 0 && slots_rewritten == slots_total {
fs::write(path, &modified)?;
return Ok(RunpathOutcome::Set);
}
}
drop(elf);
if !dynamic_present || !has_needed {
return Ok(RunpathOutcome::NotNeeded);
}
if !is_executable {
return Ok(RunpathOutcome::NotNeeded);
}
if is_self_extract {
return Ok(RunpathOutcome::SelfExtract);
}
if let Some(patchelf) = which_patchelf() {
let status = std::process::Command::new(&patchelf)
.arg("--force-rpath")
.arg("--set-rpath")
.arg(ORIGIN_RUNPATH)
.arg(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();
match status {
Ok(o) if o.status.success() => return Ok(RunpathOutcome::Set),
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
eprintln!(
" {} patchelf failed for {}: {}",
color::bold_red("warning:"),
path.display(),
stderr.trim()
);
}
Err(e) => {
eprintln!(
" {} could not run patchelf for {}: {e}",
color::bold_red("warning:"),
path.display(),
);
}
}
}
Ok(RunpathOutcome::Unguaranteed)
}
pub(crate) fn audit_unbundled_needs(directory: &Path) -> Vec<(PathBuf, Vec<String>)> {
let mut provided: std::collections::HashSet<String> = std::collections::HashSet::new();
for entry in jwalk::WalkDir::new(directory).sort(true) {
let Ok(entry) = entry else { continue };
if let Some(name) = entry.file_name().to_str() {
provided.insert(name.to_string());
}
}
let mut findings: Vec<(PathBuf, Vec<String>)> = Vec::new();
for path in find_elf_files(directory) {
let Ok(needed) = parse_needed(&path) else {
continue;
};
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);
!provided.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) -> (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) {
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));
}
if let Ok((outcome, did_scrub)) = finalize_one(&path, 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)
}
fn finalize_one(path: &Path, patchelf: Option<&Path>) -> io::Result<(RunpathOutcome, bool)> {
let mut data = fs::read(path)?;
let outcome = rewrite_origin_runpath_in(&mut data, path)?;
let scrubbed = scrub_nix_store_paths_in(&mut data);
let stripped = strip_absolute_needed_in(&mut data);
if outcome == RunpathOutcome::Set || scrubbed || stripped {
fs::write(path, &data)?;
}
if outcome == RunpathOutcome::NeedsPatchelf {
return Ok((run_patchelf_rpath(path, 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!(
"{} {} executable(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 = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".dynstr"))
.map(|sh| sh.sh_offset as usize);
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 {
soname.starts_with("ld-linux") || soname.starts_with("ld-musl-") || soname == "ld.so"
}
pub(crate) fn scrub_loader_paths(path: &Path) -> io::Result<()> {
let mut data = fs::read(path)?;
let mut changed = false;
let replacements: &[(&[u8], &[u8])] = &[
(b"/etc/", b"/XXX/"),
(b"/usr/", b"/XXX/"),
(b"/nix/", b"/XXX/"),
(b"/lib/", b"/XXX/"),
(b"/lib64/", b"/XXX///"),
];
for (needle, replace) in replacements {
debug_assert_eq!(needle.len(), replace.len());
let len = needle.len();
let mut i = 0;
while i + len <= data.len() {
if &data[i..i + len] == *needle {
data[i..i + len].copy_from_slice(replace);
changed = true;
i += len;
} else {
i += 1;
}
}
}
if changed {
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);
};
if let Ok(elf) = goblin::elf::Elf::parse(&data)
&& elf.libraries.contains(&crate::payload::ONELF_ENV_SONAME)
{
return Ok(EnvNeededOutcome::AlreadyPresent);
}
let dest = lib_dest.join(crate::payload::ONELF_ENV_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(crate::payload::ONELF_ENV_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) {
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();
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 runpath_tests {
use super::*;
const GUARD: u8 = 0xAA;
const GUARD_LEN: usize = 32;
fn elf_with_runpath(runpath: &[u8]) -> (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(&1u16.to_le_bytes()); f[58..60].copy_from_slice(&64u16.to_le_bytes()); f[60..62].copy_from_slice(&3u16.to_le_bytes()); f[62..64].copy_from_slice(&2u16.to_le_bytes());
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), (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 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();
set_origin_runpath(&path).unwrap();
let out = fs::read(&path).unwrap();
let _ = fs::remove_dir_all(&dir);
out
}
#[test]
fn short_slot_leaves_the_next_section_alone() {
let (bytes, guard_off) = elf_with_runpath(b"/usr/lib/tinysparql-3.0");
let out = rewrite("short", &bytes);
assert_eq!(out.len(), bytes.len());
assert!(
out[guard_off..guard_off + GUARD_LEN]
.iter()
.all(|&b| b == GUARD)
);
assert_eq!(&out[..guard_off], &bytes[..guard_off]);
}
#[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_RUNPATH.len()];
assert_eq!(written, ORIGIN_RUNPATH.as_bytes());
}
}