elf_loader 0.16.0

A no_std-friendly ELF loader and runtime linker for Rust.
Documentation
mod support;

#[cfg(feature = "lazy-binding")]
use elf_loader::{
    Loader,
    arch::NativeArch,
    image::LoadedCore,
    input::ElfBinary,
    lazy::NativeLazyBinder,
    relocation::{RelocationArch, Relocator},
};

#[cfg(feature = "lazy-binding")]
const REL_GOT: u32 = <NativeArch as RelocationArch>::GOT.raw();
#[cfg(feature = "lazy-binding")]
const REL_JUMP_SLOT: u32 = <NativeArch as RelocationArch>::JUMP_SLOT.raw();
#[cfg(feature = "lazy-binding")]
use gen_elf::ElfWriterConfig;
#[cfg(feature = "lazy-binding")]
use gen_elf::{Arch, ElfWriteOutput, RelocEntry, SymbolDesc};
#[cfg(feature = "lazy-binding")]
use support::binding::{BindingFixture, BindingKind};
#[cfg(feature = "lazy-binding")]
use support::{
    dylib_relocation_checks::{relocation_for_symbol, slot_word},
    generated_dylib::return_42_stub,
    test_dylib::{load_relocated_dylib, write_test_dylib, write_test_dylib_with_config},
};

#[cfg(feature = "lazy-binding")]
const SCOPED_FUNC_NAME: &str = "scoped_func";
#[cfg(feature = "lazy-binding")]
const SCOPED_VAR_NAME: &str = "scoped_var";

#[cfg(feature = "lazy-binding")]
type ScopedHelperFn = extern "C" fn() -> i32;

#[cfg(feature = "lazy-binding")]
fn write_scope_provider() -> ElfWriteOutput {
    let arch = Arch::current();
    write_test_dylib(
        &[],
        &[
            SymbolDesc::global_func(SCOPED_FUNC_NAME, &return_42_stub(arch)),
            SymbolDesc::global_object(SCOPED_VAR_NAME, &[0x12, 0x34, 0x56, 0x78]),
        ],
    )
}

#[cfg(feature = "lazy-binding")]
fn write_scope_consumer(config: ElfWriterConfig) -> ElfWriteOutput {
    let arch = Arch::current();
    write_test_dylib_with_config(
        config,
        &[
            RelocEntry::glob_dat(SCOPED_VAR_NAME, arch),
            RelocEntry::jump_slot(SCOPED_FUNC_NAME, arch),
        ],
        &[
            SymbolDesc::undefined_object(SCOPED_VAR_NAME),
            SymbolDesc::undefined_func(SCOPED_FUNC_NAME),
        ],
    )
}

#[cfg(feature = "lazy-binding")]
fn write_scope_func_consumer(config: ElfWriterConfig) -> ElfWriteOutput {
    let arch = Arch::current();
    write_test_dylib_with_config(
        config,
        &[RelocEntry::jump_slot(SCOPED_FUNC_NAME, arch)],
        &[SymbolDesc::undefined_func(SCOPED_FUNC_NAME)],
    )
}

#[cfg(feature = "lazy-binding")]
fn write_defining_scope_func_consumer(config: ElfWriterConfig) -> ElfWriteOutput {
    let arch = Arch::current();
    write_test_dylib_with_config(
        config,
        &[RelocEntry::jump_slot(SCOPED_FUNC_NAME, arch)],
        &[SymbolDesc::global_func(
            SCOPED_FUNC_NAME,
            &return_42_stub(arch),
        )],
    )
}

#[cfg(feature = "lazy-binding")]
fn scope_symbol_address(image: &LoadedCore<()>, symbol_name: &str) -> u64 {
    unsafe {
        image
            .get::<*const ()>(symbol_name)
            .unwrap_or_else(|| panic!("missing scoped symbol {symbol_name}"))
            .into_raw() as u64
    }
}

#[cfg(feature = "lazy-binding")]
fn jump_slot_word(image: &LoadedCore<()>, output: &ElfWriteOutput) -> u64 {
    slot_word(
        image,
        relocation_for_symbol(output, REL_JUMP_SLOT, SCOPED_FUNC_NAME),
    )
}

#[cfg(feature = "lazy-binding")]
fn got_slot_word(image: &LoadedCore<()>, output: &ElfWriteOutput) -> u64 {
    slot_word(
        image,
        relocation_for_symbol(output, REL_GOT, SCOPED_VAR_NAME),
    )
}

#[cfg(feature = "lazy-binding")]
fn call_scope_helper(image: &LoadedCore<()>) -> i32 {
    let helper_name = format!("{SCOPED_FUNC_NAME}@plt_helper");
    let helper_fn: ScopedHelperFn = unsafe {
        core::mem::transmute(
            image
                .get::<*const ()>(&helper_name)
                .unwrap_or_else(|| panic!("missing helper symbol {helper_name}"))
                .into_raw(),
        )
    };
    helper_fn()
}

#[cfg(feature = "lazy-binding")]
#[test]
fn lazy_jump_slots_resolve() {
    let scenario = BindingFixture::new().load(BindingKind::Lazy);

    scenario.assert_relocation_scope_entries();
    scenario.assert_non_plt_relocations();
    scenario.assert_relative_relocations();
    scenario.assert_lazy_jump_slots();
    #[cfg(feature = "tls")]
    scenario.assert_tls_relocations();
}

#[cfg(feature = "lazy-binding")]
#[test]
fn default_lazy_binding_uses_retained_scope_entry() {
    let mut loader = Loader::new();
    let provider_output = write_scope_provider();
    let provider = load_relocated_dylib(&mut loader, "libscope_provider.so", &provider_output);
    let consumer_output = write_scope_consumer(ElfWriterConfig::default());

    let relocated = Relocator::new()
        .lazy_binder(NativeLazyBinder::new())
        .run(
            loader
                .load_dylib(ElfBinary::new("scope_consumer.so", &consumer_output.data))
                .expect("failed to load scope consumer"),
        )
        .scope(std::slice::from_ref(&provider))
        .relocate()
        .expect("failed to relocate scope consumer");

    let scoped_func_addr = scope_symbol_address(&provider, SCOPED_FUNC_NAME);
    let scoped_var_addr = scope_symbol_address(&provider, SCOPED_VAR_NAME);
    assert_eq!(got_slot_word(&relocated, &consumer_output), scoped_var_addr);
    assert_ne!(
        jump_slot_word(&relocated, &consumer_output),
        scoped_func_addr,
        "default lazy binding should leave the jump slot unresolved before the first call"
    );
    assert_eq!(call_scope_helper(&relocated), 42);
    assert_eq!(
        jump_slot_word(&relocated, &consumer_output),
        scoped_func_addr
    );
    let scope = relocated.scope();
    assert_eq!(scope.len(), 1, "expected one retained scope entry");
    assert_eq!(scope[0].name(), provider.name());
}

#[cfg(feature = "lazy-binding")]
#[test]
fn default_lazy_binding_retains_scope_used_only_by_lazy_jump_slot() {
    let mut loader = Loader::new();
    let provider_output = write_scope_provider();
    let provider = load_relocated_dylib(&mut loader, "libscope_provider.so", &provider_output);
    let consumer_output = write_scope_func_consumer(ElfWriterConfig::default());

    let relocated = Relocator::new()
        .lazy_binder(NativeLazyBinder::new())
        .run(
            loader
                .load_dylib(ElfBinary::new(
                    "scope_func_consumer.so",
                    &consumer_output.data,
                ))
                .expect("failed to load scope consumer"),
        )
        .scope(std::slice::from_ref(&provider))
        .relocate()
        .expect("failed to relocate scope consumer");

    let scoped_func_addr = scope_symbol_address(&provider, SCOPED_FUNC_NAME);
    assert_ne!(
        jump_slot_word(&relocated, &consumer_output),
        scoped_func_addr,
        "default lazy binding should leave the jump slot unresolved before the first call"
    );
    let scope = relocated.scope();
    assert_eq!(
        scope.len(),
        1,
        "lazy jump slot scope entry should be retained before first call"
    );
    assert_eq!(scope[0].name(), provider.name());
    assert_eq!(call_scope_helper(&relocated), 42);
    assert_eq!(
        jump_slot_word(&relocated, &consumer_output),
        scoped_func_addr
    );
}

#[cfg(feature = "lazy-binding")]
#[test]
fn df_symbolic_lazy_jump_slot_prefers_current_definition() {
    let mut loader = Loader::new();
    let provider_output = write_scope_provider();
    let provider = load_relocated_dylib(&mut loader, "libscope_provider.so", &provider_output);
    let consumer_output =
        write_defining_scope_func_consumer(ElfWriterConfig::default().with_symbolic(true));

    let relocated = Relocator::new()
        .lazy_binder(NativeLazyBinder::new())
        .run(
            loader
                .load_dylib(ElfBinary::new("scope_consumer.so", &consumer_output.data))
                .expect("failed to load scope consumer"),
        )
        .scope(std::slice::from_ref(&provider))
        .relocate()
        .expect("failed to relocate scope consumer");

    let self_func_addr = scope_symbol_address(&relocated, SCOPED_FUNC_NAME);
    assert_ne!(
        jump_slot_word(&relocated, &consumer_output),
        self_func_addr,
        "default lazy binding should leave the jump slot unresolved before the first call"
    );
    assert_eq!(call_scope_helper(&relocated), 42);
    assert_eq!(jump_slot_word(&relocated, &consumer_output), self_func_addr);
}

#[cfg(feature = "lazy-binding")]
#[test]
fn bind_now_defaults_to_eager_resolution() {
    let mut loader = Loader::new();
    let provider_output = write_scope_provider();
    let provider = load_relocated_dylib(&mut loader, "libscope_provider.so", &provider_output);
    let consumer_output = write_scope_consumer(ElfWriterConfig::default().with_bind_now(true));

    let relocated = Relocator::new()
        .run(
            loader
                .load_dylib(ElfBinary::new("scope_consumer.so", &consumer_output.data))
                .expect("failed to load scope consumer"),
        )
        .scope(std::slice::from_ref(&provider))
        .relocate()
        .expect("failed to relocate scope consumer");

    let scoped_func_addr = scope_symbol_address(&provider, SCOPED_FUNC_NAME);
    let scoped_var_addr = scope_symbol_address(&provider, SCOPED_VAR_NAME);
    assert_eq!(got_slot_word(&relocated, &consumer_output), scoped_var_addr);
    assert_eq!(
        jump_slot_word(&relocated, &consumer_output),
        scoped_func_addr,
        "bind-now should resolve the jump slot during relocate()"
    );
    assert_eq!(call_scope_helper(&relocated), 42);
    let scope = relocated.scope();
    assert_eq!(scope.len(), 1, "expected one retained scope entry");
    assert_eq!(scope[0].name(), provider.name());
}