cargo_test_fuzz/
lib.rs

1//! # cargo-test-fuzz
2//!
3//! This crate provides the implementation for the `cargo test-fuzz` subcommand.
4//!
5//! ## Primary Exports
6//!
7//! - [`run`](fn.run.html): The main entry point function for executing fuzzing operations
8//! - [`TestFuzz`](struct.TestFuzz.html): Configuration struct containing all fuzzing options
9//! - [`Object`](enum.Object.html): Enum representing different types of fuzzing artifacts
10//!
11//! For more information on `test-fuzz`, see the project's [README].
12//!
13//! [README]: https://github.com/trailofbits/test-fuzz/blob/master/README.md
14
15#![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    // smoelius: Ensure `cargo-afl` is installed.
194    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    // smoelius: Suppress "Warning: AFL++ tools will need to set AFL_MAP_SIZE..." Setting
297    // `AFL_QUIET=1` doesn't work here, so pipe standard error to: /dev/null
298    // smoelius: Suppressing all of standard error is too extreme. For now, suppress only when
299    // displaying/replaying.
300    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        // smoelius: If stderr was silenced, re-execute the command without --message-format=json.
328        // This is easier than trying to capture and colorize `CompilerMessage`s like Cargo does.
329        // smoelius: Rather than re-execute the command, just debug print the messages.
330        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    // smoelius: A test executable's --list output ends with an empty line followed by
466    // "M tests, N benchmarks." Stop at the empty line.
467    // smoelius: Searching for the empty line is not necessary: https://stackoverflow.com/a/64913357
468    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        // smoelius: Disable dependency-binary-compatibility check when binary is a prerelease.
611        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        // smoelius: `n_children` - 1 lines for dividers plus one line at the bottom of the
986        // terminal to hold the cursor.
987        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            // width <= prefix.len()
1038            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    // Track failed targets to detect when all targets fail
1101    let mut failed_targets = std::collections::HashSet::new();
1102
1103    loop {
1104        Child::refresh(opts, n_children, children.as_mut_slice());
1105
1106        // If all targets have failed, terminate gracefully
1107        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            // Skip targets that have already failed
1119            if failed_targets.contains(&i_target) {
1120                i_task += 1;
1121                continue;
1122            }
1123
1124            // smoelius: Here is how I think this condition could arise. Suppose there are three
1125            // targets and two cpus, and that tasks 0 and 1 are currently running. Suppose then that
1126            // task 1 completes and task 2 cannot be started for some reason, so cargo-test-fuzz
1127            // tries to start task 3. Note that tasks 0 and 3 correspond to the same target. So if
1128            // task 0 is still running, `children[i_target]` will be `Some(..)`.
1129            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            // smoelius: If this is not the target's first run, then there must be insufficient
1138            // cpus.
1139            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    // smoelius: Passing `-c-` disables the cmplog fork server. Use of cmplog with a target that
1280    // spawns subprocesses (like libtest does) can leave orphan processes running. This can cause
1281    // problems, e.g., if those processes hold flocks. See:
1282    // https://github.com/AFLplusplus/AFLplusplus/issues/2178
1283    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    // smoelius: The only other tests in this file are the ones generated by the `test_fuzz` macro
1370    // for `filter_executable_targets`, and they are not affected by the change in directory.
1371    #[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        // smoelius: Any example executable should work _except_ the ones that use
1384        // `only_generic_args`. Currently, those are `generic` and `unserde`.
1385        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}