use super::{
CoverageCorpus, CoverageObservation, FuzzExecution, ReadyPreflight, RustcProfileRefusal,
RustcProfileResult, read_lcov,
};
use std::ffi::OsString;
use std::fs::{self, File};
use std::io::Read;
use std::process::{Child, Command, Stdio};
pub fn observe_rustc_profile(
ready: &ReadyPreflight,
corpus: &mut CoverageCorpus,
candidate: &[u8],
supervise: impl FnOnce(&mut Child) -> Result<FuzzExecution, String>,
) -> Result<RustcProfileResult, RustcProfileRefusal> {
if candidate.is_empty() {
return Err(RustcProfileRefusal::EmptyCandidate);
}
let case = corpus.reserve_execution(ready, candidate.len())?;
fs::create_dir_all(ready.scratch())
.map_err(|error| RustcProfileRefusal::CreateCase(error.to_string()))?;
let case_directory = ready.scratch().join(format!("case-{case:020}"));
match fs::create_dir(&case_directory) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
return Err(RustcProfileRefusal::CaseAlreadyExists(case_directory));
}
Err(error) => return Err(RustcProfileRefusal::CreateCase(error.to_string())),
}
let result = observe_case(ready, candidate, case, &case_directory, supervise);
match fs::remove_dir_all(&case_directory) {
Ok(()) => result,
Err(error) => Err(RustcProfileRefusal::CleanupCase {
after: result.err().map(Box::new),
cleanup: error.to_string(),
}),
}
}
fn observe_case(
ready: &ReadyPreflight,
candidate: &[u8],
case: u32,
case_directory: &std::path::Path,
supervise: impl FnOnce(&mut Child) -> Result<FuzzExecution, String>,
) -> Result<RustcProfileResult, RustcProfileRefusal> {
let raw = case_directory.join("coverage.profraw");
let merged = case_directory.join("coverage.profdata");
let input_path = case_directory.join("candidate.bin");
fs::write(&input_path, candidate)
.map_err(|error| RustcProfileRefusal::WriteCandidate(error.to_string()))?;
let execution = run_target(ready, &input_path, &raw, supervise)?;
if !raw.is_file() {
if matches!(execution, FuzzExecution::Success) {
return Err(RustcProfileRefusal::MissingProfile);
}
return Ok(RustcProfileResult::established(
case,
candidate.to_vec(),
execution,
CoverageObservation::empty(),
ready.standing().clone(),
));
}
merge_profile(ready, &raw, &merged)?;
let observation = export_coverage(ready, &merged)?;
Ok(RustcProfileResult::established(
case,
candidate.to_vec(),
execution,
observation,
ready.standing().clone(),
))
}
fn run_target(
ready: &ReadyPreflight,
input_path: &std::path::Path,
raw: &std::path::Path,
supervise: impl FnOnce(&mut Child) -> Result<FuzzExecution, String>,
) -> Result<FuzzExecution, RustcProfileRefusal> {
let input = File::open(input_path)
.map_err(|error| RustcProfileRefusal::OpenCandidate(error.to_string()))?;
let child = Command::new(ready.target().executable())
.args(ready.target().arguments())
.env("LLVM_PROFILE_FILE", raw)
.stdin(Stdio::from(input))
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| RustcProfileRefusal::StartTarget(error.to_string()))?;
let mut process = TargetProcess::running(child);
let execution = match supervise(process.child_mut()) {
Ok(execution) => execution,
Err(error) => {
return process.refuse(RustcProfileRefusal::SuperviseTarget(error));
}
};
process.finish(execution)
}
fn merge_profile(
ready: &ReadyPreflight,
raw: &std::path::Path,
merged: &std::path::Path,
) -> Result<(), RustcProfileRefusal> {
let status = Command::new(ready.tools().profdata())
.arg("merge")
.arg("-sparse")
.arg(raw)
.arg("-o")
.arg(merged)
.status()
.map_err(|error| RustcProfileRefusal::StartProfdata(error.to_string()))?;
if status.success() {
Ok(())
} else {
Err(RustcProfileRefusal::ProfdataFailed(status.code()))
}
}
fn export_coverage(
ready: &ReadyPreflight,
merged: &std::path::Path,
) -> Result<CoverageObservation, RustcProfileRefusal> {
let mut profile_argument = OsString::from("-instr-profile=");
profile_argument.push(merged.as_os_str());
let child = Command::new(ready.tools().cov())
.arg("export")
.arg("-format=lcov")
.arg(profile_argument)
.arg(ready.target().executable())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|error| RustcProfileRefusal::StartCov(error.to_string()))?;
let output = CovProcess::running(child)
.bounded_output(ready.standing().campaign().budgets().export_bytes())?;
read_lcov(ready.source_root(), &output).map_err(RustcProfileRefusal::Coverage)
}
struct TargetProcess {
child: Child,
custody: ProcessCustody,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProcessCustody {
Running,
Reaped,
}
impl TargetProcess {
fn running(child: Child) -> Self {
Self {
child,
custody: ProcessCustody::Running,
}
}
fn child_mut(&mut self) -> &mut Child {
&mut self.child
}
fn finish(mut self, execution: FuzzExecution) -> Result<FuzzExecution, RustcProfileRefusal> {
let status = match self.child_mut().try_wait() {
Ok(status) => status,
Err(error) => {
return self.refuse(RustcProfileRefusal::InspectTarget(error.to_string()));
}
};
if status.is_none() {
return self.refuse(RustcProfileRefusal::SupervisorReturnedBeforeExit);
}
self.custody = ProcessCustody::Reaped;
Ok(execution)
}
fn refuse(
mut self,
refusal: RustcProfileRefusal,
) -> Result<FuzzExecution, RustcProfileRefusal> {
match self.terminate_and_reap() {
Ok(()) => Err(refusal),
Err(cleanup) => Err(RustcProfileRefusal::CleanupTarget {
after: Box::new(refusal),
cleanup,
}),
}
}
fn terminate_and_reap(&mut self) -> Result<(), String> {
if let Ok(Some(_status)) = self.child_mut().try_wait() {
self.custody = ProcessCustody::Reaped;
return Ok(());
}
self.child_mut()
.kill()
.map_err(|error| format!("target termination failed: {error}"))?;
self.child_mut()
.wait()
.map_err(|error| format!("target reap failed: {error}"))?;
self.custody = ProcessCustody::Reaped;
Ok(())
}
}
impl Drop for TargetProcess {
fn drop(&mut self) {
if matches!(self.custody, ProcessCustody::Running) {
let _cleanup = self.terminate_and_reap();
}
}
}
struct CovProcess {
child: Child,
custody: ProcessCustody,
}
impl CovProcess {
fn running(child: Child) -> Self {
Self {
child,
custody: ProcessCustody::Running,
}
}
fn bounded_output(mut self, bound: u64) -> Result<Vec<u8>, RustcProfileRefusal> {
let Some(stdout) = self.child.stdout.take() else {
return self.refuse(RustcProfileRefusal::ReadCov(
"coverage export stdout was not piped".to_owned(),
));
};
let mut output = Vec::new();
let mut bounded = stdout.take(bound.saturating_add(1));
if let Err(error) = bounded.read_to_end(&mut output) {
return self.refuse(RustcProfileRefusal::ReadCov(error.to_string()));
}
let observed_at_least = u64::try_from(output.len()).unwrap_or(u64::MAX);
if observed_at_least > bound {
return self.refuse(RustcProfileRefusal::CovOutputBudgetExhausted {
bound,
observed_at_least,
});
}
let status = match self.child.wait() {
Ok(status) => status,
Err(error) => {
return self.refuse(RustcProfileRefusal::WaitCov(error.to_string()));
}
};
self.custody = ProcessCustody::Reaped;
if status.success() {
Ok(output)
} else {
Err(RustcProfileRefusal::CovFailed(status.code()))
}
}
fn refuse(mut self, refusal: RustcProfileRefusal) -> Result<Vec<u8>, RustcProfileRefusal> {
match self.terminate_and_reap() {
Ok(()) => Err(refusal),
Err(cleanup) => Err(RustcProfileRefusal::CleanupCov {
after: Box::new(refusal),
cleanup,
}),
}
}
fn terminate_and_reap(&mut self) -> Result<(), String> {
if let Ok(Some(_status)) = self.child.try_wait() {
self.custody = ProcessCustody::Reaped;
return Ok(());
}
self.child
.kill()
.map_err(|error| format!("coverage export termination failed: {error}"))?;
self.child
.wait()
.map_err(|error| format!("coverage export reap failed: {error}"))?;
self.custody = ProcessCustody::Reaped;
Ok(())
}
}
impl Drop for CovProcess {
fn drop(&mut self) {
if matches!(self.custody, ProcessCustody::Running) {
let _cleanup = self.terminate_and_reap();
}
}
}