Hi all,
I am trying to set up a dylib project that is intended to work as a LD_PRELOAD shared object. While the unit tests of what I'm implementing generally work fine, I'm having issues when running a simple integration test (the ones in ./tests
directory) of this form:
fn preload_lib_path() -> PathBuf {
let path = [
env!("CARGO_MANIFEST_DIR"),
format!("target/debug/lib{}.so", env!("CARGO_PKG_NAME")).as_str(),
]
.into_iter()
.collect::<PathBuf>();
// File must exist for the integration tests that rely on it to succeed! Just in case...
assert!(path.is_file(), "Preload .so does not exist at {path:?}. Please compile the project first and then run these tests!");
path
}
#[test]
fn preload_works_via_logs() {
// We create a random command with `assert_cmd` and pass LD_PRELOAD as an env var to it
let command = Command::new("ls")
.arg("-l")
.env("LD_PRELOAD", preload_lib_path())
.output()
.expect("failed to execute process");
// Running a innocuous command expected to work without issues
command
.assert()
.success();
// ... there are some example logs that I print, so I test that they are emitted here
}
I'm trying to set up a minimal reproducer but I haven't managed yet (I'll share as soon as I do), but the basic structure of the project has an entrypoint like this one:
#[cfg_attr(target_os = "linux", link_section = ".init_array")]
pub static LD_PRELOAD_INITIALIZE: extern "C" fn() = self::ld_preload_initialize;
extern "C" fn ld_preload_initialize() { /* logic here */ }
And performs some operations like:
- Setting up a logger with
env_logger
. - Read files and parse them with
serde
. - Manipulate a non-trivial collection (
HashMap
) inside aMutex
.
Although by the logs that are emitted indicate that the ld_preload_initialize
reaches the end, the actual command executed in the test (ls
) doesn't get to run, and instead I get this error:
fatal runtime error: thread::set_current should only be called once per thread
error: test failed, to rerun pass `--lib`
Caused by:
process didn't exit successfully: `/<PROJECT_PATH>/target/debug/deps/<PROJECT_NAME>-5f5c39d631f618e1` (signal: 6, SIGABRT: process abort signal)
I don't spawn any threads explicitly in all the logic (but I assume env_logger
might), nor call set_current
.
I have seen that if I compile the project with the static
conditionally compiled to be absent in test builds the integration tests actually succeed:
#[cfg(not(test))]
#[cfg_attr(target_os = "linux", link_section = ".init_array")]
pub static LD_PRELOAD_INITIALIZE: extern "C" fn() = self::ld_preload_initialize;
extern "C" fn ld_preload_initialize() { ... }
So I suspect some kind of this PRELOAD .init_array
section is being initialised more than once when running the "test binary"... but I don't know if testing like this is even advisable or if it should be working at all anyways. The code I'm writing tries its best to not have any panic
s (handling all Result::Err
, Option::None
and so on) and doesn't include any explicit unsafe
code.
As I said above, I'm trying to set up a minimal reproducer but I haven't been able to do so. As soon as I have it I'll either add more data here or even post the solution if I find it.
It's my first time exploring this, so there's quite a bit that I don't know yet. I wonder if there's anything in what I report that points to a possible root cause? Any recommendations when developing programs like this that leverage .init_array
and are intended to run as LD_PRELOAD
shared objects? Let me know if there's more information I can provide to help figuring out the issue, I'll try to share the details I can.
Thanks a lot!