Skip to content

Expose no_link_main and interceptors as configurable features in libafl_libfuzzer_runtime #3733

@mschwager

Description

@mschwager

Note this issue was created with the help of an LLM, and I've extensively reviewed it for correctness and accuracy.

Is your feature request related to a problem? Please describe.

LibAFL's libFuzzer.a cannot be linked into shared objects with GNU ld because FuzzerInterceptors.cpp introduces a .preinit_array section. The ELF specification states that .preinit_array is only processed in executable files and must not appear in shared objects (ELF gABI, Dynamic Section). GNU ld enforces this at link time:

.preinit_array section is not allowed in DSO

This affects projects like Ruzzy (Ruby fuzzing) and Atheris (Python fuzzing) that merge libFuzzer.a with clang's sanitizer archives into shared objects (e.g. asan_with_fuzzer.so) which are LD_PRELOADed at runtime. This is a somewhat hacky solution, but it works.

LLVM/clang's libfuzzer avoids this by shipping the interceptors in a separate archive:

Archive main Interceptors
libclang_rt.fuzzer.a yes no
libclang_rt.fuzzer_no_main.a no no
libclang_rt.fuzzer_interceptors.a no yes

The libafl_targets crate already has the internal feature flags to control both — libfuzzer_no_link_main (guarding main via #ifndef FUZZER_NO_LINK_MAIN in libfuzzer.c) and libfuzzer_interceptors (gating FuzzerInterceptors.cpp compilation in build.rs) — but libafl_libfuzzer_runtime hardcodes them and doesn't expose them to consumers.

Describe the solution you'd like

Add crate-level features to libafl_libfuzzer_runtime/Cargo.toml:

[features]
default = ["interceptors"]
interceptors = ["libafl_targets/libfuzzer_interceptors"]
no_link_main = ["libafl_targets/libfuzzer_no_link_main"]

And remove "libfuzzer_interceptors" from the hardcoded libafl_targets dependency features list.

This is a backwards-compatible change — default behavior is preserved. Consumers can then configure the build via build.sh:

# Equivalent to libclang_rt.fuzzer_no_main.a (no main, no interceptors)
bash build.sh --cargo-args "--no-default-features --features no_link_main"

# No interceptors, with main (current weak symbol)
bash build.sh --cargo-args "--no-default-features"

# Default (current behavior, interceptors + main)
bash build.sh

Describe alternatives you've considered

  • Using lld instead of GNU ld. lld permits .preinit_array in shared objects, but it adds a build dependency and the section is silently ignored at runtime anyway (the dynamic linker only processes .preinit_array for the main executable).
  • Patching the cloned source with sed -i '/"libfuzzer_interceptors",/d' Cargo.toml before running build.sh. This works but is fragile and requires modifying LibAFL source.
  • Stripping the .preinit_array section from libFuzzer.a with objcopy --remove-section. This fails because .debug_info has relocations against the section.

Additional context

The main symbol in LibAFL's libFuzzer.a is already weak (W main), so it doesn't cause linker conflicts in practice. However, exposing no_link_main would provide parity with clang's libclang_rt.fuzzer_no_main.a and give consumers explicit control, matching the separation that LLVM's build system already provides.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions