From 082e7a0566e2c79ab0cad1262454d6a9287bbe33 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Fri, 19 Jun 2026 17:38:39 -0600 Subject: [PATCH] Bundle relocated interpreter's libdir into the dist toolchain package sccache-dist packages a toolchain's shared libraries by parsing `ldd` output, which resolves NEEDED libraries against the host's dynamic loader. Yocto/OpenEmbedded "uninative" cross toolchains ship a relocated glibc whose loader has a built-in search path pointing at its own sysroot. For those binaries `ldd` reports host paths (e.g. /usr/lib/libm.so.6), yet inside the build sandbox the relocated loader searches its own sysroot lib dir, where those libraries were never packaged. The remote compile then dies with "libm.so.6: cannot open shared object file" and silently falls back to local compilation, so distribution never actually runs. When a packaged executable's PT_INTERP lives outside the standard host loader directories, also bundle the interpreter's own directory. That directory holds the libc/libm the relocated loader resolves against, so they land at the absolute path the loader searches inside the sandbox. Standard host toolchains are untouched: their interpreter is under /lib, /lib64, or /usr/lib, so the existing ldd-only path is preserved. Signed-off-by: Javier Tia --- src/dist/pkg.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/dist/pkg.rs b/src/dist/pkg.rs index 8bc1744a4..e7cab535c 100644 --- a/src/dist/pkg.rs +++ b/src/dist/pkg.rs @@ -98,6 +98,20 @@ mod toolchain_imp { } pub fn add_executable_and_deps(&mut self, executable: PathBuf) -> Result<()> { + // A relocated program interpreter (e.g. a Yocto/OE uninative + // toolchain) resolves its libc/libm against its own sysroot, not the + // host paths ldd reports. Bundle the interpreter's directory so those + // libraries exist where the loader looks inside the build sandbox. + if let Some(libdir) = read_elf_interpreter(&executable) + .and_then(|interp| relocated_interpreter_libdir(&interp)) + { + self.add_dir_contents(&libdir).with_context(|| { + format!( + "Failed to bundle relocated interpreter libdir {}", + libdir.display() + ) + })?; + } let mut remaining = vec![executable]; while let Some(obj_path) = remaining.pop() { assert!(obj_path.is_absolute()); @@ -350,6 +364,50 @@ mod toolchain_imp { libs } + /// Read a binary's ELF program interpreter (PT_INTERP), if it has one. + /// + /// Returns `None` for static binaries, non-ELF or non-64-bit files, and + /// anything that fails to parse -- callers treat that as "nothing special + /// to bundle", preserving the default ldd-only behaviour. + fn read_elf_interpreter(executable: &Path) -> Option { + use object::Endianness; + use object::read::elf::{ElfFile64, ProgramHeader}; + + let data = fs::read(executable).ok()?; + let elf = ElfFile64::::parse(data.as_slice()).ok()?; + let endian = elf.endian(); + for header in elf.elf_program_headers() { + if let Ok(Some(interp)) = header.interpreter(endian, elf.data()) { + return str::from_utf8(interp).ok().map(PathBuf::from); + } + } + None + } + + /// If `interp` is a relocated program interpreter -- one living outside the + /// standard host loader directories -- return the directory that should be + /// bundled alongside the toolchain. + /// + /// Yocto/OpenEmbedded "uninative" cross toolchains ship their own glibc and + /// loader, and the loader's built-in search path points at its own sysroot + /// lib dir rather than the host's. `ldd` resolves a binary's NEEDED + /// libraries against the *host* loader, so for these binaries it reports + /// host paths (e.g. /usr/lib/libm.so.6) that do not exist where the + /// relocated loader actually searches at runtime. Bundling the + /// interpreter's own directory -- which holds the matching libc/libm and + /// the loader itself -- makes the toolchain resolvable inside the sandbox. + fn relocated_interpreter_libdir(interp: &Path) -> Option { + const STANDARD_LOADER_PREFIXES: &[&str] = &["/lib/", "/lib64/", "/usr/lib/", "/usr/lib64/"]; + let interp_str = interp.to_str()?; + if STANDARD_LOADER_PREFIXES + .iter() + .any(|prefix| interp_str.starts_with(prefix)) + { + return None; + } + interp.parent().map(Path::to_path_buf) + } + #[test] fn test_ldd_parse() { let ubuntu_ls_output = "\tlinux-vdso.so.1 => (0x00007fffcfffe000) @@ -409,6 +467,35 @@ mod toolchain_imp { ] ); } + + #[test] + fn test_relocated_interpreter_libdir() { + // Standard host loaders are left to ldd's host resolution. + for standard in [ + "/lib64/ld-linux-x86-64.so.2", + "/usr/lib64/ld-linux-x86-64.so.2", + "/usr/lib/ld-linux-aarch64.so.1", + "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + ] { + assert_eq!( + relocated_interpreter_libdir(Path::new(standard)), + None, + "{standard} is a standard host loader and must not trigger bundling" + ); + } + + // A relocated loader (e.g. a Yocto/OE uninative toolchain) resolves its + // libc/libm against its own sysroot lib dir, which ldd does not report. + // Return that directory so it gets bundled into the toolchain package. + assert_eq!( + relocated_interpreter_libdir(Path::new( + "/home/u/build/tmp/sysroots-uninative/x86_64-linux/lib/ld-linux-x86-64.so.2" + )), + Some(PathBuf::from( + "/home/u/build/tmp/sysroots-uninative/x86_64-linux/lib" + )) + ); + } } pub fn make_tar_header(src: &Path, dest: &str) -> io::Result {