use std::fmt;
pub const MAX_LOADER_BYTES: usize = 32 * 1024 * 1024;
const GLIBC_REPLACEMENTS: &[(&[u8], &[u8])] = &[
(b"/etc/", b"/XXX/"),
(b"/usr/", b"/XXX/"),
(b"/nix/", b"/XXX/"),
(b"/lib/", b"/XXX/"),
(b"/lib64/", b"/XXX///"),
(b"/lib32/", b"/XXX///"),
(b"/libx32/", b"/XXX////"),
];
const MAX_DEFAULT_DIR_LEN: usize = 128;
const STORE_HASH_LEN: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LoaderScrub {
pub default_dirs: usize,
pub replaced: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoaderScrubError {
TooLarge(usize),
NotElf,
}
impl fmt::Display for LoaderScrubError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLarge(len) => write!(
f,
"the image is {len} bytes, more than the {MAX_LOADER_BYTES} a loader may have"
),
Self::NotElf => f.write_str("the image is not an ELF object"),
}
}
}
impl std::error::Error for LoaderScrubError {}
pub fn is_glibc_loader_name(name: &str) -> bool {
name.starts_with("ld-linux") || matches!(name, "ld.so.1" | "ld64.so.1" | "ld64.so.2")
}
pub fn scrub_glibc_loader(image: &mut [u8]) -> Result<LoaderScrub, LoaderScrubError> {
if image.len() > MAX_LOADER_BYTES {
return Err(LoaderScrubError::TooLarge(image.len()));
}
if !image.starts_with(b"\x7fELF") {
return Err(LoaderScrubError::NotElf);
}
let default_dirs = count_default_dirs(image);
let replaced = replace_equal_length(image, GLIBC_REPLACEMENTS);
Ok(LoaderScrub {
default_dirs,
replaced,
})
}
pub fn replace_equal_length(data: &mut [u8], pairs: &[(&[u8], &[u8])]) -> usize {
let mut replaced = 0;
for (needle, replacement) in pairs {
debug_assert_eq!(needle.len(), replacement.len());
let len = needle.len();
if len == 0 {
continue;
}
let mut i = 0;
while i + len <= data.len() {
if data[i..i + len] == **needle {
data[i..i + len].copy_from_slice(replacement);
replaced += 1;
i += len;
} else {
i += 1;
}
}
}
replaced
}
fn count_default_dirs(image: &[u8]) -> usize {
let mut count = 0;
let mut run_start = Some(0);
for (i, &b) in image.iter().enumerate() {
if b == 0 {
if let Some(start) = run_start
&& is_default_dir(&image[start..i])
{
count += 1;
}
run_start = Some(i + 1);
} else if !is_path_byte(b) {
run_start = Some(i + 1);
} else if run_start.is_some_and(|start| i - start >= MAX_DEFAULT_DIR_LEN) {
run_start = None;
}
}
count
}
fn is_path_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'/' | b'_' | b'-' | b'.' | b'+')
}
fn is_default_dir(dir: &[u8]) -> bool {
is_system_lib_dir(dir) || is_store_lib_dir(dir)
}
fn is_system_lib_dir(dir: &[u8]) -> bool {
let rest = dir.strip_prefix(b"/usr").unwrap_or(dir);
let Some(rest) = rest.strip_prefix(b"/lib") else {
return false;
};
let rest = [&b"64"[..], b"32", b"x32"]
.iter()
.find_map(|suffix| rest.strip_prefix(*suffix))
.unwrap_or(rest);
let Some(rest) = rest.strip_prefix(b"/") else {
return false;
};
if rest.is_empty() {
return true;
}
rest.strip_suffix(b"/").is_some_and(is_multiarch_triplet)
}
fn is_store_lib_dir(dir: &[u8]) -> bool {
let Some(rest) = dir.strip_prefix(b"/") else {
return false;
};
let mut parts = rest.split(|&b| b == b'/');
let (Some(root), Some(b"store"), Some(entry), Some(b"lib"), Some(b""), None) = (
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
) else {
return false;
};
let lower_alnum = |b: &u8| b.is_ascii_lowercase() || b.is_ascii_digit();
let Some((hash, name)) = entry.split_at_checked(STORE_HASH_LEN) else {
return false;
};
!root.is_empty()
&& root.iter().all(u8::is_ascii_lowercase)
&& hash.iter().all(lower_alnum)
&& name.starts_with(b"-glibc-")
}
fn is_multiarch_triplet(name: &[u8]) -> bool {
name.windows(7).any(|w| w == b"-linux-")
&& name
.iter()
.all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn image(body: &[u8]) -> Vec<u8> {
let mut out = b"\x7fELF\x02\x01\x01\0".to_vec();
out.extend_from_slice(body);
out
}
fn scrubbed(body: &[u8]) -> (Vec<u8>, LoaderScrub) {
let mut data = image(body);
let outcome = scrub_glibc_loader(&mut data).unwrap();
(data, outcome)
}
#[test]
fn each_layout_loses_its_default_directories() {
let layouts: &[(&[u8], usize)] = &[
(b"\0/lib64/\0/usr/lib64/\0", 2),
(
b"\0/lib/x86_64-linux-gnu/\0/usr/lib/x86_64-linux-gnu/\0/lib/\0/usr/lib/\0",
4,
),
(
b"\0/lib/aarch64-linux-gnu/\0/usr/lib/aarch64-linux-gnu/\0/lib/\0/usr/lib/\0",
4,
),
(b"\0/usr/lib/\0", 1),
(b"\0/lib32/\0/usr/lib32/\0", 2),
(b"\0/libx32/\0/usr/libx32/\0", 2),
(
b"\x01/nix/store/0123456789abcdefghijklmnopqrstuv-glibc-2.40-66/lib/\0",
1,
),
(
b"\0/gnu/store/0123456789abcdefghijklmnopqrstuv-glibc-2.39/lib/\0",
1,
),
];
for (body, dirs) in layouts {
let (out, outcome) = scrubbed(body);
assert_eq!(outcome.default_dirs, *dirs, "{body:?}");
assert_eq!(out.len(), image(body).len());
assert_eq!(count_default_dirs(&out), 0, "{out:?}");
assert!(!out.windows(4).any(|w| w == b"/lib"), "{out:?}");
}
}
#[test]
fn the_cache_and_preload_files_are_closed_too() {
let (out, outcome) =
scrubbed(b"\0/etc/ld.so.cache\0/etc/ld.so.preload\0/usr/lib64/\0/lib64/\0");
assert_eq!(outcome.default_dirs, 2);
assert_eq!(
&out[8..],
b"\0/XXX/ld.so.cache\0/XXX/ld.so.preload\0/XXX/XXX///\0/XXX///\0"
);
}
#[test]
fn a_directory_after_binary_data_counts() {
let (_, outcome) = scrubbed(b"\x80\x01/usr/lib64/\0");
assert_eq!(outcome.default_dirs, 1);
}
#[test]
fn paths_that_only_contain_a_default_directory_do_not_count() {
for body in [
&b"\0/usr/lib64/ld-linux-x86-64.so.2\0"[..],
b"\0/opt/lib/\0",
b"\0/usr/lib/locale\0",
b"\0/lib/not a triplet/\0",
b"\0/usr/lib/X86_64-linux-gnu/\0",
b"\0/usr/lib/gcc/\0",
b"/usr/lib",
b"\0/nix/store/0123456789abcdefghijklmnopqrstuv-zlib-1.3/lib/\0",
b"\0/nix/store/0123456789-glibc-2.40/lib/\0",
b"\0/nix/store/0123456789abcdefghijklmnopqrstuv-glibc-2.40/lib/gconv/\0",
b"\0/nix/store/0123456789abcdefghijklmnopqrstuv-glibc-2.40/\0",
b"\0/nix/var/0123456789abcdefghijklmnopqrstuv-glibc-2.40/lib/\0",
b"\0/Nix/store/0123456789abcdefghijklmnopqrstuv-glibc-2.40/lib/\0",
b"\0//store/0123456789abcdefghijklmnopqrstuv-glibc-2.40/lib/\0",
] {
let (_, outcome) = scrubbed(body);
assert_eq!(outcome.default_dirs, 0, "{body:?}");
}
}
#[test]
fn an_overlong_run_ending_in_a_directory_does_not_count() {
let mut body = vec![b'a'; 4 * MAX_DEFAULT_DIR_LEN];
body.extend_from_slice(b"/lib64/\0");
let (_, outcome) = scrubbed(&body);
assert_eq!(outcome.default_dirs, 0);
}
#[test]
fn malformed_images_are_refused_untouched() {
let mut text = b"/usr/lib64/\0".to_vec();
assert_eq!(scrub_glibc_loader(&mut text), Err(LoaderScrubError::NotElf));
assert_eq!(text, b"/usr/lib64/\0");
assert_eq!(scrub_glibc_loader(&mut []), Err(LoaderScrubError::NotElf));
assert_eq!(
scrub_glibc_loader(&mut b"\x7fEL".to_vec()),
Err(LoaderScrubError::NotElf)
);
let mut huge = image(&vec![0; MAX_LOADER_BYTES]);
assert_eq!(
scrub_glibc_loader(&mut huge),
Err(LoaderScrubError::TooLarge(huge.len()))
);
}
#[test]
fn every_glibc_loader_name_is_recognized() {
for name in [
"ld-linux-x86-64.so.2",
"ld-linux.so.2",
"ld-linux-aarch64.so.1",
"ld-linux-armhf.so.3",
"ld-linux-riscv64-lp64d.so.1",
"ld-linux-loongarch-lp64d.so.1",
"ld.so.1",
"ld64.so.1",
"ld64.so.2",
] {
assert!(is_glibc_loader_name(name), "{name}");
}
for name in [
"ld-musl-x86_64.so.1",
"ld.so",
"libc.so.6",
"ld64.so.3",
"ld.so.2",
] {
assert!(!is_glibc_loader_name(name), "{name}");
}
}
#[test]
fn an_empty_needle_is_skipped() {
let mut data = b"/usr/lib".to_vec();
assert_eq!(replace_equal_length(&mut data, &[(b"", b"")]), 0);
assert_eq!(data, b"/usr/lib");
}
fn runs(loader: &Path, args: &[&str]) -> bool {
for _ in 0..100 {
match std::process::Command::new(loader)
.args(args)
.env_remove("LD_LIBRARY_PATH")
.env_remove("LD_PRELOAD")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
Ok(status) => return status.success(),
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(_) => return false,
}
}
false
}
#[test]
fn real_loaders_are_closed() {
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
let candidates = [
"/lib64/ld-linux-x86-64.so.2",
"/usr/lib/ld-linux-x86-64.so.2",
"/usr/lib64/ld-linux-x86-64.so.2",
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
"/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
"/lib/ld-linux-aarch64.so.1",
"/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1",
"/usr/aarch64-linux-gnu/usr/lib/ld-linux-aarch64.so.1",
"/lib/ld-linux.so.2",
"/usr/lib32/ld-linux.so.2",
"/usr/powerpc-linux-gnu/usr/lib/ld.so.1",
"/usr/powerpc64-linux-gnu/usr/lib/ld64.so.2",
"/usr/powerpc64le-linux-gnu/usr/lib/ld64.so.2",
];
let dir = std::env::temp_dir().join(format!(
"onelf-ldso-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::DirBuilder::new().mode(0o700).create(&dir).unwrap();
let mut checked = 0;
for (i, path) in candidates.iter().enumerate() {
let Ok(original) = std::fs::read(path) else {
continue;
};
let is_glibc_loader = original.len() >= 64
&& original.starts_with(b"\x7fELF")
&& original.windows(11).any(|w| w == b"ld.so.cache");
if !is_glibc_loader {
continue;
}
let mut data = original.clone();
let outcome = scrub_glibc_loader(&mut data).unwrap();
assert!(outcome.default_dirs > 0, "{path}");
assert_eq!(data.len(), original.len(), "{path}");
assert_eq!(count_default_dirs(&data), 0, "{path}");
assert_eq!(data[..64], original[..64], "{path}");
checked += 1;
let place = |name: String, bytes: &[u8]| {
let copy = dir.join(name);
std::fs::write(©, bytes).unwrap();
std::fs::set_permissions(©, std::fs::Permissions::from_mode(0o700)).unwrap();
copy
};
let unmodified = place(format!("ld-{i}.orig.so"), &original);
if !runs(&unmodified, &["--version"]) {
eprintln!("{path}: run checks skipped, an unmodified copy does not run here");
continue;
}
let original_runs_true = runs(&unmodified, &["--inhibit-cache", "/bin/true"]);
let copy = place(format!("ld-{i}.so"), &data);
assert!(runs(©, &["--version"]), "{path} runs once closed");
if original_runs_true {
assert!(
!runs(©, &["--inhibit-cache", "/bin/true"]),
"{path} closed still finds a library in a default directory"
);
} else {
eprintln!(
"{path}: closure check skipped, the unmodified copy cannot run /bin/true"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
if cfg!(all(
target_os = "linux",
target_env = "gnu",
target_arch = "x86_64"
)) {
assert!(
checked > 0,
"a glibc x86_64 host has a loader at one of {candidates:?}"
);
}
}
}