1#![deny(clippy::expect_used)]
16#![deny(clippy::unwrap_used)]
17#![warn(clippy::panic)]
18
19use anyhow::{Context, Result, anyhow, bail, ensure};
20use bitflags::bitflags;
21use cargo_metadata::{
22 Artifact, ArtifactProfile, CargoOpt, Message, Metadata, MetadataCommand, Package, PackageId,
23};
24use clap::{ValueEnum, crate_version};
25use heck::ToKebabCase;
26use internal::dirs::{
27 corpus_directory_from_target, crashes_directory_from_target,
28 generic_args_directory_from_target, hangs_directory_from_target,
29 impl_generic_args_directory_from_target, output_directory_from_target,
30 queue_directory_from_target, target_directory,
31};
32use log::debug;
33use mio::{Events, Interest, Poll, Token, unix::pipe::Receiver};
34use semver::{Version, VersionReq};
35use serde::{Deserialize, Serialize};
36use std::{
37 collections::VecDeque,
38 ffi::OsStr,
39 fmt::{Debug, Formatter},
40 fs::{File, create_dir_all, read, read_dir, remove_dir_all},
41 io::{BufRead, Read},
42 iter,
43 path::{Path, PathBuf},
44 process::{Child as StdChild, Command, Stdio, exit},
45 sync::OnceLock,
46 time::Duration,
47};
48use strum_macros::Display;
49use subprocess::{CommunicateError, Exec, ExitStatus, NullFile, Redirection};
50
51const AUTO_GENERATED_SUFFIX: &str = "_fuzz__::auto_generate";
52const ENTRY_SUFFIX: &str = "_fuzz__::entry";
53
54const BASE_ENVS: &[(&str, &str)] = &[("TEST_FUZZ", "1"), ("TEST_FUZZ_WRITE", "0")];
55
56const DEFAULT_TIMEOUT: u64 = 1;
57
58const MILLIS_PER_SEC: u64 = 1_000;
59
60bitflags! {
61 #[derive(Copy, Clone, Eq, PartialEq)]
62 struct Flags: u8 {
63 const REQUIRES_CARGO_TEST = 0b0000_0001;
64 const RAW = 0b0000_0010;
65 }
66}
67
68#[derive(Clone, Copy, Debug, Display, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
69#[remain::sorted]
70pub enum Object {
71 Corpus,
72 CorpusInstrumented,
73 Crashes,
74 CrashesInstrumented,
75 GenericArgs,
76 Hangs,
77 HangsInstrumented,
78 ImplGenericArgs,
79 Queue,
80 QueueInstrumented,
81}
82
83#[allow(clippy::struct_excessive_bools)]
84#[derive(Clone, Debug, Default, Deserialize, Serialize)]
85#[remain::sorted]
86pub struct TestFuzz {
87 pub backtrace: bool,
88 pub consolidate: bool,
89 pub consolidate_all: bool,
90 pub cpus: Option<usize>,
91 pub display: Option<Object>,
92 pub exact: bool,
93 pub exit_code: bool,
94 pub features: Vec<String>,
95 pub list: bool,
96 pub manifest_path: Option<String>,
97 pub max_total_time: Option<u64>,
98 pub no_default_features: bool,
99 pub no_run: bool,
100 pub no_ui: bool,
101 pub package: Option<String>,
102 pub persistent: bool,
103 pub pretty: bool,
104 pub release: bool,
105 pub replay: Option<Object>,
106 pub reset: bool,
107 pub reset_all: bool,
108 pub resume: bool,
109 pub run_until_crash: bool,
110 pub slice: u64,
111 pub test: Option<String>,
112 pub timeout: Option<u64>,
113 pub verbose: bool,
114 pub ztarget: Option<String>,
115 pub zzargs: Vec<String>,
116}
117
118#[derive(Clone, Deserialize, Serialize)]
119struct Executable {
120 path: PathBuf,
121 name: String,
122 test_fuzz_version: Option<Version>,
123 afl_version: Option<Version>,
124}
125
126impl Debug for Executable {
127 #[cfg_attr(dylint_lib = "general", allow(non_local_effect_before_error_return))]
128 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
129 let test_fuzz_version = self
130 .test_fuzz_version
131 .as_ref()
132 .map(ToString::to_string)
133 .unwrap_or_default();
134 let afl_version = self
135 .afl_version
136 .as_ref()
137 .map(ToString::to_string)
138 .unwrap_or_default();
139 fmt.debug_struct("Executable")
140 .field("path", &self.path)
141 .field("name", &self.name)
142 .field("test_fuzz_version", &test_fuzz_version)
143 .field("afl_version", &afl_version)
144 .finish()
145 }
146}
147
148pub fn run(opts: TestFuzz) -> Result<()> {
149 let opts = {
150 let mut opts = opts;
151 if opts.exit_code {
152 opts.no_ui = true;
153 }
154 opts
155 };
156
157 let no_instrumentation = opts.list
158 || matches!(
159 opts.display,
160 Some(
161 Object::Corpus
162 | Object::Crashes
163 | Object::Hangs
164 | Object::ImplGenericArgs
165 | Object::GenericArgs
166 | Object::Queue
167 )
168 )
169 || matches!(
170 opts.replay,
171 Some(Object::Corpus | Object::Crashes | Object::Hangs | Object::Queue)
172 );
173
174 run_without_exit_code(&opts, !no_instrumentation).map_err(|error| {
175 if opts.exit_code {
176 eprintln!("{error:?}");
177 exit(2);
178 }
179 error
180 })
181}
182
183#[doc(hidden)]
184pub fn run_without_exit_code(opts: &TestFuzz, use_instrumentation: bool) -> Result<()> {
185 if let Some(object) = opts.replay {
186 ensure!(
187 !matches!(object, Object::ImplGenericArgs | Object::GenericArgs),
188 "`--replay {}` is invalid.",
189 object.to_string().to_kebab_case()
190 );
191 }
192
193 let _ = cached_cargo_afl_version();
195
196 let display = opts.display.is_some();
197
198 let replay = opts.replay.is_some();
199
200 let executables = build(opts, use_instrumentation, display || replay)?;
201
202 let mut executable_targets = executable_targets(&executables)?;
203
204 if let Some(pat) = &opts.ztarget {
205 executable_targets = filter_executable_targets(opts, pat, &executable_targets);
206 }
207
208 check_test_fuzz_and_afl_versions(&executable_targets)?;
209
210 if opts.list {
211 println!("{executable_targets:#?}");
212 return Ok(());
213 }
214
215 if opts.no_run {
216 return Ok(());
217 }
218
219 if opts.consolidate_all || opts.reset_all {
220 if opts.consolidate_all {
221 consolidate(opts, &executable_targets)?;
222 }
223 return reset(opts, &executable_targets);
224 }
225
226 if opts.consolidate || opts.reset || display || replay {
227 let (executable, target) = executable_target(opts, &executable_targets)?;
228
229 if opts.consolidate || opts.reset {
230 if opts.consolidate {
231 consolidate(opts, &executable_targets)?;
232 }
233 return reset(opts, &executable_targets);
234 }
235
236 let (flags, dir) = None
237 .or_else(|| {
238 opts.display
239 .map(|object| flags_and_dir(object, &executable.name, &target))
240 })
241 .or_else(|| {
242 opts.replay
243 .map(|object| flags_and_dir(object, &executable.name, &target))
244 })
245 .unwrap_or_else(|| (Flags::empty(), PathBuf::default()));
246
247 return for_each_entry(opts, &executable, &target, display, replay, flags, &dir);
248 }
249
250 if !use_instrumentation {
251 return Ok(());
252 }
253
254 let executable_targets = flatten_executable_targets(opts, executable_targets)?;
255
256 fuzz(opts, &executable_targets)
257}
258
259#[allow(clippy::too_many_lines)]
260fn build(opts: &TestFuzz, use_instrumentation: bool, quiet: bool) -> Result<Vec<Executable>> {
261 let metadata = metadata(opts)?;
262 let silence_stderr = quiet && !opts.verbose;
263
264 let mut args = vec![];
265 if use_instrumentation {
266 args.extend_from_slice(&["afl"]);
267 }
268 args.extend_from_slice(&["test", "--offline", "--no-run"]);
269 if opts.no_default_features {
270 args.extend_from_slice(&["--no-default-features"]);
271 }
272 for features in &opts.features {
273 args.extend_from_slice(&["--features", features]);
274 }
275 if opts.release {
276 args.extend_from_slice(&["--release"]);
277 }
278 let target_dir = target_directory(true);
279 let target_dir_str = target_dir.to_string_lossy();
280 if use_instrumentation {
281 args.extend_from_slice(&["--target-dir", &target_dir_str]);
282 }
283 if let Some(path) = &opts.manifest_path {
284 args.extend_from_slice(&["--manifest-path", path]);
285 }
286 if let Some(package) = &opts.package {
287 args.extend_from_slice(&["--package", package]);
288 }
289 if opts.persistent {
290 args.extend_from_slice(&["--features", "test-fuzz/__persistent"]);
291 }
292 if let Some(name) = &opts.test {
293 args.extend_from_slice(&["--test", name]);
294 }
295
296 let mut exec = Exec::cmd("cargo")
301 .args(
302 &args
303 .iter()
304 .chain(iter::once(&"--message-format=json"))
305 .collect::<Vec<_>>(),
306 )
307 .stdout(Redirection::Pipe);
308 if silence_stderr {
309 exec = exec.stderr(NullFile);
310 }
311 debug!("{exec:?}");
312 let mut popen = exec.clone().popen()?;
313 let messages = popen
314 .stdout
315 .take()
316 .map_or(Ok(vec![]), |stream| -> Result<_> {
317 let reader = std::io::BufReader::new(stream);
318 Message::parse_stream(reader)
319 .collect::<std::result::Result<_, std::io::Error>>()
320 .with_context(|| format!("`parse_stream` failed for `{exec:?}`"))
321 })?;
322 let status = popen
323 .wait()
324 .with_context(|| format!("`wait` failed for `{popen:?}`"))?;
325
326 if !status.success() {
327 eprintln!("{messages:#?}");
331 bail!("Command failed: {:?}", exec);
332 }
333
334 let executables = messages
335 .into_iter()
336 .map(|message| {
337 if let Message::CompilerArtifact(Artifact {
338 package_id,
339 target: build_target,
340 profile: ArtifactProfile { test: true, .. },
341 executable: Some(executable),
342 ..
343 }) = message
344 {
345 let (test_fuzz_version, afl_version) =
346 test_fuzz_and_afl_versions(&metadata, &package_id)?;
347 Ok(Some(Executable {
348 path: executable.into(),
349 name: build_target.name,
350 test_fuzz_version,
351 afl_version,
352 }))
353 } else {
354 Ok(None)
355 }
356 })
357 .collect::<Result<Vec<_>>>()?;
358 Ok(executables.into_iter().flatten().collect())
359}
360
361fn metadata(opts: &TestFuzz) -> Result<Metadata> {
362 let mut command = MetadataCommand::new();
363 if opts.no_default_features {
364 command.features(CargoOpt::NoDefaultFeatures);
365 }
366 let mut features = opts.features.clone();
367 features.push("test-fuzz/__persistent".to_owned());
368 command.features(CargoOpt::SomeFeatures(features));
369 if let Some(path) = &opts.manifest_path {
370 command.manifest_path(path);
371 }
372 command.exec().map_err(Into::into)
373}
374
375fn test_fuzz_and_afl_versions(
376 metadata: &Metadata,
377 package_id: &PackageId,
378) -> Result<(Option<Version>, Option<Version>)> {
379 let test_fuzz = package_dependency(metadata, package_id, "test-fuzz")?;
380 let afl = test_fuzz
381 .as_ref()
382 .map(|package_id| package_dependency(metadata, package_id, "afl"))
383 .transpose()?;
384 let test_fuzz_version = test_fuzz
385 .map(|package_id| package_version(metadata, &package_id))
386 .transpose()?;
387 let afl_version = afl
388 .flatten()
389 .map(|package_id| package_version(metadata, &package_id))
390 .transpose()?;
391 Ok((test_fuzz_version, afl_version))
392}
393
394fn package_dependency(
395 metadata: &Metadata,
396 package_id: &PackageId,
397 name: &str,
398) -> Result<Option<PackageId>> {
399 let resolve = metadata
400 .resolve
401 .as_ref()
402 .ok_or_else(|| anyhow!("No dependency graph"))?;
403 let node = resolve
404 .nodes
405 .iter()
406 .find(|node| node.id == *package_id)
407 .ok_or_else(|| anyhow!("Could not find package `{}`", package_id))?;
408 let package_ids_and_names = node
409 .dependencies
410 .iter()
411 .map(|package_id| {
412 package_name(metadata, package_id).map(|package_name| (package_id, package_name))
413 })
414 .collect::<Result<Vec<_>>>()?;
415 Ok(package_ids_and_names
416 .into_iter()
417 .find_map(|(package_id, package_name)| {
418 if package_name == name {
419 Some(package_id.clone())
420 } else {
421 None
422 }
423 }))
424}
425
426fn package_name(metadata: &Metadata, package_id: &PackageId) -> Result<String> {
427 package(metadata, package_id).map(|package| package.name.clone())
428}
429
430fn package_version(metadata: &Metadata, package_id: &PackageId) -> Result<Version> {
431 package(metadata, package_id).map(|package| package.version.clone())
432}
433
434fn package<'a>(metadata: &'a Metadata, package_id: &PackageId) -> Result<&'a Package> {
435 metadata
436 .packages
437 .iter()
438 .find(|package| package.id == *package_id)
439 .ok_or_else(|| anyhow!("Could not find package `{}`", package_id))
440}
441
442fn executable_targets(executables: &[Executable]) -> Result<Vec<(Executable, Vec<String>)>> {
443 let executable_targets: Vec<(Executable, Vec<String>)> = executables
444 .iter()
445 .map(|executable| {
446 let targets = targets(&executable.path)?;
447 Ok((executable.clone(), targets))
448 })
449 .collect::<Result<_>>()?;
450
451 Ok(executable_targets
452 .into_iter()
453 .filter(|(_, targets)| !targets.is_empty())
454 .collect())
455}
456
457fn targets(executable: &Path) -> Result<Vec<String>> {
458 let exec = Exec::cmd(executable)
459 .env_extend(&[("AFL_QUIET", "1")])
460 .args(&["--list", "--format=terse"])
461 .stderr(NullFile);
462 debug!("{exec:?}");
463 let stream = exec.clone().stream_stdout()?;
464
465 let mut targets = Vec::<String>::default();
469 for line in std::io::BufReader::new(stream).lines() {
470 let line = line.with_context(|| format!("Could not get output of `{exec:?}`"))?;
471 let Some(line) = line.strip_suffix(": test") else {
472 continue;
473 };
474 let Some(line) = line.strip_suffix(ENTRY_SUFFIX) else {
475 continue;
476 };
477 targets.push(line.to_owned());
478 }
479 Ok(targets)
480}
481
482fn filter_executable_targets(
483 opts: &TestFuzz,
484 pat: &str,
485 executable_targets: &[(Executable, Vec<String>)],
486) -> Vec<(Executable, Vec<String>)> {
487 executable_targets
488 .iter()
489 .filter_map(|(executable, targets)| {
490 let targets = filter_targets(opts, pat, targets);
491 if targets.is_empty() {
492 None
493 } else {
494 Some((executable.clone(), targets))
495 }
496 })
497 .collect()
498}
499
500fn filter_targets(opts: &TestFuzz, pat: &str, targets: &[String]) -> Vec<String> {
501 targets
502 .iter()
503 .filter(|target| (!opts.exact && target.contains(pat)) || target.as_str() == pat)
504 .cloned()
505 .collect()
506}
507
508fn executable_target(
509 opts: &TestFuzz,
510 executable_targets: &[(Executable, Vec<String>)],
511) -> Result<(Executable, String)> {
512 let mut executable_targets = executable_targets.to_vec();
513
514 ensure!(
515 executable_targets.len() <= 1,
516 "Found multiple executables with fuzz targets{}: {:#?}",
517 match_message(opts),
518 executable_targets
519 );
520
521 let Some(mut executable_targets) = executable_targets.pop() else {
522 bail!("Found no fuzz targets{}", match_message(opts));
523 };
524
525 ensure!(
526 executable_targets.1.len() <= 1,
527 "Found multiple fuzz targets{} in {:?}: {:#?}",
528 match_message(opts),
529 executable_targets.0,
530 executable_targets.1
531 );
532
533 #[allow(clippy::expect_used)]
534 Ok((
535 executable_targets.0,
536 executable_targets
537 .1
538 .pop()
539 .expect("Executable with no fuzz targets"),
540 ))
541}
542
543fn match_message(opts: &TestFuzz) -> String {
544 opts.ztarget.as_ref().map_or(String::new(), |pat| {
545 format!(
546 " {} `{}`",
547 if opts.exact { "equal to" } else { "containing" },
548 pat
549 )
550 })
551}
552
553fn check_test_fuzz_and_afl_versions(
554 executable_targets: &[(Executable, Vec<String>)],
555) -> Result<()> {
556 let cargo_test_fuzz_version = Version::parse(crate_version!())?;
557 for (executable, _) in executable_targets {
558 check_dependency_version(
559 &executable.name,
560 "test-fuzz",
561 executable.test_fuzz_version.as_ref(),
562 "cargo-test-fuzz",
563 &cargo_test_fuzz_version,
564 )?;
565 check_dependency_version(
566 &executable.name,
567 "afl",
568 executable.afl_version.as_ref(),
569 "cargo-afl",
570 cached_cargo_afl_version(),
571 )?;
572 }
573 Ok(())
574}
575
576fn cached_cargo_afl_version() -> &'static Version {
577 #[allow(clippy::unwrap_used)]
578 CARGO_AFL_VERSION.get_or_init(|| cargo_afl_version().unwrap())
579}
580
581static CARGO_AFL_VERSION: OnceLock<Version> = OnceLock::new();
582
583fn cargo_afl_version() -> Result<Version> {
584 let mut command = Command::new("cargo");
585 command.args(["afl", "--version"]);
586 let output = command
587 .output()
588 .with_context(|| format!("Could not get output of `{command:?}`"))?;
589 let stdout = String::from_utf8_lossy(&output.stdout);
590 let version = stdout
591 .strip_prefix("cargo-afl ")
592 .and_then(|s| s.split_ascii_whitespace().next())
593 .ok_or_else(|| {
594 anyhow!(
595 "Could not determine `cargo-afl` version. Is it installed? Try `cargo install \
596 cargo-afl`."
597 )
598 })?;
599 Version::parse(version).map_err(Into::into)
600}
601
602fn check_dependency_version(
603 name: &str,
604 dependency: &str,
605 dependency_version: Option<&Version>,
606 binary: &str,
607 binary_version: &Version,
608) -> Result<()> {
609 if let Some(dependency_version) = dependency_version {
610 if binary_version.pre.is_empty() {
612 ensure!(
613 as_version_req(dependency_version).matches(binary_version)
614 || as_version_req(binary_version).matches(dependency_version),
615 "`{}` depends on `{} {}`, which is incompatible with `{} {}`.",
616 name,
617 dependency,
618 dependency_version,
619 binary,
620 binary_version
621 );
622 }
623 if !as_version_req(dependency_version).matches(binary_version) {
624 eprintln!(
625 "`{name}` depends on `{dependency} {dependency_version}`, which is newer than \
626 `{binary} {binary_version}`. Consider upgrading with `cargo install {binary} \
627 --force --version '>={dependency_version}'`."
628 );
629 }
630 } else {
631 bail!("`{}` does not depend on `{}`", name, dependency)
632 }
633 Ok(())
634}
635
636fn as_version_req(version: &Version) -> VersionReq {
637 #[allow(clippy::expect_used)]
638 VersionReq::parse(&version.to_string()).expect("Could not parse version as version request")
639}
640
641fn consolidate(opts: &TestFuzz, executable_targets: &[(Executable, Vec<String>)]) -> Result<()> {
642 assert!(opts.consolidate_all || executable_targets.len() == 1);
643
644 for (executable, targets) in executable_targets {
645 assert!(opts.consolidate_all || targets.len() == 1);
646
647 for target in targets {
648 let corpus_dir = corpus_directory_from_target(&executable.name, target);
649 let crashes_dir = crashes_directory_from_target(&executable.name, target);
650 let hangs_dir = hangs_directory_from_target(&executable.name, target);
651 let queue_dir = queue_directory_from_target(&executable.name, target);
652
653 for dir in &[crashes_dir, hangs_dir, queue_dir] {
654 for entry in read_dir(dir)
655 .with_context(|| format!("`read_dir` failed for `{}`", dir.to_string_lossy()))?
656 {
657 let entry = entry.with_context(|| {
658 format!("`read_dir` failed for `{}`", dir.to_string_lossy())
659 })?;
660 let path = entry.path();
661 let file_name = path
662 .file_name()
663 .map(OsStr::to_string_lossy)
664 .unwrap_or_default();
665
666 if file_name == "README.txt" || file_name == ".state" {
667 continue;
668 }
669
670 let data = read(&path).with_context(|| {
671 format!("`read` failed for `{}`", path.to_string_lossy())
672 })?;
673 runtime::write_data(&corpus_dir, &data).with_context(|| {
674 format!(
675 "`test_fuzz::runtime::write_data` failed for `{}`",
676 corpus_dir.to_string_lossy()
677 )
678 })?;
679 }
680 }
681 }
682 }
683
684 Ok(())
685}
686
687fn reset(opts: &TestFuzz, executable_targets: &[(Executable, Vec<String>)]) -> Result<()> {
688 assert!(opts.reset_all || executable_targets.len() == 1);
689
690 for (executable, targets) in executable_targets {
691 assert!(opts.reset_all || targets.len() == 1);
692
693 for target in targets {
694 let output_dir = output_directory_from_target(&executable.name, target);
695 if !output_dir.exists() {
696 continue;
697 }
698 remove_dir_all(&output_dir).with_context(|| {
699 format!(
700 "`remove_dir_all` failed for `{}`",
701 output_dir.to_string_lossy()
702 )
703 })?;
704 }
705 }
706
707 Ok(())
708}
709
710#[allow(clippy::panic)]
711fn flags_and_dir(object: Object, krate: &str, target: &str) -> (Flags, PathBuf) {
712 match object {
713 Object::Corpus | Object::CorpusInstrumented => (
714 Flags::REQUIRES_CARGO_TEST,
715 corpus_directory_from_target(krate, target),
716 ),
717 Object::Crashes | Object::CrashesInstrumented => {
718 (Flags::empty(), crashes_directory_from_target(krate, target))
719 }
720 Object::Hangs | Object::HangsInstrumented => {
721 (Flags::empty(), hangs_directory_from_target(krate, target))
722 }
723 Object::Queue | Object::QueueInstrumented => {
724 (Flags::empty(), queue_directory_from_target(krate, target))
725 }
726 Object::ImplGenericArgs => (
727 Flags::REQUIRES_CARGO_TEST | Flags::RAW,
728 impl_generic_args_directory_from_target(krate, target),
729 ),
730 Object::GenericArgs => (
731 Flags::REQUIRES_CARGO_TEST | Flags::RAW,
732 generic_args_directory_from_target(krate, target),
733 ),
734 }
735}
736
737#[allow(clippy::too_many_lines)]
738fn for_each_entry(
739 opts: &TestFuzz,
740 executable: &Executable,
741 target: &str,
742 display: bool,
743 replay: bool,
744 flags: Flags,
745 dir: &Path,
746) -> Result<()> {
747 ensure!(
748 dir.exists(),
749 "Could not find `{}`{}",
750 dir.to_string_lossy(),
751 if flags.contains(Flags::REQUIRES_CARGO_TEST) {
752 ". Did you remember to run `cargo test`?"
753 } else {
754 ""
755 }
756 );
757
758 let mut envs = BASE_ENVS.to_vec();
759 envs.push(("AFL_QUIET", "1"));
760 if display {
761 envs.push(("TEST_FUZZ_DISPLAY", "1"));
762 }
763 if replay {
764 envs.push(("TEST_FUZZ_REPLAY", "1"));
765 }
766 if opts.backtrace {
767 envs.push(("RUST_BACKTRACE", "1"));
768 }
769 if opts.pretty {
770 envs.push(("TEST_FUZZ_PRETTY_PRINT", "1"));
771 }
772
773 let args: Vec<String> = vec![
774 "--exact",
775 &(target.to_owned() + ENTRY_SUFFIX),
776 "--nocapture",
777 ]
778 .into_iter()
779 .map(String::from)
780 .collect();
781
782 let mut nonempty = false;
783 let mut failure = false;
784 let mut timeout = false;
785 let mut output = false;
786
787 for entry in read_dir(dir)
788 .with_context(|| format!("`read_dir` failed for `{}`", dir.to_string_lossy()))?
789 {
790 let entry =
791 entry.with_context(|| format!("`read_dir` failed for `{}`", dir.to_string_lossy()))?;
792 let path = entry.path();
793 let mut file = File::open(&path)
794 .with_context(|| format!("`open` failed for `{}`", path.to_string_lossy()))?;
795 let file_name = path
796 .file_name()
797 .map(OsStr::to_string_lossy)
798 .unwrap_or_default();
799
800 if file_name == "README.txt" || file_name == ".state" {
801 continue;
802 }
803
804 let (buffer, status) = if flags.contains(Flags::RAW) {
805 let mut buffer = Vec::new();
806 file.read_to_end(&mut buffer).with_context(|| {
807 format!("`read_to_end` failed for `{}`", path.to_string_lossy())
808 })?;
809 (buffer, Some(ExitStatus::Exited(0)))
810 } else {
811 let exec = Exec::cmd(&executable.path)
812 .env_extend(&envs)
813 .args(&args)
814 .stdin(file)
815 .stdout(NullFile)
816 .stderr(Redirection::Pipe);
817 debug!("{exec:?}");
818 let mut popen = exec
819 .clone()
820 .popen()
821 .with_context(|| format!("`popen` failed for `{exec:?}`"))?;
822 let secs = opts.timeout.unwrap_or(DEFAULT_TIMEOUT);
823 let time = Duration::from_secs(secs);
824 let mut communicator = popen.communicate_start(None).limit_time(time);
825 match communicator.read() {
826 Ok((_, buffer)) => {
827 let status = popen.wait()?;
828 (buffer.unwrap_or_default(), Some(status))
829 }
830 Err(CommunicateError {
831 error,
832 capture: (_, buffer),
833 }) => {
834 popen
835 .kill()
836 .with_context(|| format!("`kill` failed for `{popen:?}`"))?;
837 if error.kind() != std::io::ErrorKind::TimedOut {
838 return Err(anyhow!(error));
839 }
840 let _ = popen.wait()?;
841 (buffer.unwrap_or_default(), None)
842 }
843 }
844 };
845
846 print!("{file_name}: ");
847 if let Some(last) = buffer.last() {
848 print!("{}", String::from_utf8_lossy(&buffer));
849 if last != &b'\n' {
850 println!();
851 }
852 output = true;
853 }
854 status.map_or_else(
855 || {
856 println!("Timeout");
857 timeout = true;
858 },
859 |status| {
860 if !flags.contains(Flags::RAW) && buffer.is_empty() {
861 println!("{status:?}");
862 }
863 failure |= !status.success();
864 },
865 );
866
867 nonempty = true;
868 }
869
870 assert!(!(!nonempty && (failure || timeout || output)));
871
872 if !nonempty {
873 eprintln!(
874 "Nothing to {}.",
875 match (display, replay) {
876 (true, true) => "display/replay",
877 (true, false) => "display",
878 (false, true) => "replay",
879 (false, false) => unreachable!(),
880 }
881 );
882 return Ok(());
883 }
884
885 if !failure && !timeout && !output {
886 eprintln!("No output on stderr detected.");
887 return Ok(());
888 }
889
890 if (failure || timeout) && !replay {
891 eprintln!(
892 "Encountered a {} while not replaying. A buggy Debug implementation perhaps?",
893 if failure {
894 "failure"
895 } else if timeout {
896 "timeout"
897 } else {
898 unreachable!()
899 }
900 );
901 return Ok(());
902 }
903
904 Ok(())
905}
906
907fn flatten_executable_targets(
908 opts: &TestFuzz,
909 executable_targets: Vec<(Executable, Vec<String>)>,
910) -> Result<Vec<(Executable, String)>> {
911 let executable_targets = executable_targets
912 .into_iter()
913 .flat_map(|(executable, targets)| {
914 targets
915 .into_iter()
916 .map(move |target| (executable.clone(), target))
917 })
918 .collect::<Vec<_>>();
919
920 ensure!(
921 !executable_targets.is_empty(),
922 "Found no fuzz targets{}",
923 match_message(opts)
924 );
925
926 Ok(executable_targets)
927}
928
929struct Config {
930 ui: bool,
931 sufficient_cpus: bool,
932 first_run: bool,
933}
934
935struct Child {
936 exec: String,
937 popen: StdChild,
938 receiver: Receiver,
939 unprinted_data: Vec<u8>,
940 output_buffer: VecDeque<String>,
941 time_limit_was_reached: bool,
942 testing_aborted_programmatically: bool,
943}
944
945impl Child {
946 fn read_lines(&mut self) -> Result<String> {
947 loop {
948 let mut buf = [0; 4096];
949 let n = match self.receiver.read(&mut buf) {
950 Ok(0) => break,
951 Ok(n) => n,
952 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
953 Err(error) => return Err(error.into()),
954 };
955 self.unprinted_data.extend_from_slice(&buf[0..n]);
956 }
957 if let Some(i) = self.unprinted_data.iter().rev().position(|&c| c == b'\n') {
958 let mut buf = self.unprinted_data.split_off(self.unprinted_data.len() - i);
959 std::mem::swap(&mut self.unprinted_data, &mut buf);
960 String::from_utf8(buf).map_err(Into::into)
961 } else {
962 Ok(String::new())
963 }
964 }
965
966 fn print_line(&mut self, opts: &TestFuzz, line: String) {
967 if opts.no_ui {
968 println!("{line}");
969 } else {
970 self.output_buffer.push_back(line);
971 }
972 }
973
974 fn refresh(opts: &TestFuzz, n_children: usize, children: &mut [Option<Child>]) {
975 if opts.no_ui {
976 return;
977 }
978
979 cursor_to_home_position();
980
981 let termsize::Size { rows, cols } = termsize::get().unwrap();
982 let rows = rows as usize;
983 let cols = cols as usize;
984
985 let Some(n_available_rows) = rows.checked_sub(n_children) else {
988 return;
989 };
990
991 let children = children.iter_mut().flatten().collect::<Vec<_>>();
992
993 assert_eq!(n_children, children.len());
994
995 for (i_child, child) in children.into_iter().enumerate() {
996 if i_child != 0 {
997 println!("{:-<cols$}", "");
998 }
999 let n_child_rows = n_available_rows / n_children
1000 + if i_child < n_available_rows % n_children {
1001 1
1002 } else {
1003 0
1004 };
1005 let n_lines_to_skip = child.output_buffer.len().saturating_sub(n_child_rows);
1006 child.output_buffer.drain(..n_lines_to_skip);
1007 for i in 0..n_child_rows {
1008 if let Some(line) = child.output_buffer.get(i) {
1009 let prefix = prefix_with_width(line, cols);
1010 print!("{prefix}");
1011 }
1012 clear_to_end_of_line();
1013 println!();
1014 }
1015 }
1016 }
1017}
1018
1019fn cursor_to_home_position() {
1020 print!("\x1b[H");
1021}
1022
1023fn clear_to_end_of_line() {
1024 print!("\x1b[0K");
1025}
1026
1027#[cfg_attr(dylint_lib = "supplementary", allow(commented_out_code))]
1028fn prefix_with_width(s: &str, width: usize) -> &str {
1029 let mut min = 0;
1030 let mut max = s.len();
1031 while min < max {
1032 let mid = (min + max) / 2;
1033 let prefix = strip_ansi_escapes::strip(&s[..mid]);
1034 if prefix.len() < width {
1035 min = mid + 1;
1036 } else {
1037 max = mid;
1039 }
1040 }
1041 assert_eq!(min, max);
1042 &s[..min]
1043}
1044
1045#[allow(clippy::too_many_lines)]
1046fn fuzz(opts: &TestFuzz, executable_targets: &[(Executable, String)]) -> Result<()> {
1047 auto_generate_corpora(executable_targets)?;
1048
1049 let mut config = Config {
1050 ui: !opts.no_ui,
1051 sufficient_cpus: true,
1052 first_run: true,
1053 };
1054
1055 if let (false, [(executable, target)]) = (opts.exit_code, executable_targets) {
1056 let mut command = fuzz_command(opts, &config, executable, target);
1057 let status = command
1058 .status()
1059 .with_context(|| format!("Could not get status of `{command:?}`"))?;
1060 ensure!(status.success(), "Command failed: {:?}", command);
1061 return Ok(());
1062 }
1063
1064 let n_cpus = std::cmp::min(
1065 opts.cpus.unwrap_or_else(|| num_cpus::get() - 1),
1066 num_cpus::get(),
1067 );
1068
1069 ensure!(n_cpus >= 1, "Number of cpus must be greater than zero");
1070
1071 config.sufficient_cpus = n_cpus >= executable_targets.len();
1072
1073 if !config.sufficient_cpus {
1074 ensure!(
1075 opts.max_total_time.is_none(),
1076 "--max-total-time cannot be used when number of cpus ({n_cpus}) is less than number \
1077 of fuzz targets ({})",
1078 executable_targets.len()
1079 );
1080
1081 eprintln!(
1082 "Number of cpus ({n_cpus}) is less than number of fuzz targets ({}); fuzzing each for \
1083 {} seconds",
1084 executable_targets.len(),
1085 opts.slice
1086 );
1087 }
1088
1089 let mut n_children = 0;
1090 let mut i_task = 0;
1091 let mut executable_targets_iter = executable_targets.iter().cycle();
1092 let mut poll = Poll::new().with_context(|| "`Poll::new` failed")?;
1093 let mut events = Events::with_capacity(128);
1094 let mut children = vec![(); executable_targets.len()]
1095 .into_iter()
1096 .map(|()| None::<Child>)
1097 .collect::<Vec<_>>();
1098 let mut i_target_prev = executable_targets.len();
1099
1100 let mut failed_targets = std::collections::HashSet::new();
1102
1103 loop {
1104 Child::refresh(opts, n_children, children.as_mut_slice());
1105
1106 if failed_targets.len() == executable_targets.len() {
1108 bail!("All targets failed to start");
1109 }
1110
1111 if n_children < n_cpus && (i_task < executable_targets.len() || !config.sufficient_cpus) {
1112 let Some((executable, target)) = executable_targets_iter.next() else {
1113 unreachable!();
1114 };
1115
1116 let i_target = i_task % executable_targets.len();
1117
1118 if failed_targets.contains(&i_target) {
1120 i_task += 1;
1121 continue;
1122 }
1123
1124 if children[i_target].is_some() {
1130 assert!(!config.sufficient_cpus);
1131 i_task += 1;
1132 continue;
1133 }
1134
1135 config.first_run = i_task < executable_targets.len();
1136
1137 assert!(config.first_run || !config.sufficient_cpus);
1140
1141 let mut command = fuzz_command(opts, &config, executable, target);
1142
1143 let exec = format!("{command:?}");
1144 command.stdout(Stdio::piped());
1145 let mut popen = command
1146 .spawn()
1147 .with_context(|| format!("Could not spawn `{exec:?}`"))?;
1148 let stdout = popen
1149 .stdout
1150 .take()
1151 .ok_or_else(|| anyhow!("Could not get output of `{exec:?}`"))?;
1152 let mut receiver = Receiver::from(stdout);
1153 receiver
1154 .set_nonblocking(true)
1155 .with_context(|| "Could not make receiver non-blocking")?;
1156 poll.registry()
1157 .register(&mut receiver, Token(i_target), Interest::READABLE)
1158 .with_context(|| "Could not register receiver")?;
1159 children[i_target] = Some(Child {
1160 exec,
1161 popen,
1162 receiver,
1163 unprinted_data: Vec::new(),
1164 output_buffer: VecDeque::new(),
1165 time_limit_was_reached: false,
1166 testing_aborted_programmatically: false,
1167 });
1168
1169 n_children += 1;
1170 i_task += 1;
1171 continue;
1172 }
1173
1174 if n_children == 0 {
1175 assert!(config.sufficient_cpus);
1176 assert!(i_task >= executable_targets.len());
1177 break;
1178 }
1179
1180 poll.poll(&mut events, None)
1181 .with_context(|| "`poll` failed")?;
1182
1183 for event in &events {
1184 let Token(i_target) = event.token();
1185 let (_, target) = &executable_targets[i_target];
1186 #[allow(clippy::panic)]
1187 let child = children[i_target]
1188 .as_mut()
1189 .unwrap_or_else(|| panic!("Child for token {i_target} should exist"));
1190
1191 let s = child.read_lines()?;
1192 for line in s.lines() {
1193 if line.contains("Time limit was reached") {
1194 child.time_limit_was_reached = true;
1195 }
1196 if line.contains("+++ Testing aborted programmatically +++") {
1197 child.testing_aborted_programmatically = true;
1198 }
1199 if opts.no_ui
1200 && i_target_prev < executable_targets.len()
1201 && i_target_prev != i_target
1202 {
1203 println!("---");
1204 }
1205 child.print_line(opts, format!("{target}: {line}"));
1206 i_target_prev = i_target;
1207 }
1208
1209 if event.is_read_closed() {
1210 #[allow(clippy::panic)]
1211 let mut child = children[i_target]
1212 .take()
1213 .unwrap_or_else(|| panic!("Child for token {i_target} should exist"));
1214 poll.registry()
1215 .deregister(&mut child.receiver)
1216 .with_context(|| "Could not deregister receiver")?;
1217 n_children -= 1;
1218
1219 let status = child
1220 .popen
1221 .wait()
1222 .with_context(|| format!("`wait` failed for `{:?}`", child.popen))?;
1223
1224 if !status.success() {
1225 eprintln!(
1226 "Warning: Command failed for target {}: {:?}\nstdout: ```\n{}\n```",
1227 target,
1228 child.exec,
1229 itertools::join(child.output_buffer.iter(), "\n")
1230 );
1231 failed_targets.insert(i_target);
1232 continue;
1233 }
1234
1235 if !child.testing_aborted_programmatically {
1236 eprintln!(
1237 r#"Warning: Could not find "Testing aborted programmatically" in command output for target {}: {:?}"#,
1238 target, child.exec
1239 );
1240 failed_targets.insert(i_target);
1241 continue;
1242 }
1243
1244 if opts.exit_code && !child.time_limit_was_reached {
1245 exit(1);
1246 }
1247 }
1248 }
1249 }
1250
1251 Ok(())
1252}
1253
1254fn fuzz_command(
1255 opts: &TestFuzz,
1256 config: &Config,
1257 executable: &Executable,
1258 target: &str,
1259) -> Command {
1260 let input_dir = if opts.resume || !config.first_run {
1261 "-".to_owned()
1262 } else {
1263 corpus_directory_from_target(&executable.name, target)
1264 .to_string_lossy()
1265 .into_owned()
1266 };
1267
1268 let output_dir = output_directory_from_target(&executable.name, target);
1269 create_dir_all(&output_dir).unwrap_or_default();
1270
1271 let mut envs = BASE_ENVS.to_vec();
1272 if !config.ui {
1273 envs.push(("AFL_NO_UI", "1"));
1274 }
1275 if opts.run_until_crash {
1276 envs.push(("AFL_BENCH_UNTIL_CRASH", "1"));
1277 }
1278
1279 let mut args = vec![];
1284 args.extend(
1285 vec![
1286 "afl",
1287 "fuzz",
1288 "-c-",
1289 "-i",
1290 &input_dir,
1291 "-o",
1292 &output_dir.to_string_lossy(),
1293 "-M",
1294 "default",
1295 ]
1296 .into_iter()
1297 .map(String::from),
1298 );
1299 if !config.sufficient_cpus {
1300 args.extend(["-V".to_owned(), opts.slice.to_string()]);
1301 } else if let Some(max_total_time) = opts.max_total_time {
1302 args.extend(["-V".to_owned(), max_total_time.to_string()]);
1303 }
1304 if let Some(timeout) = opts.timeout {
1305 args.extend(["-t".to_owned(), format!("{}", timeout * MILLIS_PER_SEC)]);
1306 }
1307 args.extend(opts.zzargs.clone());
1308 args.extend(
1309 vec![
1310 "--",
1311 &executable.path.to_string_lossy(),
1312 "--exact",
1313 &(target.to_owned() + ENTRY_SUFFIX),
1314 ]
1315 .into_iter()
1316 .map(String::from),
1317 );
1318
1319 let mut command = Command::new("cargo");
1320 command.envs(envs).args(args);
1321 debug!("{command:?}");
1322 command
1323}
1324
1325fn auto_generate_corpora(executable_targets: &[(Executable, String)]) -> Result<()> {
1326 for (executable, target) in executable_targets {
1327 let corpus_dir = corpus_directory_from_target(&executable.name, target);
1328 if !corpus_dir.exists() {
1329 eprintln!(
1330 "Could not find `{}`. Trying to auto-generate it...",
1331 corpus_dir.to_string_lossy(),
1332 );
1333 auto_generate_corpus(executable, target)?;
1334 ensure!(
1335 corpus_dir.exists(),
1336 "Could not find or auto-generate `{}`. Please ensure `{}` is tested.",
1337 corpus_dir.to_string_lossy(),
1338 target
1339 );
1340 eprintln!("Auto-generated `{}`.", corpus_dir.to_string_lossy());
1341 }
1342 }
1343
1344 Ok(())
1345}
1346
1347fn auto_generate_corpus(executable: &Executable, target: &str) -> Result<()> {
1348 let mut command = Command::new(&executable.path);
1349 command.args(["--exact", &(target.to_owned() + AUTO_GENERATED_SUFFIX)]);
1350 debug!("{command:?}");
1351 let status = command
1352 .status()
1353 .with_context(|| format!("Could not get status of `{command:?}`"))?;
1354
1355 ensure!(status.success(), "Command failed: {:?}", command);
1356
1357 Ok(())
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362 #![allow(clippy::unwrap_used)]
1363
1364 use super::{TestFuzz, build, executable_targets};
1365 use predicates::prelude::*;
1366 use std::{env::set_current_dir, process::Command};
1367 use testing::CommandExt;
1368
1369 #[cfg_attr(dylint_lib = "supplementary", allow(non_thread_safe_call_in_test))]
1372 #[test]
1373 fn warn_if_test_fuzz_not_enabled() {
1374 const WARNING: &str = "If you are trying to run a test-fuzz-generated fuzzing harness, be \
1375 sure to run with `TEST_FUZZ=1`.";
1376
1377 set_current_dir("../examples").unwrap();
1378
1379 let executables = build(&TestFuzz::default(), false, false).unwrap();
1380
1381 let executable_targets = executable_targets(&executables).unwrap();
1382
1383 let (executable, _) = executable_targets
1386 .iter()
1387 .filter(|(executable, _)| !["generic", "unserde"].contains(&executable.name.as_str()))
1388 .next()
1389 .unwrap();
1390
1391 for enable in [false, true] {
1392 let mut command = Command::new(&executable.path);
1393 if enable {
1394 command.env("TEST_FUZZ", "1");
1395 }
1396 let assert = command.logged_assert().success();
1397 if enable {
1398 assert.stderr(predicate::str::contains(WARNING).not());
1399 } else {
1400 assert.stderr(predicate::str::contains(WARNING));
1401 }
1402 }
1403 }
1404}