blob: 5349fc147f2ef4724bc2bd0f5d60ccfcaa200766 [file] [log] [blame]
#!/bin/bash
# Copyright 2018 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.
# Keep the usage info below in sync with //zircon/ulib/fuzz-utils/fuzzer.cpp!
### run a fuzz test on target a device
##
## Usage: fx fuzz [options] [command] [command-arguments]
##
## Options (must be first):
## -d, --device <name> Connect to device using Fuchsia link-local name.
## Must be specified if multiple devices are present.
## -f, --foreground Run in the foreground (default is background).
## -o, --output <dir> Use the given directory for saving output files.
## Defaults to the current directory.
## -s, --staging <dir> Use the given directory for staging temporary
## corpus files being transferred on or off of a
## target device. Defaults to a temporary directory
## that is removed on completion; use this options to
## preserve those temporary files on the host.
##
## Commands:
## help Prints this message and exits.
## list [name] Lists fuzzers matching 'name' if provided, or all
## fuzzers.
## fetch <name> [digest] Retrieves the corpus for the named fuzzer and
## version given by 'digest'. Defaults to the latest
## if 'digest' is omitted.
## start <name> [...] Starts the named fuzzer. Additional arguments are
## passed to the fuzzer. If the target does not have
## a corpus for the fuzzer, and the metadata lists one
## available, this will fetch the corpus first.
## check <name> Reports information about the named fuzzer, such as
## execution status, corpus location and size, and
## number of crashes.
## stop <name> Stops all instances of the named fuzzer.
## repro <name> [...] Runs the named fuzzer on specific inputs. If no
## additional inputs are provided, uses all previously
## found crashes.
## merge <name> [...] Merges the corpus for the named fuzzer. If no
## additional inputs are provided, minimizes the
## current corpus.
## store <name> Gathers the current corpus from the target platform
## and publishes it. Requires a pristine repository,
## as it will updates the build files with the new
## corpus location.
## zbi Adds Zircon fuzzers to 'fuchsia.zbi'
##
## The RECOMMENDED way to run a fuzzer is to omit 'command', which will use
## "automatic" mode. In this mode, 'fx fuzz' will check if a corpus is already
## present, and if not it will fetch the latest. It will then start the fuzzer
## and watch it to see when it stops. Each of these steps respects the options
## above.
##
## Example workflow:
## 1. Shows available fuzzers:
## fx fuzz list
##
## 2. Run a fuzzer for 8 hours (e.g. overnight), fetching the initial corpus
## if needed:
## fx fuzz -o out foo_package/bar_fuzz_test -max_total_time=28800
##
## 3. Check if the fuzzer is still running.
## fx fuzz check foo/bar
##
## 4. Execute the fuzzer with a crashing input:
## fx fuzz repro foo/bar crash-deadbeef
##
## 5. Use the artifacts in 'out/foo_package/bar_fuzz_test/latest' to file and
## fix bugs. Repeat step 4 until the target doesn't crash.
##
## 6. Repeat steps 2-4 until no crashes found.
##
## 7. Minimize the resulting corpus:
## fx fuzz merge foo/bar
##
## 8. Save the new, minimized corpus in CIPD:
## fx fuzz store foo/bar
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/vars.sh || exit $?
fx-config-read
# Constants
CIPD="${FUCHSIA_DIR}/buildtools/cipd"
CIPD_PREFIX="fuchsia/test_data/fuzzing"
# Global variables
device=""
fg=0
output="."
staging=""
keep=0
host=$(uname)
fuzzer=""
status=""
data_path=""
corpus_size="0"
cipd_path=""
# Utility functions
fatal() {
echo "Fatal error: $@"
exit 1
}
abspath() {
if [[ ${host} == "Darwin" ]] ; then
# Ugh, Mac OSX, why can't you have decent utilities?
if [[ -d "$1" ]] ; then
cd $1 && pwd -P
else
cd $(dirname $1) && echo "$(pwd -P)/$(basename $1)"
fi
else
realpath $1
fi
}
set_staging() {
if [[ -z "${staging}" ]] ; then
staging=$(mktemp -d)
[[ $? -eq 0 ]] || fatal "failed to make staging directory"
fi
}
# Ensure the temporary directory is removed as needed
cleanup() {
if [[ -n "${staging}" ]] && [[ ${keep} -eq 0 ]] ; then
rm -rf "${staging}"
fi
}
trap cleanup EXIT
# Commands
query() {
local tmp
tmp="$(fx-command-run ssh "${device}" fuzz check "$1" | tr -s '\n' ' ')"
[[ -n "${tmp}" ]] || fatal "Use 'fx fuzz list' to see available fuzzers."
[[ $? -eq 0 ]] || fatal "failed to query fuzzer"
tmp="$(echo "${tmp}" | tr -d ':' | cut -d' ' -f1,2,8,11)"
read fuzzer status data_path corpus_size <<< "${tmp}"
cipd_path="${CIPD_PREFIX}/${fuzzer}"
}
fetch() {
[[ "${status}" == "STOPPED" ]] || fatal "fuzzer must be stopped to run this command"
local version="${1:-latest}"
# "latest" is a ref, but digests are tags that are prefixed with "version:"
if [[ "${version}" != "latest" ]] ; then
version="version:${version}"
fi
local corpus="${staging}/${fuzzer}/corpus"
mkdir -p "${corpus}"
[[ $? -eq 0 ]] || fatal "failed to create local directory: ${corpus}"
# Get corpus from CIPD
if ${CIPD} ls -r "${CIPD_PREFIX}" | grep -q "${fuzzer}" ; then
${CIPD} install "${cipd_path}" ${version} --root "${corpus}"
[[ $? -eq 0 ]] || fatal "failed to retrieve corpus"
fi
# Add any upstream third-party corpora
if [[ "${version}" == "latest" ]] ; then
local seed_corpora=$(fx-command-run ssh ${device} fuzz seeds ${fuzzer})
for seed in ${seed_corpora} ; do
if echo "${seed}" | grep -q "^//third_party" ; then
rsync -a ${FUCHSIA_DIR}/${seed}/ ${corpus}/
fi
done
fi
# Fuchsia's scp doesn't like to glob
fx-command-run scp -r ${corpus} "[${device}]:${data_path}"
}
store() {
[[ "${status}" == "STOPPED" ]] || fatal "fuzzer must be stopped to run this command"
[[ "${corpus_size}" != "0" ]] || fatal "refusing to store empty corpus"
local corpus="${staging}/${fuzzer}/corpus"
mkdir -p "${corpus}"
[[ $? -eq 0 ]] || fatal "failed to create local directory: ${corpus}"
# Fuchsia's scp doesn't like to glob
fx-command-run scp -r "[${device}]:${data_path}corpus" $(dirname "${corpus}")
echo "***"
echo "This script may prompt for credentials."
echo "This is to allow it to add POSIX-style ACLs to corpus files."
echo "***"
sudo chmod +x ${corpus}
[[ $? -eq 0 ]] || fatal "failed to grant access"
pushd ${corpus}
local version
version=$(tar c * | sha256sum | cut -d' ' -f1)
[[ $? -eq 0 ]] || fatal "failed to calculate digest"
cat >cipd.yaml <<EOF
package: ${cipd_path}
description: Auto-generated fuzzing corpus for ${fuzzer}.
install_mode: copy
data:
$(ls -1 | grep -v cipd | sed 's/^/ - file: /')
EOF
# TODO: catch the error and tell user to do this
# $ cipd auth-login # One-time auth.
${CIPD} create --pkg-def cipd.yaml -tag version:${version} -ref latest
echo "***"
echo "Successfully stored package for ${fuzzer}, version ${version}."
# cipd creates a .cipd directory in corpus that misses +x so it cannot be
# cleaned up properly in cleanup().
sudo chmod -R +x ${corpus}
popd
}
start() {
# Get fuzzer info and check status
query "${fuzzer}"
[[ ${status} != "RUNNING" ]] || fatal "${fuzzer} is already running"
# Ensure we have a directory for this target
mkdir -p "${output}/${fuzzer}"
[[ $? -eq 0 ]] || fatal "failed to make directory: ${output}/${fuzzer}"
pushd "${output}/${fuzzer}" >/dev/null
# Clear all old logs
fx-command-run ssh ${device} rm "${data_path}/fuzz-*.log"
killall loglistener 2>/dev/null
# Create a directory for this run
local results="$(date +%F-%T)"
mkdir ${results}
[[ $? -eq 0 ]] || fatal "failed to make directory: ${results}"
rm -f latest
ln -s ${results} latest
pushd latest >/dev/null
# Start logging
${ZIRCON_TOOLS_DIR}/loglistener >zircon.log &
echo $! >.loglistener.pid
# Start the fuzzer
if [[ ${fg} -eq 0 ]] ; then
fx-command-run ssh ${device} fuzz start ${fuzzer} "$@" &
else
fx-command-run ssh ${device} fuzz start ${fuzzer} -jobs=0 "$@" 2>&1 | tee "fuzz-0.log"
fi
query "${fuzzer}"
if [[ ${status} == "RUNNING" ]] ; then
echo "'${fuzzer}' started; you should be notified when it stops."
echo "To check its progress, use 'fx fuzz check ${fuzzer}'."
echo "To stop it manually, use 'fx fuzz stop ${fuzzer}'."
elif [[ ${fg} -ne 0 ]] ; then
echo "Test units written to $(pwd -P)"
fi
monitor &
# Undo pushds
popd >/dev/null
popd >/dev/null
}
monitor() {
# Wait for completion
query "${fuzzer}"
while [[ "${status}" == "RUNNING" ]] ; do
sleep 2
query "${fuzzer}"
done
if [[ ${fg} -eq 0 ]] ; then
fx-command-run scp "[${device}]:${data_path}/fuzz-*.log" .
fi
# Stop log collection and symbolize
if [[ -f .loglistener.pid ]] ; then
kill $(cat .loglistener.pid)
rm -f .loglistener.pid
fi
if [[ -f zircon.log ]] ; then
${FUCHSIA_DIR}/zircon/scripts/symbolize \
-i ${FUCHSIA_BUILD_DIR}/ids.txt <zircon.log >symbolized.log
fi
# Transfer the fuzz logs
local units=0
for log in * ; do
for unit in $(grep 'Test unit written to ' ${log} | sed 's/.* //') ; do
fx-command-run scp "[${device}]:${unit}" .
units=$((${units} + 1))
done
done
# Notify user
local title="${fuzzer} has stopped"
local body="${units} test units written to $(pwd -P)"
if [[ ${host} == "Linux" ]] ; then
if [[ -x /usr/bin/notify-send ]] ; then
/usr/bin/notify-send "${title}." "${body}"
else
wall "${title}; ${body}"
fi
elif [[ ${host} == "Darwin" ]] ; then
osascript -e "display notification \"${body}\" with title \"${title}.\""
fi
}
add_to_zbi() {
local image="${FUCHSIA_BUILD_DIR}/fuchsia.zbi"
if [[ ! -f "${image}" ]] ; then
fatal "No such ZBI file: ${image}"
elif [[ -f "${image}.orig" ]] ; then
fatal "Cowardly refusing to overwrite existing ${image}.orig"
fi
# Build zircon with instrumentation
echo "Building Zircon fuzzers..."
pushd "${FUCHSIA_DIR}/zircon"
USE_ASAN=1 USE_SANCOV=1 scripts/build-zircon-${FUCHSIA_ARCH} -C
# Find the lines in the bootfs manifest that are relevant to fuzzing, and create a new fuzz
# manifest that we can use to inject this objects into a ZBI.
local bootfs_manifest="build-${FUCHSIA_ARCH}-asan/bootfs.manifest"
local fuzz_manifest="build-${FUCHSIA_ARCH}-asan/fuzz.manifest"
grep '{core}lib.*asan.*=' "${bootfs_manifest}" > "${fuzz_manifest}"
grep '^{libs}lib/asan' "${bootfs_manifest}" >> "${fuzz_manifest}"
grep '^{test}test/fuzz' "${bootfs_manifest}" >> "${fuzz_manifest}"
# Check that all fuzzers listed in the zircon_fuzzers package are present in the build
# The `targets` regex looks at the resources under the data/ directory to find the fuzz target
# name, i.e. "data/some_fuzz_target/corpora" => "some_fuzz_target".
local zircon_manifest="${FUCHSIA_BUILD_DIR}/obj/garnet/tests/zircon/zircon_fuzzers.manifest"
local targets="$(sed -n 's/^data\/\([^\/]*\)\/.*/\1/p' "${zircon_manifest}" | sort | uniq )"
for target in $targets ; do
grep -q "$target" "${fuzz_manifest}" || \
fatal "target not found in ${fuzz_manifest}: ${target}"
done
# Copy fuzzers into Fuchsia
mv "${image}" "${image}.orig"
if ! ${ZIRCON_TOOLS_DIR}/zbi -o "${image}" "${image}.orig" "${fuzz_manifest}" ; then
mv "${image}.orig" "${image}"
fatal "Could not create ${image}"
fi
popd
echo "Zircon fuzzers added to ${image}"
rm -f "${image}.orig"
}
# Main
main() {
fx-config-read
# Parse options
while [[ "$1" == "-"* ]] ; do
local opt="$1"
shift
local has_optval
case "${opt}" in
-f|--foreground)
fg=1
has_optval=0
;;
-o|--output)
output="$1"
has_optval=1
;;
-s|--staging)
keep=1
staging="$1"
has_optval=1
;;
*)
fatal "unknown option: ${opt}"
;;
esac
if [[ ${has_optval} -ne 0 ]] ; then
if [[ -z "$1" ]] || [[ "$1" == "-"* ]] ; then
fatal "missing value for ${opt}"
fi
shift
fi
done
output=$(abspath "${output}")
# Parse command
local device="$(get-fuchsia-device-addr)"
local command=$1
local fuzzer=$2
local args="${@:3}"
case ${command} in
help)
fx-command-help
exit 0
;;
list|check|stop|repro|merge)
fx-command-run ssh ${device} fuzz ${command} ${fuzzer} ${args}
;;
start)
start "${args}"
;;
fetch|store)
set_staging
query "${fuzzer}"
${command} ${args}
;;
zbi)
# TODO(security): SEC-141. This command should be replaced by something using //build/images
# once vanilla drivers in instrumented devhosts are fixed and/or partial Zircon
# instrumentation is implemented.
echo "NOTE: This command is subject to change. Check the documentation at"
echo "//docs/development/workflows/libfuzzer.md for the currently supported way of"
echo "running Zircon fuzzers in a Fuchsia environment."
add_to_zbi
;;
*)
# "Automatic" mode
fuzzer="${command}"
args="${@:2}"
echo "Command omitted; starting fuzzer '${fuzzer}' in automatic mode."
echo "If this isn't what you intended, try 'fx fuzz help'."
set_staging
query "${fuzzer}"
if [[ ${corpus_size} == "0" ]] ; then
fetch
fi
start "${args}"
;;
esac
}
main "$@"