Skip to content

fix: don't link msvcrt when the target uses the static CRT - #3926

Open
kkuehl wants to merge 3 commits into
AFLplusplus:mainfrom
kkuehl:kkuehl/crt-static-msvcrt
Open

kkuehl wants to merge 3 commits into
AFLplusplus:mainfrom
kkuehl:kkuehl/crt-static-msvcrt

Conversation

@kkuehl

@kkuehl kkuehl commented Sep 30, 2026 •

Copy link
Copy Markdown

Problem

crates/libafl/build.rs links msvcrt unconditionally on Windows (added in #3431 to help z3 link):

if cfg!(target_os = "windows") {
    println!("cargo:rustc-link-lib=msvcrt");
}

msvcrt is the dynamically loaded C runtime. Consumers that build with -C target-feature=+crt-static already get the static CRT (libcmt) from rustc, so the extra import library makes the MSVC linker mix both CRTs:

LINK : warning LNK4098: defaultlib 'libcmt' conflicts with use of other libs; use /NODEFAULTLIB:library

This is not just noise: mixed CRT modes mean duplicate CRT state (heaps, stdio, locale) inside one process — exactly what +crt-static users are trying to avoid. It is particularly visible for injected/agent DLLs built on top of LibAFL, where the static CRT is often chosen deliberately so the artifact has no MSVC runtime dependency.

Note that this repository already ships fuzzers that build with crt-static (fuzzers/binary_only/frida_libpng and fuzzers/binary_only/frida_windows_gdiplus), so they are affected too.

Fix

Only link msvcrt when the target does not have the crt-static feature, and declare cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FEATURE so the build script reruns whenever the CRT mode changes.

let crt_static = std::env::var("CARGO_CFG_TARGET_FEATURE")
    .is_ok_and(|features| features.contains("crt-static"));
if cfg!(target_os = "windows") && !crt_static {
    println!("cargo:rustc-link-lib=msvcrt");
}

Verification (x86_64-pc-windows-msvc, stable 1.98)

Inspecting the build script output at target/debug/build/libafl-*/output on this branch:

  • cargo build -p libafl → still emits cargo:rustc-link-lib=msvcrt
  • RUSTFLAGS="-C target-feature=+crt-static" cargo build -p libafl → emits no link directive

The static-CRT path no longer asks the linker for msvcrt; rustc's own libcmt is the only CRT in the link.


Also included: two CI fixes for Rust 1.98

The 1.98 toolchain surfaced two pre-existing failures (the last push CI run on main predates 1.98, so they are not visible there). Both jobs run on this PR, so the fixes are included here to keep CI green — happy to split them into separate PRs if you prefer:

  • 🔧 libafl_qemu_asan — the powerpc memcpy/memset shims in libafl_asan_libc are rejected by the new deny-by-default invalid_runtime_symbol_definitions lint. Their signatures now match exactly what the lint expects (*mut c_void / *const c_void, i32 for memset's value, returning *mut c_void).
  • 🚀 forkserver/libafl-fuzz — clippy 1.98 flags the late-initialized scheduler (needless_late_init) under the fuzzer's #![deny(clippy::all)]; the scheduler is now built in a single if/else expression.

`crates/libafl/build.rs` unconditionally emits `rustc-link-lib=msvcrt` on
Windows (added in AFLplusplus#3431 to help z3 link). When a consumer builds with
`-C target-feature=+crt-static`, Rust already links the static CRT
(`libcmt`), so the extra `msvcrt` import library makes the MSVC linker mix
the two CRTs: LNK4098 ("defaultlib 'libcmt' conflicts with use of other
libs") and potentially two separate CRT heaps in one process.

Only link `msvcrt` when the target does not have the `crt-static` feature,
and track `CARGO_CFG_TARGET_FEATURE` so the build script reruns when the
CRT mode changes.

Verified with `cargo build -p libafl` on x86_64-pc-windows-msvc:
- default: build script still emits `cargo:rustc-link-lib=msvcrt`
- RUSTFLAGS="-C target-feature=+crt-static": no link directive is emitted
Rust 1.98 enables `invalid_runtime_symbol_definitions` by default, which
rejects the powerpc `memcpy`/`memset` shims in `libafl_asan_libc`:

    error: invalid definition of the runtime `memcpy` symbol used by the standard library
    = note: expected `unsafe extern "C" fn(*mut c_void, *const c_void, usize) -> *mut c_void`

This breaks the `libafl_qemu_asan` ppc guest build. Use the exact
signatures the lint expects (`*mut c_void` / `*const c_void`, and `i32`
for `memset`'s value, both returning `*mut c_void`).
The libafl-fuzz fuzzer denies `clippy::all`, and clippy 1.98 flags the
late-initialized `let scheduler;` (`needless_late_init`), failing the
`forkserver/libafl-fuzz` CI job. Build the scheduler in a single
`if`/`else` expression instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant