| #!/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 "$@" |