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,
}
pub(crate) fn set_origin_runpath(path: &Path) -> io::Result<RunpathOutcome> {
const NEW: &str = "$ORIGIN/../lib:$ORIGIN/../../lib:$ORIGIN/../../../lib";
let new_bytes = NEW.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_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 dynamic_present = elf.dynamic.is_some();
let mut slots_total = 0usize;
let mut slots_rewritten = 0usize;
if let (Some(dynstr_offset), Some(dynamic)) = (dynstr_offset, &elf.dynamic) {
let mut modified = data.clone();
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 >= modified.len() {
continue;
}
let mut end = file_pos;
while end < modified.len() && modified[end] != 0 {
end += 1;
}
while end < modified.len() && 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(NEW)
.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 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();
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));
}
tally_origin_runpath(&path, &mut rewritten, &mut unguaranteed, &mut self_extract);
let before = fs::metadata(&path).and_then(|m| m.modified()).ok();
let _ = scrub_nix_store_paths(&path);
let _ = strip_absolute_needed(&path);
let after = fs::metadata(&path).and_then(|m| m.modified()).ok();
if before.is_some() && before != after {
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 tally_origin_runpath(
path: &Path,
set: &mut usize,
unguaranteed: &mut Vec<PathBuf>,
self_extract: &mut Vec<PathBuf>,
) {
match set_origin_runpath(path) {
Ok(RunpathOutcome::Set) => *set += 1,
Ok(RunpathOutcome::Unguaranteed) => unguaranteed.push(path.to_path_buf()),
Ok(RunpathOutcome::SelfExtract) => self_extract.push(path.to_path_buf()),
Ok(RunpathOutcome::NotNeeded) | Err(_) => {}
}
}
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(path: &Path) -> io::Result<()> {
let data = fs::read(path)?;
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
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) = dynstr_offset else {
return Ok(());
};
let Some(dynamic) = &elf.dynamic else {
return Ok(());
};
let mut modified = data;
let mut changed = false;
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 original = &modified[file_pos..end];
let slot_size = end - file_pos;
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;
}
let basename: Vec<u8> = original[basename_start..].to_vec();
modified[file_pos..file_pos + basename_len].copy_from_slice(&basename);
for i in basename_len..slot_size {
modified[file_pos + i] = 0;
}
changed = true;
}
if changed {
fs::write(path, &modified)?;
}
Ok(())
}
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(path: &Path) -> io::Result<()> {
let mut data = fs::read(path)?;
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 = i + suffix.len();
continue;
}
let mut end = i + suffix.len();
while end < data.len() && data[end] != 0 {
end += 1;
}
let slot = end - start;
if replacement.len() + 1 > slot {
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;
}
}
if changed {
fs::write(path, &data)?;
}
Ok(())
}
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) {
if elf
.libraries
.iter()
.any(|l| *l == 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() % 8 != 0 {
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));
}
}