Skip to content

Isolate feature doesn't work with $ORIGIN in DT_RUNPATH #38

@ComputerDruid

Description

@ComputerDruid

While reading the blog post at https://harrystern.net/extrasafe-user-namespaces.html I thought to myself: that doesn't sound like it would work, because it would mess up how binaries find their shared libraries when they set a DT_RUNPATH with $ORIGIN in it.

I thought it would be quick to check this by whipping up a quick paired dylib + binary crate, but I was very wrong about how quick it would be.

But after hours of futzing with things, I managed to get a binary which the dynamic loader loads fine outside the isolated environment, but fails inside:

It goes like:

dylib/Cargo.toml

[package]
name = "dylib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]

dylib/src/lib.rs

#[repr(C)]
pub struct S {
    ptr: *const u8,
    len: usize,
}


#[no_mangle]
pub extern "C" fn hello() -> S {
    let s = "hello world!".as_bytes();
    let ptr = s.as_ptr();
    let len = s.len();
    S { ptr, len }
}

example/Cargo.toml

[package]
name = "example"
version = "0.1.0"
edition = "2021"

[dependencies]
dylib = { path = "../dylib"  }
extrasafe = { version = "0.5.0", features = ["isolate"] }

example/src/main.rs

use extrasafe::isolate::Isolate;
use std::collections::HashMap;

mod sys {

    #[repr(C)]
    pub(crate) struct S {
        pub ptr: *const u8,
        pub len: usize,
    }

    #[link(name = "dylib")]
    extern "C" {
        pub(crate) fn hello() -> S;
    }
}

fn hello() -> &'static str {
    // Safety: FFI with no special requirements
    let s = unsafe { sys::hello() };
    // SAFETY: hello returns a &'static str in its raw parts
    let s: &'static [u8] = unsafe { std::slice::from_raw_parts(s.ptr, s.len) };
    std::str::from_utf8(s).unwrap()
}

fn isolate_print(name: &'static str) -> Isolate {
    fn print() {
        println!("{}", hello());
    }
    Isolate::new(name, print)
}

fn main() {
    //works outside isolate:
    println!("{}", hello());
    Isolate::main_hook("isolate_print", isolate_print);
    let output = Isolate::run("isolate_print", &HashMap::new()).unwrap();
    dbg!(output);
}

Then we just need some final hackery to set up DT_RUNPATH:

cd example
cargo build
# make the loader look for libdylib.so in deps/ next to the executable (which happens to be where cargo puts it)
patchelf --set-rpath '$ORIGIN/deps' target/debug/example
target/debug/example

Which gives me:

hello world!
[src/main.rs:38:5] output = Output {
    status: ExitStatus(
        unix_wait_status(
            32512,
        ),
    ),
    stdout: "",
    stderr: "isolate_print: error while loading shared libraries: libdylib.so: cannot open shared object file: No such file or directory\n",
}

and looking inside strace -f I can see the working open at the top:

execve("target/debug/example", ["target/debug/example"], 0x7ffebd5fd9a8 /* 85 vars */) = 0
brk(NULL)                               = 0x55afdf5e8000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f763d642000
readlinkat(AT_FDCWD, "/proc/self/exe", "/home/cdruid/src/extrasafe_examp"..., 4096) = 63
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/cdruid/src/extrasafe_example/example/target/debug/deps/glibc-hwcaps/x86-64-v3/libdylib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/home/cdruid/src/extrasafe_example/example/target/debug/deps/glibc-hwcaps/x86-64-v3/", 0x7ffda910c5c0, 0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/cdruid/src/extrasafe_example/example/target/debug/deps/glibc-hwcaps/x86-64-v2/libdylib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/home/cdruid/src/extrasafe_example/example/target/debug/deps/glibc-hwcaps/x86-64-v2/", 0x7ffda910c5c0, 0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/cdruid/src/extrasafe_example/example/target/debug/deps/libdylib.so", O_RDONLY|O_CLOEXEC) = 3

followed by the broken one down below:

[pid 581730] execve("/proc/self/fd/3", ["isolate_print"], 0x55afdf5e8c00 /* 0 vars */ <unfinished ...>
...
[pid 581730] <... execve resumed>)      = 0
[pid 581730] brk(NULL)                  = 0x55c303abd000
[pid 581730] mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f908a71b000
[pid 581730] readlinkat(AT_FDCWD, "/proc/self/exe", "/memfd:isolate_memfd (deleted)", 4096) = 30
[pid 581730] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[pid 581730] openat(AT_FDCWD, "//deps/glibc-hwcaps/x86-64-v3/libdylib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid 581730] newfstatat(AT_FDCWD, "//deps/glibc-hwcaps/x86-64-v3/", 0x7ffd54011770, 0) = -1 ENOENT (No such file or directory)
[pid 581730] openat(AT_FDCWD, "//deps/glibc-hwcaps/x86-64-v2/libdylib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid 581730] newfstatat(AT_FDCWD, "//deps/glibc-hwcaps/x86-64-v2/", 0x7ffd54011770, 0) = -1 ENOENT (No such file or directory)
[pid 581730] openat(AT_FDCWD, "//deps/libdylib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

Looking through man ld.so, it looks like the fix might be as simple as setting LD_ORIGIN_PATH based on dirname $(readlink /proc/self/exe)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions