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)
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_RUNPATHwith$ORIGINin 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:
Then we just need some final hackery to set up
DT_RUNPATH:Which gives me:
and looking inside
strace -fI can see the working open at the top:followed by the broken one down below:
Looking through
man ld.so, it looks like the fix might be as simple as settingLD_ORIGIN_PATHbased ondirname $(readlink /proc/self/exe)