rust-fuzz / cargo-fuzz

Command line helpers for fuzzing
https://rust-fuzz.github.io/book/cargo-fuzz.html
Apache License 2.0
1.53k stars 110 forks source link

Fuzzing Windows DLL (cdylib) - Unresolved External Symbol Main #386

Open cwshugg opened 4 days ago

cwshugg commented 4 days ago

Hi cargo-fuzz devs! I am working on fuzzing a Windows DLL with cargo-fuzz and have been hitting a wall with a particular linker error. I've reproduced the problem in a small & simple Cargo project to demonstrate the issue.

Problem Description

The issue arises when the to-be-fuzzed Cargo project defines its crate-type as ["cdylib"], specifically on Windows (it will compile into a DLL). The Cargo.toml looks something like this:

[package]
name = "simplelib"
version = "0.1.0"
edition = "2021"
# ...
[lib]
crate-type = ["cdylib"]
# ...
[dependencies.windows]
version = "0.58.0"
features = [ "Win32" ]
# ...

A very basic DLL DllMain function in lib.rs looks like this:

use windows::Win32::Foundation::*;

#[export_name = "DllMain"]
extern "system" fn dll_main(_: HINSTANCE, _: u32, _: *mut ()) -> bool
{
    true
}

After running cargo fuzz init to initialize a basic fuzzing sub-project, the command cargo fuzz build (or cargo fuzz run) will fail during building with the following linker error:

  = note:    Creating library C:\Users\USERNAME\dev\simplelib\fuzz\target\x86_64-pc-windows-msvc\release\deps\simplelib.dll.lib and object C:\Users\USERNAME\dev\simplelib\fuzz\target\x86_64-pc-windows-msvc\release\deps\simplelib.dll.exp
          LINK : error LNK2001: unresolved external symbol main
          C:\Users\USERNAME\dev\simplelib\fuzz\target\x86_64-pc-windows-msvc\release\deps\simplelib.dll : fatal error LNK1120: 1 unresolved externals

Steps to Reproduce

  1. On a Windows system (with Rust/Cargo installed), open PowerShell
  2. cargo init --lib ./simplelib
  3. (edit Cargo.toml to have the above information)
  4. cargo fuzz init
  5. cargo fuzz build

Problem Analysis

I spent time digging through the various options passed to the Rust compiler by cargo-fuzz, and found this commit, which adds the /include:main linker argument for builds using the msvc triple. Based on the comment left with that line of code, and reading up on the /include option, I understand the intention is to force libraries compiled on Windows to include the main symbol, so it can be found within LibFuzzer.

I also found #281, and based on the discussion there it seems like crate-type = ["cdylib"] may not be supported by cargo-fuzz. If I'm thinking about this problem correctly, I understand why cdylib crates cause issues: DLLs/shared libraries, by nature, are loaded at runtime and don't have (don't need) prior knowledge of a main function. But, as it sits now, it appears that Cargo projects that can only build as cdylibs aren't able to be built by cargo-fuzz. (Please correct me if I am wrong)

Possible Solution

Would it be possible to modify the cargo-fuzz building routine to build & instrument DLLs without the main function? This would allow the DLL to be built and instrumented in the same way as the fuzzing targets, but it would then be up to the user as to where the DLL should be dropped, so it can be loaded during fuzzing. (Since the DLL would be built with Sanitizer Coverage instrumentation, LibFuzzer should still be able to detect code coverage changes when the fuzzing target loads it in at runtime and invokes the DLL's functions.)

Perhaps something like this:

if build.triple.contains("-msvc") && !build.crate_type.ne("cdylib") {
    // The entrypoint is in the bundled libfuzzer rlib, this gets the linker to find it.
    // (Do not do this if we're building a DLL; instead let it build & be instrumented without `main`)
    rustflags.push_str(" -Clink-arg=/include:main");
}

I've been testing this theory locally (modifying my local copy of cargo-fuzz's code by commenting out the rustflags.push_str(" -Clink-arg=/include:main") when building the DLL), and it's building successfully with instrumentation. After it's built, I drop it into a location where my fuzzing target can pick it up for loading, and when I run cargo fuzz run my_fuzz_target, LibFuzzer appears to be quickly discovering a set of cascading if-statements and a bug I inserted inside the DLL.

cwshugg commented 1 day ago

After doing a bit more studying and experimenting with the source code, I realized that the build arguments established during exec_build() aren't individually executed for each dependency of the fuzz targets; rather, it's a single cargo build ... command that passes all of these arguments to every dependency.

Because of this, the code snippet I shared above under "Possible Solution" wouldn't work. I think this lines up with Nick's reasoning in #340.

However, there's a fix for this particular MSVC /include:main issue for DLLs that can be implemented on the user's side. Adding /force:unresolved as a linker argument will force linker to produce a DLL/executable even with an unresolved symbol (see here for more info). So, adding this to build.rs for the DLL will allow cargo fuzz build and cargo fuzz run to succeed:

println!("cargo:rustc-link-arg=/force:unresolved");