diff --git a/Cargo.lock b/Cargo.lock index 211b11a..011a9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,12 +71,14 @@ dependencies = [ "anyhow", "assert_cmd", "cargo_metadata", + "cc", "clap", "current_platform", "predicates", "rustc_version", "tempfile", "toml", + "windows-sys 0.52.0", ] [[package]] @@ -104,9 +106,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.77" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" [[package]] name = "cfg-if" @@ -245,7 +247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -257,7 +259,7 @@ dependencies = [ "hermit-abi", "io-lifetimes", "rustix", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -453,7 +455,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -646,13 +648,38 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.0", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm 0.42.0", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -661,38 +688,86 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index ada61cb..a2cf584 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,10 @@ toml = "0.5.9" rustc_version = "0.4.0" cargo_metadata = "0.18.1" +[target.'cfg(target_env = "msvc")'.dependencies] +cc = "1.1" +windows-sys = { version = "0.52", features = ["Win32_Foundation", "Win32_Security","Win32_System_Threading", "Win32_System_JobObjects"] } + [dev-dependencies] assert_cmd = "2.0.7" predicates = "2.1.4" diff --git a/src/project.rs b/src/project.rs index 3bcc37e..3c9a61e 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,7 +1,8 @@ use crate::options::{self, BuildMode, BuildOptions, Sanitizer}; -use crate::utils::default_target; +use crate::utils::{create_job_object, default_target}; use anyhow::{anyhow, bail, Context, Result}; use cargo_metadata::MetadataCommand; +use cc::windows_registry::VsVers; use std::collections::HashSet; use std::io::Read; use std::io::Write; @@ -187,17 +188,24 @@ impl FuzzProject { rustflags.push_str(" -Cinstrument-coverage"); } - match build.sanitizer { - Sanitizer::None => {} - Sanitizer::Memory => { - // Memory sanitizer requires more flags to function than others: - // https://doc.rust-lang.org/unstable-book/compiler-flags/sanitizer.html#memorysanitizer - rustflags.push_str(" -Zsanitizer=memory -Zsanitizer-memory-track-origins") + if cfg!(windows) { + match build.sanitizer { + Sanitizer::Address | Sanitizer::None => rustflags.push_str(" -Zsanitizer=address"), + sanitizer => bail!("Windows does not support sanitizer '{sanitizer}'"), + } + } else { + match build.sanitizer { + Sanitizer::None => {} + Sanitizer::Memory => { + // Memory sanitizer requires more flags to function than others: + // https://doc.rust-lang.org/unstable-book/compiler-flags/sanitizer.html#memorysanitizer + rustflags.push_str(" -Zsanitizer=memory -Zsanitizer-memory-track-origins") + } + _ => rustflags.push_str(&format!( + " -Zsanitizer={sanitizer}", + sanitizer = build.sanitizer + )), } - _ => rustflags.push_str(&format!( - " -Zsanitizer={sanitizer}", - sanitizer = build.sanitizer - )), } if build.careful_mode { @@ -272,6 +280,28 @@ impl FuzzProject { artifact_arg.push(self.artifacts_for(fuzz_target)?); cmd.arg("--").arg(artifact_arg); + #[cfg(target_env = "msvc")] + { + use crate::utils::{append_to_pathvar, get_asan_path}; + // On Windows asan is in a DLL. This DLL is not on PATH by default, so the recommended + // action is to add the directory to PATH when running + + match (get_asan_path(), cc::windows_registry::find_vs_version()) { + (_, Ok(VsVers::Vs14 | VsVers::Vs15)) => { + bail!("AddressSanitizer is not supported on this MSVC version, 2019 or later is required.") + } + (None, _) => { + bail!("could not find AddressSanitizer DLL") + } + (Some(asan), _) => { + let new_path = append_to_pathvar(&asan).unwrap_or(asan.into_os_string()); + cmd.env("PATH", new_path); + } + } + + create_job_object()?; + } + Ok(cmd) } @@ -335,7 +365,7 @@ impl FuzzProject { ) -> Result> { let mut artifacts = HashSet::new(); - let artifacts_dir = self.artifacts_for(target)?; + let artifacts_dir = dbg!(self.artifacts_for(target)?); for entry in fs::read_dir(&artifacts_dir).with_context(|| { format!( diff --git a/src/utils.rs b/src/utils.rs index acf0dba..a477e1a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,124 @@ +use std::ffi::OsString; + /// The default target to pass to cargo, to workaround issue #11. pub fn default_target() -> &'static str { current_platform::CURRENT_PLATFORM } + +/// Gets the path to the asan DLL required for the asan instrumented binary to run. +#[cfg(target_env = "msvc")] +pub fn get_asan_path() -> Option { + // The asan DLL sits next to cl & link.exe. So grab the parent path. + Some( + cc::windows_registry::find_tool(default_target(), "link.exe")? + .path() + .parent()? + .to_owned(), + ) +} + +/// Append a value to the PATH variable +#[cfg(target_env = "msvc")] +pub fn append_to_pathvar(path: &std::path::Path) -> Option { + use std::env; + + if let Some(current) = env::var_os("PATH") { + let mut current = env::split_paths(¤t).collect::>(); + current.push(path.to_path_buf()); + return env::join_paths(current).ok(); + } + + return None; +} + +/// Add current process to a Windows Job Object +/// This means that when the current process is terminated, all children are as well. +#[cfg(target_env = "msvc")] +pub fn create_job_object() -> anyhow::Result<()> { + use anyhow::bail; + use std::{mem::MaybeUninit, ptr}; + use windows_sys::Win32::System::{ + JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation, + QueryInformationJobObject, SetInformationJobObject, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + }, + Threading::GetCurrentProcess, + }; + + // Safety: Both parameters are optional. The first parameter being null + // ensures that the child processes cannot inherit the job handle. + // The second argument means that it is anonymous. + let maybe_handle = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) }; + + // If the handle is null, the above function failed. If it is non-null, it succeeded. + if maybe_handle == 0 { + bail!("invalid job handle returned"); + } + + let mut info = MaybeUninit::::uninit(); + + // Safety: + // We pass in a MaybeUninit extended info object, and then also give the size of it as the size parameter. + // The return length is optional, so we set it to null. + let err = unsafe { + QueryInformationJobObject( + maybe_handle, + JobObjectExtendedLimitInformation, + std::ptr::from_mut(&mut info) as _, + std::mem::size_of_val(&info) as _, + ptr::null_mut(), + ) + }; + + // This function returns zero on failure. + if err == 0 { + bail!("JobObject information query failed"); + } + + // Safety: + // The query infomation called suceeded, so it is now init. + let mut info = unsafe { info.assume_init() }; + + // Flag for killing children upon closure. + info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + // Safety: + // Handle is valid & has set attributes rights + // Info object is not exclusively held anywhere and it a valid object for this API. + let err = unsafe { + SetInformationJobObject( + maybe_handle, + JobObjectExtendedLimitInformation, + std::ptr::from_ref(&info) as _, + std::mem::size_of_val(&info) as _, + ) + }; + + // Failure is zero + if err == 0 { + bail!("setting job object information failed"); + } + + // Safety: + // I do not see any preconditions on this function. + // Receive the current processes's handle. + let this_process = unsafe { GetCurrentProcess() }; + + // Safety: + // The job is valid and has the ASSIGN_PROCESS access right because we created it. + // the handle has the terminate & set quote access right because it is our handle. + let err = unsafe { AssignProcessToJobObject(maybe_handle, this_process) }; + + if err == 0 { + bail!("failed to assign current process to job object") + } + + // Note: + // We could return the handle, but there is no reason to. All child processes will automatically + // be added to the job. As soon as the handle we created is closed, all children will be terminated. + // + // Since we added ourselves to the Job, is we close the handle manually we will terminate ourselves. + // If we allowed our process to exit by exiting main(), Windows will close the handle for us, terminating all children. + Ok(()) +} diff --git a/tests/tests/main.rs b/tests/tests/main.rs index 668461d..6fe609a 100644 --- a/tests/tests/main.rs +++ b/tests/tests/main.rs @@ -103,12 +103,14 @@ fn init_twice() { .cargo_fuzz() .arg("init") .assert() - .stderr(predicates::str::contains("File exists (os error 17)").and( - predicates::str::contains(format!( - "failed to create directory {}", - project.fuzz_dir().display() - )), - )) + .stderr( + predicates::str::contains("File exists (os error 17)") + .or(predicates::str::contains("os error 183")) + .and(predicates::str::contains(format!( + "failed to create directory {}", + project.fuzz_dir().display() + ))), + ) .failure(); } @@ -203,8 +205,10 @@ fn add_twice() { .arg("new_fuzz_target") .assert() .stderr( - predicate::str::contains("could not add target") - .and(predicate::str::contains("File exists (os error 17)")), + predicate::str::contains("could not add target").and( + predicate::str::contains("File exists (os error 17)") + .or(predicate::str::contains("os error 80")), + ), ) .failure(); } @@ -510,7 +514,8 @@ fn run_one_input() { .assert() .stderr( predicate::str::contains("Running 1 inputs 1 time(s) each.").and( - predicate::str::contains("Running: fuzz/corpus/run_one/pass"), + predicate::str::contains("Running: fuzz/corpus/run_one/pass") + .or(predicates::str::contains(r"fuzz\corpus\run_one\pass")), ), ) .success(); @@ -551,7 +556,8 @@ fn run_a_few_inputs() { .assert() .stderr( predicate::str::contains("Running 4 inputs 1 time(s) each.").and( - predicate::str::contains("Running: fuzz/corpus/run_few/pass"), + predicate::str::contains("Running: fuzz/corpus/run_few/pass") + .or(predicates::str::contains(r"fuzz\corpus\run_few\pass")), ), ) .success(); @@ -591,7 +597,12 @@ fn run_alt_corpus() { .assert() .stderr( predicate::str::contains("3 files found in fuzz/alt-corpus/run_alt") - .and(predicate::str::contains("fuzz/corpus/run_alt").not()) + .or(predicates::str::contains(r"fuzz\alt-corpus\run_alt")) + .and( + predicate::str::contains("fuzz/corpus/run_alt") + .or(predicates::str::contains(r"fuzz\corpus\run_alt")) + .not(), + ) // libFuzzer will always test the empty input, so the number of // runs performed is always one more than the number of files in // the corpus. @@ -766,8 +777,13 @@ fn build_all() { let build_dir = project.fuzz_build_dir().join("release"); - let a_bin = build_dir.join("build_all_a"); - let b_bin = build_dir.join("build_all_b"); + let mut a_bin = build_dir.join("build_all_a"); + let mut b_bin = build_dir.join("build_all_b"); + + if cfg!(windows) { + a_bin.set_extension("exe"); + b_bin.set_extension("exe"); + } // Remove the files we just built. fs::remove_file(&a_bin).unwrap(); @@ -806,8 +822,13 @@ fn build_one() { project.cargo_fuzz().arg("build").assert().success(); let build_dir = project.fuzz_build_dir().join("release"); - let a_bin = build_dir.join("build_one_a"); - let b_bin = build_dir.join("build_one_b"); + let mut a_bin = build_dir.join("build_one_a"); + let mut b_bin = build_dir.join("build_one_b"); + + if cfg!(windows) { + a_bin.set_extension("exe"); + b_bin.set_extension("exe"); + } // Remove the files we just built. fs::remove_file(&a_bin).unwrap(); @@ -857,8 +878,16 @@ fn build_dev() { let build_dir = project.fuzz_build_dir().join("debug"); - let a_bin = build_dir.join("build_dev_a"); - let b_bin = build_dir.join("build_dev_b"); + let a_bin = build_dir.join(if cfg!(windows) { + "build_dev_a.exe" + } else { + "build_dev_a" + }); + let b_bin = build_dir.join(if cfg!(windows) { + "build_dev_b.exe" + } else { + "build_dev_b" + }); // Remove the files we just built. fs::remove_file(&a_bin).unwrap(); @@ -901,7 +930,11 @@ fn build_stripping_dead_code() { let build_dir = project.fuzz_build_dir().join("debug"); - let a_bin = build_dir.join("build_strip_a"); + let a_bin = build_dir.join(if cfg!(windows) { + "build_strip_a.exe" + } else { + "build_strip_a" + }); assert!(a_bin.is_file(), "Not a file: {}", a_bin.display()); } diff --git a/tests/tests/project.rs b/tests/tests/project.rs index 3491d72..c4cc50b 100644 --- a/tests/tests/project.rs +++ b/tests/tests/project.rs @@ -106,12 +106,12 @@ impl ProjectBuilder { r#" [[bin]] name = "{name}" - path = "{path}" + path = {path} test = false doc = false "#, name = name, - path = path.display(), + path = toml::to_string(&path).unwrap(), ) .unwrap();