Intercepting libc readlink with a RUST LD_PRELOAD program hangs when applying to "cargo build"

It looks like just the act of intercepting readlink hangs when using on "cargo build" as a test

I am writing an open source LD_PRELOAD library that tracks file system activities.
While testing it I ran into hang.
I have simplified it as far as I can to get it to pretty much bare bones

  1. intercept readlink,
  2. Call the original readlink.
    There is nothing else going.

The test I am doing is to invoke
LD_PRELOAD=/path/libreadlink.so cargo build

I initialized a new project "cargo new"
Cargo.toml is as follows

[package]
name = "readlink"
version = "0.1.0"
authors = ["Saravanan Shanmugham <sarvi@cisco.com>"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "readlink"
crate_type = ["dylib"]

[dependencies]
libc = "0.2"

The code looks like this. If this looks familiar, its because it a fork of the redhook crate that helps write other ld_preload program. Infact, redhook, does have its own readlink interception example that works just fine by itself. But fails when running the LD_PRELOAD on "cargo build"
The example below is simpler and does away with all the macros and is simple enough to be cut and pare and executed.

extern crate core;
extern crate libc;


use libc::{c_void,c_char,c_int,size_t,ssize_t};

use std::sync::atomic;

#[cfg(any(target_os = "macos", target_os = "ios"))]
pub mod dyld_insert_libraries;

/* Some Rust library functionality (e.g., jemalloc) initializes
 * lazily, after the hooking library has inserted itself into the call
 * path. If the initialization uses any hooked functions, this will lead
 * to an infinite loop. Work around this by running some initialization
 * code in a static constructor, and bypassing all hooks until it has
 * completed. */

static INIT_STATE: atomic::AtomicBool = atomic::AtomicBool::new(false);

pub fn initialized() -> bool {
    INIT_STATE.load(atomic::Ordering::SeqCst)
}

extern "C" fn initialize() {
    Box::new(0u8);
    INIT_STATE.store(true, atomic::Ordering::SeqCst);
}


#[link(name = "dl")]
extern "C" {
    fn dlsym(handle: *const c_void, symbol: *const c_char) -> *const c_void;
}

const RTLD_NEXT: *const c_void = -1isize as *const c_void;

pub unsafe fn dlsym_next(symbol: &'static str) -> *const u8 {
    let ptr = dlsym(RTLD_NEXT, symbol.as_ptr() as *const c_char);
    if ptr.is_null() {
        panic!("redhook: Unable to find underlying function for {}", symbol);
    }
    ptr as *const u8
}

/* Rust doesn't directly expose __attribute__((constructor)), but this
 * is how GNU implements it. */
#[link_section = ".init_array"]
pub static INITIALIZE_CTOR: extern "C" fn() = ::initialize;



#[allow(non_camel_case_types)]
pub struct readlink {__private_field: ()}
#[allow(non_upper_case_globals)]
static readlink: readlink = readlink {__private_field: ()};

impl readlink {
    fn get(&self) -> unsafe extern fn (path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t  {
        use ::std::sync::Once;

        static mut REAL: *const u8 = 0 as *const u8;
        static mut ONCE: Once = Once::new();

        unsafe {
            ONCE.call_once(|| {
                REAL = dlsym_next(concat!("readlink", "\0"));
            });
            ::std::mem::transmute(REAL)
        }
    }

    #[no_mangle]
    pub unsafe extern "C" fn readlink(path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t {
        if initialized() {
            ::std::panic::catch_unwind(|| my_readlink ( path, buf, bufsiz )).ok()
        } else {
            None
        }.unwrap_or_else(|| readlink.get() ( path, buf, bufsiz ))
    }
}

pub unsafe fn my_readlink(path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t {
    readlink.get()(path, buf, bufsiz)
}

Once setup I do the following

cargo clean
cargo build
#Copy the libreadlink.so to local, so as to not cleaup when doing the test
cp target/debug/libreadlink.so .
cargo clean; LD_PRELOAD=`pwd`/libreadlink.so cargo build

This hangs.
Ran an strace

brk(NULL)                               = 0x5644c60b7000
read(3, "U5qfeCTiRShFAh6d8WWQUe7UREN3+v9X"..., 4096) = 4096
read(3, "LLCo4MBANzX2hFxc469CeP6nyQ1Q6g2E"..., 4096) = 4096
read(3, "\nQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3I"..., 4096) = 4096
read(3, "P8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0Y"..., 4096) = 4096
read(3, "dGlmaWNhdGlvbiBBdXRob3JpdHkwHhcN"..., 4096) = 4096
read(3, "PVQT60nKWVSFJuUrjxuf6/WhkcIz\nSdh"..., 4096) = 4096
read(3, "TxzhT5yvDwyd93gN2PQ1VoDat20Xj50e"..., 4096) = 4096
read(3, "gMCGgUAMAcGBWcqAwAABBRFsMLH\nClZ8"..., 4096) = 4096
brk(NULL)                               = 0x5644c60b7000
brk(0x5644c60d8000)                     = 0x5644c60d8000
read(3, "GcxCzAJBgNVBAYTAklO\nMRMwEQYDVQQL"..., 4096) = 4096
read(3, "9yaXpl\nZCB1c2Ugb25seTEkMCIGA1UEA"..., 4096) = 964
read(3, "", 4096)                       = 0
close(3)                                = 0
getuid()                                = 19375
geteuid()                               = 19375
getgid()                                = 25
getegid()                               = 25
futex(0x5644c553fcb0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x5644c553f634, FUTEX_WAKE_PRIVATE, 2147483647) = 0
access("/users/sarvi/.gitconfig", F_OK) = 0
stat("/users/sarvi/.gitconfig", {st_mode=S_IFREG|0644, st_size=367, ...}) = 0
access("/users/sarvi/.gitconfig", F_OK) = 0
access("/users/sarvi/.gitconfig", R_OK) = 0
stat("/users/sarvi/.gitconfig", {st_mode=S_IFREG|0644, st_size=367, ...}) = 0
stat("/users/sarvi/.gitconfig", {st_mode=S_IFREG|0644, st_size=367, ...}) = 0
openat(AT_FDCWD, "/users/sarvi/.gitconfig", O_RDONLY) = 3
read(3, "[user]\n\tname = Saravanan Shanmug"..., 367) = 367
close(3)                                = 0
access("/users/sarvi/.config/git/config", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/gitconfig", F_OK)          = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/Cargo.toml", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=274, ...}) = 0
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/Cargo.toml", O_RDONLY|O_CLOEXEC) = 3
statx(3, "", AT_STATX_SYNC_AS_STAT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=274, ...}) = 0
read(3, "[package]\nname = \"readlink\"\nvers"..., 275) = 274
read(3, "", 1)                          = 0
close(3)                                = 0
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/src/lib.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=2537, ...}) = 0
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/src/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=45, ...}) = 0
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/src/bin", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/examples", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/tests", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/benches", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/build.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec88230) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/README.md", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec88610) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/README.txt", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec88610) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/README", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec88610) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/Cargo.toml", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec8b550) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/Cargo.toml", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=507, ...}) = 0
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/Cargo.toml", O_RDONLY|O_CLOEXEC) = 3
statx(3, "", AT_STATX_SYNC_AS_STAT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=507, ...}) = 0
read(3, "[package]\nname = \"redhook\"\nversi"..., 508) = 507
read(3, "", 1)                          = 0
close(3)                                = 0
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/src/lib.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=930, ...}) = 0
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/src/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec88230) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/src/bin", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, /* 7 entries */, 32768)   = 208
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/fakeroot/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec879e0) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/neverfree/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec879e0) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlinkspy/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec879e0) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec879e0) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/varprintspy/main.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec879e0) = -1 ENOENT (No such file or directory)
getdents64(3, /* 0 entries */, 32768)   = 0
close(3)                                = 0
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/tests", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/benches", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/redhook/build.rs", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec88230) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/sarvi-sjc/Cargo.toml", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec8b550) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/ws/Cargo.toml", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec8b550) = -1 ENOENT (No such file or directory)
statx(AT_FDCWD, "/Cargo.toml", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7fffdec8b550) = -1 ENOENT (No such file or directory)
sched_getaffinity(0, 128, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) = 16
statx(AT_FDCWD, "/users/sarvi/.cargo/bin/rustc", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0755, stx_size=12391632, ...}) = 0
lstat("/users", {st_mode=S_IFDIR|0755, st_size=0, ...}) = 0
lstat("/users/sarvi", {st_mode=S_IFDIR|0755, st_size=36864, ...}) = 0
lstat("/users/sarvi/.cargo", {st_mode=S_IFLNK|0777, st_size=19, ...}) = 0
readlink("/users/sarvi/.cargo", "/ws/sarvi-sjc/cargo", 4095) = 19
lstat("/ws", {st_mode=S_IFDIR|0755, st_size=0, ...}) = 0
lstat("/ws/sarvi-sjc", {st_mode=S_IFDIR|0755, st_size=16384, ...}) = 0
lstat("/ws/sarvi-sjc/cargo", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat("/ws/sarvi-sjc/cargo/bin", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat("/ws/sarvi-sjc/cargo/bin/rustc", {st_mode=S_IFREG|0755, st_size=12391632, ...}) = 0
statx(AT_FDCWD, "/ws/sarvi-sjc/cargo/bin/rustc", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0755, stx_size=12391632, ...}) = 0
statx(AT_FDCWD, "/users/sarvi/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustc", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0755, stx_size=2707888, ...}) = 0
openat(AT_FDCWD, "/ws/sarvi-sjc/redhook/examples/readlink/target/.rustc_info.json", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/dev/null", O_RDONLY|O_CLOEXEC) = 3
pipe2([4, 5], O_CLOEXEC)                = 0
pipe2([6, 7], O_CLOEXEC)                = 0
futex(0x7f005d94d0e8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=4*1024, rlim_max=4*1024}) = 0
mmap(NULL, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f005e6a9000
rt_sigprocmask(SIG_BLOCK, ~[], [], 8)   = 0
clone(child_stack=0x7f005e6b1ff0, flags=CLONE_VM|CLONE_VFORK|SIGCHLD) = 20534
munmap(0x7f005e6a9000, 36864)           = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
close(3)                                = 0
close(5)                                = 0
close(7)                                = 0
ioctl(4, FIONBIO, [1])                  = 0
ioctl(6, FIONBIO, [1])                  = 0
poll([{fd=4, events=POLLIN}, {fd=6, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN}])
read(4, "readlink(\"/proc/self/exe\")\n", 32) = 27
read(4, 0x5644c601543b, 5)              = -1 EAGAIN (Resource temporarily unavailable)
poll([{fd=4, events=POLLIN}, {fd=6, events=POLLIN}], 2, -1

Can anyone explain why this might be hanging? Cosidering the intercept code is pretty much a No-Op, I am not sure why it is hanging. And only on "cargo build"

this for example works fine, Added an extrace "println!()" in the following case to make sure it indeed works.

LD_PRELOAD=/ws/sarvi-sjc/redhook/examples/readlinkspy/target/debug/libreadlinkspy.so ls -al ./test.link 
readlink("./test.link")
lrwxrwxrwx 1 sarvi eng 10 Aug 29 22:30 ./test.link -> Cargo.toml

I'm not sure if this will help your problem, but I saw a couple points that could be improved.

I also don't see any loops in your code, so it may be that you've got infinite recursion (e.g. an overridden function inadvertently calls itself) or calling code expects you to return a particular value and will keep trying in a loop until that happens.

You need to use cdylib here. A dylib isn't really intended for calling from C, it's more of a dynamic library use when linking Rust code together.

You may want to use the ctor crate here. It can reduce the boilerplate and depended on by a large number of crates, so is more likely to be correct and handle edge cases.

I'd fire up gdb and see if you can step through the code. You should also be able to interrupt the process when it's locked up and ask for a backtrace.'

Does LD_PRELOAD get inherited by sub-processes? What happens if you invoke the binary in target/ directly?

1 Like

Done

Done

Trying to to rust-gdb -p pid fails asking me to report to a bug as follows

Reading symbols from /lib64/libpthread.so.0...BFD: BFD (GNU Binutils) 2.24.51.20150113 internal error, aborting at /auto/swtools/prod-builds/src/gdb-7.8.4/gdb/bfd/elf64-x86-64.c line 5364 in elf_x86_64_get_plt_sym_val

BFD: Please report this bug.

With pstree I narrowed it down the following command.
bash-4.4$ LD_PRELOAD=`pwd`/libreadlink.so rustc
The strace hangs on a futex now

mprotect(0x7fc37ca55000, 4096, PROT_READ) = 0
munmap(0x7fc37ca1f000, 209397)          = 0
set_tid_address(0x7fc37ca19910)         = 130917
set_robust_list(0x7fc37ca19920, 24)     = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7fc37771d9a0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7fc377729dc0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7fc37771da30, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7fc377729dc0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
futex(0x7fc3777160e8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x55f18ee57218, FUTEX_WAIT_PRIVATE, 2, NULL

The code post fixes

extern crate core;
extern crate libc;
#[macro_use]
extern crate ctor; 


use libc::{c_void,c_char,c_int,size_t,ssize_t};

use std::sync::atomic;

#[cfg(any(target_os = "macos", target_os = "ios"))]
pub mod dyld_insert_libraries;

/* Some Rust library functionality (e.g., jemalloc) initializes
 * lazily, after the hooking library has inserted itself into the call
 * path. If the initialization uses any hooked functions, this will lead
 * to an infinite loop. Work around this by running some initialization
 * code in a static constructor, and bypassing all hooks until it has
 * completed. */

static INIT_STATE: atomic::AtomicBool = atomic::AtomicBool::new(false);

pub fn initialized() -> bool {
    INIT_STATE.load(atomic::Ordering::SeqCst)
}

#[ctor]
fn initialize() {
    Box::new(0u8);
    INIT_STATE.store(true, atomic::Ordering::SeqCst);
}
// /* Rust doesn't directly expose __attribute__((constructor)), but this
//  * is how GNU implements it. */
//  #[link_section = ".init_array"]
//  pub static INITIALIZE_CTOR: extern "C" fn() = ::initialize;

#[link(name = "dl")]
extern "C" {
    fn dlsym(handle: *const c_void, symbol: *const c_char) -> *const c_void;
}

const RTLD_NEXT: *const c_void = -1isize as *const c_void;

pub unsafe fn dlsym_next(symbol: &'static str) -> *const u8 {
    let ptr = dlsym(RTLD_NEXT, symbol.as_ptr() as *const c_char);
    if ptr.is_null() {
        panic!("redhook: Unable to find underlying function for {}", symbol);
    }
    ptr as *const u8
}


#[allow(non_camel_case_types)]
pub struct readlink {__private_field: ()}
#[allow(non_upper_case_globals)]
static readlink: readlink = readlink {__private_field: ()};

impl readlink {
    fn get(&self) -> unsafe extern fn (path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t  {
        use ::std::sync::Once;

        static mut REAL: *const u8 = 0 as *const u8;
        static mut ONCE: Once = Once::new();

        unsafe {
            ONCE.call_once(|| {
                REAL = dlsym_next(concat!("readlink", "\0"));
            });
            ::std::mem::transmute(REAL)
        }
    }

    #[no_mangle]
    pub unsafe extern "C" fn readlink(path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t {
        if initialized() {
            ::std::panic::catch_unwind(|| my_readlink ( path, buf, bufsiz )).ok()
        } else {
            None
        }.unwrap_or_else(|| readlink.get() ( path, buf, bufsiz ))
    }
}

pub unsafe fn my_readlink(path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t {
    readlink.get()(path, buf, bufsiz)
}

I further added some println! statements to track how far things are going
I know readlink works by itself because

bash-4.4$ touch /tmp/file
bash-4.4$ ln -s /tmp/file /tmp/link
bash-4.4$ LD_PRELOAD=`pwd`/libreadlink.so ls -al /tmp/link 
init begin
do dlsym
return REAL
init end
readlink C
return REAL
readlink complete
lrwxrwxrwx 1 sarvi eng 9 Aug 31 11:11 /tmp/link -> /tmp/file

But some how rustc doees not like being intercepted

bash-4.4$ LD_PRELOAD=`pwd`/libreadlink.so rustc
init begin
do dlsym
return REAL
init end
readlink C
return REAL
readlink complete

Its hung at the end there.
Is there someone from the rustc compiler team that can provide some insight ?
The open source library i am building is to track dependency gnu make/ cargo build, etc.
So it will be working with rustc and other tools to track its input output dependencies. So not something I can ignore.

rustc use jemalloc and jemalloc internally calls readlink during memory initialization path, which the above code intercepts. But before calling the real readlink, it use dlsym* which calls malloc (but this call chain is already in malloc)

Its seems like its a known problem intercepting programs that use jemalloc for certain calls(open, mmap, readlink etc)
There are several reference to this LD_PRELOAD issue (even in non Rust based libraries).

I don't if there a good way to solve it without support from jemalloc.

Here's the full backtrace

(gdb) bt
#0 0x00007f144590e4ed in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x00007f1445909dcb in L_lock_883 () from /lib64/libpthread.so.0
#2 0x00007f1445909c98 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x000056401d0150ed in malloc_mutex_lock_final (mutex=0x56401d2351d8 <init_lock>) at ../jemalloc/include/jemalloc/internal/mutex.h:141
#4 rjem_je_malloc_mutex_lock_slow (mutex=0x56401d2351d8 <init_lock>) at ../jemalloc/src/mutex.c:84
#5 0x000056401cfea09f in malloc_mutex_lock (tsdn=0x0, mutex=) at ../jemalloc/include/jemalloc/internal/mutex.h:205
#6 malloc_init_hard () at ../jemalloc/src/jemalloc.c:1506
#7 malloc_init () at ../jemalloc/src/jemalloc.c:217
#8 imalloc (sopts=, dopts=) at ../jemalloc/src/jemalloc.c:1986
#9 calloc (num=1, size=32) at ../jemalloc/src/jemalloc.c:2138
#10 0x00007f14456fd550 in dlerror_run () from /lib64/libdl.so.2
#11 0x00007f14456fd058 in dlsym () from /lib64/libdl.so.2
#12 0x00007f14497b6cc7 in ldpreload::dlsym_next::h7cab03892daf2fab (symbol=...) at src/lib.rs:43
#13 0x00007f14497b7429 in ldpreload::readlink_local::get::
$u7b$$u7b$closure$u7d$$u7d$::haeb8628401923c4c () at src/lib.rs:71
#14 0x00007f14497b73b0 in std::sync::once::Once::call_once::
$u7b$$u7b$closure$u7d$$u7d$::haf60324ae025b6d9 () at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd/src/libstd/sync/once.rs:264
#15 0x00007f14497de848 in std::sync::once::Once::call_inner::hfbdd978c729db7b8 () at src/libstd/sync/once.rs:416
#16 0x00007f14497b7329 in std::sync::once::Once::call_once::hc3adf9476c282443 (self=0x7f1449a720a0 ldpreload::readlink_local::get::ONCE::h19ff01a158e3f42e, f=...) at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd/src/libstd/sync/once.rs:264
#17 0x00007f14497b6db9 in ldpreload::readlink_local::get::hc45eb880fee9d18f (self=0x7f14498379f4) at src/lib.rs:70
#18 0x00007f14497b7488 in ldpreload::readlink_local::readlink::
$u7b$$u7b$closure$u7d$$u7d$::h8cc84822c0f8be3c () at src/lib.rs:83
#19 0x00007f14497b7b40 in core::option::Option$LT$T$GT$::unwrap_or_else::hb73d717eb9eefcaa (self=..., f=...) at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd/src/libcore/option.rs:428
#20 0x00007f14497b6e93 in readlink (path=0x56401d023e2d "/etc/malloc.conf", buf=0x7ffde2568650 "", bufsiz=4096) at src/lib.rs:79
#21 0x000056401cfeee12 in malloc_conf_init () at ../jemalloc/src/jemalloc.c:913
#22 malloc_init_hard_a0_locked () at ../jemalloc/src/jemalloc.c:1281
#23 0x000056401cfe8f4f in malloc_init_hard () at ../jemalloc/src/jemalloc.c:1517
#24 malloc_init () at ../jemalloc/src/jemalloc.c:217
#25 imalloc (sopts=, dopts=) at ../jemalloc/src/jemalloc.c:1986
#26 malloc (size=size@entry=72704) at ../jemalloc/src/jemalloc.c:2038
#27 0x00007f14420dfae0 in pool (this=0x7f1444c4eca0 <(anonymous namespace)::emergency_pool>) at ../../../../gcc-5.5.0/libstdc++-v3/libsupc++/eh_alloc.cc:117
#28 __static_initialization_and_destruction_0 (__priority=65535, __initialize_p=1) at ../../../../gcc-5.5.0/libstdc++-v3/libsupc++/eh_alloc.cc:244
#29 _GLOBAL__sub_I_eh_alloc.cc(void) () at ../../../../gcc-5.5.0/libstdc++-v3/libsupc++/eh_alloc.cc:307
#30 0x00007f1449a828f3 in _dl_init_internal () from /lib64/ld-linux-x86-64.so.2
#31 0x00007f1449a7415a in _dl_start_user () from /lib64/ld-linux-x86-64.so.2
#32 0x0000000000000001 in ?? ()
#33 0x00007ffde256a2af in ?? ()
#34 0x0000000000000000 in ?? ()

2 Likes

Found the problem with the help of a friend.
Looks like my gdb on RHEL8.1 was broken for this case.
was able to GDB on RHEL7

Looks like rustc uses its own jemalloc, which implements its own readlink to read some malloc.conf file

(gdb) bt
#0  0x00007f843a78a8dd in __lll_lock_wait () from /lib64/libpthread.so.0
#1  0x00007f843a783af9 in pthread_mutex_lock () from /lib64/libpthread.so.0
#2  0x0000560e484dfe2d in malloc_mutex_lock_final (mutex=0x560e487001d8 <init_lock>)
    at ../jemalloc/include/jemalloc/internal/mutex.h:141
#3  _rjem_je_malloc_mutex_lock_slow (mutex=0x560e487001d8 <init_lock>)
    at ../jemalloc/src/mutex.c:84
#4  0x0000560e484b46c5 in malloc_mutex_lock (tsdn=0x0, mutex=<optimized out>)
    at ../jemalloc/include/jemalloc/internal/mutex.h:205
#5  malloc_init_hard () at ../jemalloc/src/jemalloc.c:1506
#6  malloc_init () at ../jemalloc/src/jemalloc.c:217
#7  imalloc (sopts=<optimized out>, dopts=<optimized out>) at ../jemalloc/src/jemalloc.c:1986
#8  calloc (num=1, size=32) at ../jemalloc/src/jemalloc.c:2138
#9  0x00007f8439fe3723 in __cxa_thread_atexit_impl () from /lib64/libc.so.6
#10 0x00007f843f58d26f in std::sys::unix::thread_local_dtor::register_dtor ()
    at library/std/src/sys/unix/thread_local_dtor.rs:36
#11 std::thread::local::fast::Key<T>::try_register_dtor ()
    at library/std/src/thread/local.rs:442
#12 std::thread::local::fast::Key<T>::try_initialize [_ZN3std6thread5local...] ()
    at library/std/src/thread/local.rs:428
#13 0x00007f843f59c6d6 in std::thread::local::fast::Key<T>::get ()
    at library/std/src/thread/local.rs:414
#14 std::io::stdio::LOCAL_STDOUT::__getit () at library/std/src/thread/local.rs:179
#15 std::thread::local::LocalKey<T>::try_with () at library/std/src/thread/local.rs:266
#16 std::io::stdio::print_to () at library/std/src/io/stdio.rs:970
#17 std::io::stdio::_print [_ZN3std2io5stdio6_pr...] () at library/std/src/io/stdio.rs:998
#18 0x00007f843f577307 in readlink::readlink::readlink [readlink] (
    path=0x560e484eed2d "/etc/malloc.conf", buf=0x7fff7423cfa0 "", bufsiz=4096) at src/lib.rs:80
#19 0x0000560e484b9962 in malloc_conf_init () at ../jemalloc/src/jemalloc.c:913
#20 malloc_init_hard_a0_locked () at ../jemalloc/src/jemalloc.c:1281
#21 0x0000560e484b3428 in malloc_init_hard () at ../jemalloc/src/jemalloc.c:1517
#22 malloc_init () at ../jemalloc/src/jemalloc.c:217
#23 imalloc (sopts=<optimized out>, dopts=<optimized out>) at ../jemalloc/src/jemalloc.c:1986
#24 malloc (size=size@entry=72704) at ../jemalloc/src/jemalloc.c:2038
#25 0x00007f8436e9f310 in (anonymous namespace)::pool::pool (
    this=0x7f8439d3baa0 <(anonymous namespace)::emergency_pool>)
    at ../../../../gcc-5.5.0/libstdc++-v3/libsupc++/eh_alloc.cc:117
#26 __static_initialization_and_destruction_0 (__priority=65535, __initialize_p=1)
    at ../../../../gcc-5.5.0/libstdc++-v3/libsupc++/eh_alloc.cc:244
#27 _GLOBAL__sub_I_eh_alloc.cc(void) [_GLOBAL__sub_I_eh_al...] ()
    at ../../../../gcc-5.5.0/libstdc++-v3/libsupc++/eh_alloc.cc:307
#28 0x00007f843f89dd0a in call_init.part () from /lib64/ld-linux-x86-64.so.2
#29 0x00007f843f89de0a in _dl_init () from /lib64/ld-linux-x86-64.so.2
#30 0x00007f843f88f06a in _dl_start_user () from /lib64/ld-linux-x86-64.so.2
#31 0x0000000000000001 in ?? ()
#32 0x00007fff7423ff47 in ?? ()
#33 0x0000000000000000 in ?? ()
(gdb) 

So I guess I need way to override libc readlink but not disrupt jemalloc's readlink

Maybe you could use syscall(SYS_readlink, ...) for forwarding instead?

Hi Michael,
I noticed that cdylib doesnt seem to be working as LD_PRELOAD properly for me.
Both do produce libX.so file.

When I use dylib, my LD_preload intercept funntion works fine and I see println() inside the intercept code.
When I use cdylib, my LD_PRELOAD the intercept code doesnt work.

#ctor does, work and I can see a println!() i put inside my constructor function in both cases though.

cdylib for some reason, seems to not override/intercept the intercepted function.

I would stop playing around with dylib completely. A dylib is an internal compiler thing and shouldn't really be used. Much like a rlib there are no guarantees about anything it contains, so the fact that LD_PRELOAD seems to do what you want is a bit of a fluke/hack.

It looks like you are creating your #[no_mangle] functions as static methods on a readlink type. Usually it should be a standalone function, so try moving readlink::readlink() outside of the impl block and see if that works. If you'd like it to still be accessible to your Rust code as readlink::readlink(), you can create a simple method that wraps a call to the extern "C" function.

I moved to using syscalls like you suggested. Still hangs in jemalloc/mutex, though readlink is not in the backtrace anymore.

Hangs here

bash-4.4$ LD_PRELOAD=target/debug/libreadlink.so rustc
Constructor
readlink
initialized
my_readlink
my_readlink: Complete

backtrace of the hung process in GDB looks like

(gdb) bt
#0  0x00007ff84737e8dd in __lll_lock_wait () from /lib64/libpthread.so.0
#1  0x00007ff847377af9 in pthread_mutex_lock () from /lib64/libpthread.so.0
#2  0x000055fa09bade2d in malloc_mutex_lock_final (mutex=0x55fa09dce1d8 <init_lock>)
    at ../jemalloc/include/jemalloc/internal/mutex.h:141
#3  _rjem_je_malloc_mutex_lock_slow (mutex=0x55fa09dce1d8 <init_lock>)
    at ../jemalloc/src/mutex.c:84
#4  0x000055fa09b826c5 in malloc_mutex_lock (tsdn=0x0, mutex=<optimized out>)
    at ../jemalloc/include/jemalloc/internal/mutex.h:205
#5  malloc_init_hard () at ../jemalloc/src/jemalloc.c:1506
#6  malloc_init () at ../jemalloc/src/jemalloc.c:217
#7  imalloc (sopts=<optimized out>, dopts=<optimized out>)
    at ../jemalloc/src/jemalloc.c:1986
#8  calloc (num=1, size=32) at ../jemalloc/src/jemalloc.c:2138
#9  0x00007ff846bd7723 in __cxa_thread_atexit_impl () from /lib64/libc.so.6
#10 0x00007ff84c186bdf in ?? ()
#11 0x00007ffef2913ad0 in ?? ()
#12 0x0000000000000002 in ?? ()
---Type <return> to continue, or q <return> to quit---
#13 0x0000000000000000 in ?? ()
(gdb) 

The test code looks like below and uses

extern crate core;
extern crate libc;
#[macro_use]
extern crate ctor;
#[macro_use]
extern crate syscalls;


use libc::{c_void,c_char,c_int,size_t,ssize_t};

use std::sync::atomic;

#[cfg(any(target_os = "macos", target_os = "ios"))]
pub mod dyld_insert_libraries;

/* Some Rust library functionality (e.g., jemalloc) initializes
 * lazily, after the hooking library has inserted itself into the call
 * path. If the initialization uses any hooked functions, this will lead
 * to an infinite loop. Work around this by running some initialization
 * code in a static constructor, and bypassing all hooks until it has
 * completed. */

static INIT_STATE: atomic::AtomicBool = atomic::AtomicBool::new(false);

pub fn initialized() -> bool {
    INIT_STATE.load(atomic::Ordering::SeqCst)
}

// extern "C" fn initialize() {
//     Box::new(0u8);
//     INIT_STATE.store(true, atomic::Ordering::SeqCst);
// }

// /* Rust doesn't directly expose __attribute__((constructor)), but this
//  * is how GNU implements it. */
//  #[link_section = ".init_array"]
//  pub static INITIALIZE_CTOR: extern "C" fn() = ::initialize;

#[ctor]
fn initialize() {
    Box::new(0u8);
    INIT_STATE.store(true, atomic::Ordering::SeqCst);
    println!("Constructor");
}


#[link(name = "dl")]
extern "C" {
    fn dlsym(handle: *const c_void, symbol: *const c_char) -> *const c_void;
}

const RTLD_NEXT: *const c_void = -1isize as *const c_void;

pub unsafe fn dlsym_next(symbol: &'static str) -> *const u8 {
    let ptr = dlsym(RTLD_NEXT, symbol.as_ptr() as *const c_char);
    if ptr.is_null() {
        panic!("redhook: Unable to find underlying function for {}", symbol);
    }
    ptr as *const u8
}


#[allow(non_camel_case_types)]
pub struct readlink {__private_field: ()}
#[allow(non_upper_case_globals)]
static readlink: readlink = readlink {__private_field: ()};

impl readlink {
    fn get(&self) -> unsafe extern fn (path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t  {
        use ::std::sync::Once;

        static mut REAL: *const u8 = 0 as *const u8;
        static mut ONCE: Once = Once::new();

        unsafe {
            ONCE.call_once(|| {
                REAL = dlsym_next(concat!("readlink", "\0"));
            });
            ::std::mem::transmute(REAL)
        }
    }

    #[no_mangle]
    pub unsafe extern "C" fn readlink(path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t {
        println!("readlink");
        if initialized() {
            println!("initialized");
            ::std::panic::catch_unwind(|| my_readlink ( path, buf, bufsiz )).ok()
        } else {
            println!("not initialized");
            None
        }.unwrap_or_else(|| readlink.get() ( path, buf, bufsiz ))
    }
}

pub unsafe fn my_readlink(path: *const c_char, buf: *mut c_char, bufsiz: size_t) -> ssize_t {

    println!("my_readlink");
    // readlink.get()(path, buf, bufsiz)
    let sz = syscall!(SYS_readlink, path, buf, bufsiz).unwrap();
    println!("my_readlink: Complete");
    sz as ssize_t
}

I use syscalls = "0.3.2" for syscalls

I'm not sure about the syscalls crate -- I just meant libc::syscall. But there may still be other recursion issues from your handler if it triggers jemalloc any other way.

The direct syscall is still my backup option. Though I havent had a chance to debug that hang yet.
But the LD_PRELOAD library has to co-operate and not distrupt other LD_PRELOADs that might be already active.
Tools like pseudo/chroot for example.
Build tools like bitbake seem to use these LD_PRELOAD programs extensively and wisktrack tool that I am writing needs to be transparent to them.
My current C version is.

So I am trying to see if there is anyway I can do this without resorting to direct syscalls if I can.

Thank @cuviper
I did it get working by using syscalls as you suggested.
just for the duration of the initialization, which I think should handle the case of LD_PRELOADs.

Though, I still need to cleanup the code further.
Thanks for the detailed explanation and responses.
Couldnt have done it without your help.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.