blob: 3c39512229a387fd5fd144978032d9bf297f343a [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{bail, Context as _, Error, Result};
use diagnostics_data::LogsData;
use fidl::endpoints::{create_proxy, create_request_stream};
use fidl_fuchsia_fuzzer::{self as fuzz, Result_ as FuzzResult};
use fuchsia_component::client::connect_to_protocol;
use futures::{join, AsyncReadExt, AsyncWriteExt, TryStreamExt};
use serde_json::Deserializer;
use std::cell::RefCell;
use std::rc::Rc;
use {fuchsia_async as fasync, fuchsia_zircon as zx};
const FUZZER_URL: &str = "fuchsia-pkg://fuchsia.com/fuzz-test-runner-tests#meta/fuzzer.cm";
const BUF_SIZE: u32 = 32768;
// Connects to the fuzz-manager, and then connects a controller to the fuzzer.
async fn setup() -> (fuzz::ManagerProxy, fuzz::ControllerProxy, fasync::Task<()>) {
let fuzz_manager =
connect_to_protocol::<fuzz::ManagerMarker>().expect("failed to connect fuzz-manager");
let (controller, log_task) = connect(&fuzz_manager).await;
let options = fuzz::Options { seed: Some(1), ..Default::default() };
let response = controller
.configure(&options)
.await
.unwrap_or_else(|e| panic!("{}: {:?}", controller_name("Configure"), e));
assert!(response.is_ok());
(fuzz_manager, controller, log_task)
}
// Connects a controller to the fuzzer using the fuzz-manager.
async fn connect(fuzz_manager: &fuzz::ManagerProxy) -> (fuzz::ControllerProxy, fasync::Task<()>) {
let (controller, server_end) =
create_proxy::<fuzz::ControllerMarker>().expect("failed to create proxy");
let status = fuzz_manager
.connect(FUZZER_URL, server_end)
.await
.unwrap_or_else(|e| panic!("{}: {:?}", manager_name("Connect"), e));
assert_eq!(status, Ok(()));
// Forward syslog. This isn't strictly necessary for testing, but is very useful when debugging.
let (rx, tx) = zx::Socket::create_stream();
let status = fuzz_manager
.get_output(FUZZER_URL, fuzz::TestOutput::Syslog, tx)
.await
.unwrap_or_else(|e| panic!("{}: {:?}", manager_name("GetOutput"), e));
assert_eq!(status, Ok(()));
let log_task = fasync::Task::spawn(async move {
let mut socket = fasync::Socket::from_socket(rx);
let mut buf: [u8; BUF_SIZE as usize] = [0; BUF_SIZE as usize];
let mut logs = Vec::new();
loop {
let len = socket.read(&mut buf).await.expect("failed to read syslog socket");
if len == 0 {
break;
}
let line = std::str::from_utf8(&buf[..len]).expect("failed to convert syslog to UTF-8");
let deserializer = Deserializer::from_str(line);
let iter = deserializer.into_iter::<LogsData>();
for deserialized in iter {
let data = match deserialized {
Ok(data) => data,
Err(_) => continue,
};
let msg = data.msg().unwrap_or("<missing>");
logs.push(format!("[{}] {}", data.metadata.severity, msg));
}
}
eprintln!("{}", logs.join("\n"));
});
(controller, log_task)
}
// Constructs a `fuzz::Input` that can be sent over FIDL, and the socket to write into it.
fn make_fidl_input(input: &str) -> Result<(fasync::Socket, fuzz::Input)> {
let (rx, tx) = zx::Socket::create_stream();
let fidl_input = fuzz::Input { socket: rx, size: input.len() as u64 };
let tx = fasync::Socket::from_socket(tx);
Ok((tx, fidl_input))
}
// Receives data from the socket in the |input|.
async fn recv_input(input: fuzz::Input) -> Result<Vec<u8>> {
let mut buf = vec![0; input.size as usize];
let mut rx = fasync::Socket::from_socket(input.socket);
rx.read_exact(&mut buf).await.context("Async socket read failed")?;
Ok(buf)
}
// Gets an artifact after a long-running workflow.
async fn watch_artifact(
controller: &fuzz::ControllerProxy,
) -> Result<(FuzzResult, Option<Vec<u8>>)> {
let artifact = controller.watch_artifact().await.context(controller_name("WatchArtifact"))?;
if let Some(e) = artifact.error {
bail!("workflow returned error: {:?}", e)
}
let fuzz_result = artifact.result.unwrap_or(FuzzResult::NoErrors);
let input = match artifact.input {
Some(input) => {
let input = recv_input(input).await?;
Some(input)
}
None => None,
};
Ok((fuzz_result, input))
}
// Simple |fuchsia.fuzzer.CorpusReader| implementation.
async fn serve_corpus_reader(stream: fuzz::CorpusReaderRequestStream) -> Result<Vec<String>> {
// let corpus: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let corpus = Rc::new(RefCell::new(Vec::new()));
stream
.try_for_each(|request| async {
// let corpus_for_request = corpus.clone();
match request {
fuzz::CorpusReaderRequest::Next { test_input, responder } => {
let status = match recv_input(test_input).await {
Ok(received) => {
// let mut corpus = corpus_for_request.lock().await;
let as_str = std::str::from_utf8(&received).unwrap().to_string();
if !as_str.is_empty() {
let mut corpus_mut = corpus.borrow_mut();
corpus_mut.push(as_str);
}
zx::Status::OK
}
Err(_) => zx::Status::INTERNAL,
};
responder.send(status.into_raw())
}
}
})
.await
.context("CorpusReader failed")?;
let corpus = corpus.borrow();
Ok(corpus.clone())
}
// Handles status updates published by the fuzzer.
async fn subscribe_to_updates(
stream: fuzz::MonitorRequestStream,
) -> Result<Vec<fuzz::UpdateReason>> {
let reasons = Rc::new(RefCell::new(Vec::new()));
stream
.try_for_each(|request| async {
match request {
fuzz::MonitorRequest::Update { reason, status: _, responder } => {
{
let mut reasons = reasons.borrow_mut();
reasons.push(reason);
}
responder.send()
}
}
})
.await
.expect("Monitor error");
let reasons = reasons.borrow();
Ok(reasons.clone())
}
// Helper function return a `Ok(_)` or `Err(_)` depending on whether the arguments are equal or not,
// respectively.
fn compare<T: PartialEq>(x: &T, y: T) -> Result<()> {
if x.ne(&y) {
bail!("not equal");
}
Ok(())
}
// Returns the name of the fuzz-manager FIDL method.
fn manager_name(method: &str) -> String {
format!("fuchsia.fuzzer.Manager/{}", method)
}
// Returns the name of the fuzzer controller FIDL method.
fn controller_name(method: &str) -> String {
format!("fuchsia.fuzzer.Controller/{}", method)
}
// Stops the fuzzer and dumps the log if the test resulted in an error.
async fn teardown(
fuzz_manager: fuzz::ManagerProxy,
test_result: Result<()>,
log_task: fasync::Task<()>,
) -> Result<()> {
let stop_result = stop(&fuzz_manager).await.map_err(Error::msg);
log_task.await;
test_result.and(stop_result)
}
// Stops the fuzzer.
async fn stop(fuzz_manager: &fuzz::ManagerProxy) -> Result<(), zx::Status> {
match fuzz_manager.stop(FUZZER_URL).await {
Ok(result) => result.map_err(|e| zx::Status::from_raw(e)),
Err(e) => {
eprintln!("fuchsia.fuzzer/Manager.Stop: {}", e);
Err(zx::Status::INTERNAL)
}
}
}
#[fuchsia::test]
async fn test_reconnect() -> Result<()> {
let fuzz_manager =
connect_to_protocol::<fuzz::ManagerMarker>().expect("failed to connect to fuzz-manager");
// Set an option.
let (controller1, log_task1) = connect(&fuzz_manager).await;
let test1 = || async move {
let options = fuzz::Options { dictionary_level: Some(13), ..Default::default() };
let response =
controller1.configure(&options).await.context(controller_name("Configure"))?;
response.map_err(Error::msg)
};
let test_result1 = test1().await;
// Now reconnect and use the option to verify the instance is the same.
let (controller2, log_task2) = connect(&fuzz_manager).await;
let test2 = || async move {
let options = controller2.get_options().await.context(controller_name("GetOptions"))?;
compare(&options.dictionary_level, Some(13))
};
let test_result2 = test2().await;
// Stop the fuzzer and join the logs.
let stop_result = stop(&fuzz_manager).await.map_err(Error::msg);
if let Err(e) = test_result1.and(test_result2).and(stop_result) {
let _ = join!(log_task1, log_task2);
return Err(e);
}
Ok(())
}
#[fuchsia::test]
async fn test_stop() -> Result<()> {
let (fuzz_manager, _controller, log_task) = setup().await;
// Stop.
stop(&fuzz_manager).await.map_err(Error::msg)?;
// Stop when already stopped.
let response = stop(&fuzz_manager).await;
assert_eq!(response, Err(zx::Status::NOT_FOUND));
log_task.await;
Ok(())
}
#[fuchsia::test]
async fn test_configure() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let options = fuzz::Options { max_input_size: Some(1024), ..Default::default() };
let response =
controller.configure(&options).await.context(controller_name("Configure"))?;
response.map_err(Error::msg)?;
let options = controller.get_options().await.context(controller_name("GetOptions"))?;
compare(&options.max_input_size, Some(1024))
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_fuzz_until_crash() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let response = controller.fuzz().await.context(controller_name("Fuzz"))?;
response.map_err(Error::msg)?;
let (fuzz_result, input) = watch_artifact(&controller).await?;
compare(&fuzz_result, FuzzResult::Crash)?;
compare(&input, Some(b"CRASH".to_vec()))
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_fuzz_until_runs() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let options = fuzz::Options {
seed: Some(1),
runs: Some(100),
max_input_size: Some(4),
..Default::default()
};
let response =
controller.configure(&options).await.context(controller_name("Configure"))?;
response.map_err(Error::msg)?;
let (client_end, stream) = create_request_stream::<fuzz::MonitorMarker>()?;
controller.add_monitor(client_end).await.context(controller_name("AddMonitor"))?;
let results = join!(controller.fuzz(), subscribe_to_updates(stream));
let response = results.0.context(controller_name("Fuzz"))?;
response.map_err(Error::msg)?;
let (fuzz_result, _input) = watch_artifact(&controller).await?;
compare(&fuzz_result, FuzzResult::NoErrors)?;
let reasons = results.1.context("failed to get updates")?;
compare(&reasons.first(), Some(&fuzz::UpdateReason::Init))?;
compare(&reasons.last(), Some(&fuzz::UpdateReason::Done))
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_try_one_no_errors() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let input = "NoErrors";
let (mut tx, fidl_input) = make_fidl_input(input).context("failed to make FIDL input")?;
let results = join!(tx.write_all(input.as_bytes()), controller.try_one(fidl_input));
results.0.context("failed to send input")?;
let response = results.1.context(controller_name("TryOne"))?;
response.map_err(Error::msg)?;
let (fuzz_result, _input) = watch_artifact(&controller).await?;
compare(&fuzz_result, FuzzResult::NoErrors)
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_try_one_crash() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let input = "CRASH";
let (mut tx, fidl_input) = make_fidl_input(input).context("failed to make FIDL input")?;
let results = join!(tx.write_all(input.as_bytes()), controller.try_one(fidl_input));
results.0.context("failed to send input")?;
let response = results.1.context(controller_name("TryOne"))?;
response.map_err(Error::msg)?;
let (fuzz_result, _input) = watch_artifact(&controller).await?;
compare(&fuzz_result, FuzzResult::Crash)
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_minimize() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let input = "THIS CRASH CAN BE MINIMIZED";
let (mut tx, fidl_input) = make_fidl_input(input).context("failed to make FIDL input")?;
let results = join!(tx.write_all(input.as_bytes()), controller.minimize(fidl_input));
results.0.context("failed to send input")?;
let response = results.1.context("FIDL failure")?;
response.map_err(Error::msg)?;
let (fuzz_result, input) = watch_artifact(&controller).await?;
compare(&fuzz_result, FuzzResult::Minimized)?;
compare(&input, Some(b"CRASH".to_vec()))
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_cleanse() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let input = "AAAACRASHAAAA";
let (mut tx, fidl_input) = make_fidl_input(input).context("failed to make FIDL input")?;
let results = join!(tx.write_all(input.as_bytes()), controller.cleanse(fidl_input));
results.0.context("failed to send input")?;
let response = results.1.context("FIDL failure")?;
response.map_err(Error::msg)?;
let (fuzz_result, input) = watch_artifact(&controller).await?;
compare(&fuzz_result, FuzzResult::Cleansed)?;
compare(&input, Some(b" CRASH ".to_vec()))
};
teardown(fuzz_manager, test().await, log_task).await
}
#[fuchsia::test]
async fn test_merge() -> Result<()> {
let (fuzz_manager, controller, log_task) = setup().await;
let test = || async move {
let seed_inputs = vec!["C", "AS"];
for input in seed_inputs.iter() {
let (mut tx, fidl_input) =
make_fidl_input(input).context("failed to make FIDL input")?;
let results = join!(
tx.write_all(input.as_bytes()),
controller.add_to_corpus(fuzz::Corpus::Seed, fidl_input)
);
results.0.context("failed to send input")?;
let result = results.1.context(controller_name("AddToCorpus"))?;
result.map_err(Error::msg)?;
}
let live_inputs = vec!["CR", "CRY", "CASH", "CRASS", "CRAM", "CRAMS", "SCRAM"];
for input in live_inputs {
let (mut tx, fidl_input) =
make_fidl_input(input).context("failed to make FIDL input")?;
let results = join!(
tx.write_all(input.as_bytes()),
controller.add_to_corpus(fuzz::Corpus::Live, fidl_input)
);
results.0.context("failed to send input")?;
let result = results.1.context(controller_name("AddToCorpus"))?;
result.map_err(Error::msg)?;
}
let response = controller.merge().await.context(controller_name("Merge"))?;
response.map_err(Error::msg)?;
// Get the seed corpus.
let (client_end, stream) = create_request_stream::<fuzz::CorpusReaderMarker>()?;
let results = join!(
controller.read_corpus(fuzz::Corpus::Seed, client_end),
serve_corpus_reader(stream),
);
results.0.context(controller_name("ReadCorpus (seed)"))?;
let actual_seed = results.1.context("failed to serve seed corpus reader")?;
compare(&actual_seed, seed_inputs.iter().map(|s| s.to_string()).collect())?;
// Get the live corpus.
let (client_end, stream) = create_request_stream::<fuzz::CorpusReaderMarker>()?;
let results = join!(
controller.read_corpus(fuzz::Corpus::Live, client_end),
serve_corpus_reader(stream),
);
results.0.context(controller_name("ReadCorpus (live)"))?;
let actual_live = results.1.context("failed to serve live corpus reader")?;
// The fake runner "measures" inputs by setting the number of features to matching prefix
// length. The merge algorithm sorts inputs by shortest first, then most features, and it keeps
// any inputs that add new features.
compare(&actual_live, vec!["CR", "CRAM", "CRASS"].iter().map(|s| s.to_string()).collect())
};
teardown(fuzz_manager, test().await, log_task).await
}