| #!/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 |
| ## 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. |
| ## -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]}")" && pwd)"/lib/vars.sh |
| fx-config-read |
| |
| # Constants |
| CIPD="${FUCHSIA_DIR}/buildtools/cipd" |
| |
| # Global variables |
| device="" |
| output="." |
| staging="" |
| keep=0 |
| |
| host=$(uname) |
| fuzzer="" |
| status="" |
| data_path="" |
| corpus_size="0" |
| cipd_path="" |
| |
| # Utility functions |
| fatal() { |
| echo "Fatal error: $@" |
| echo "Try 'fx help fuzz'" |
| 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' ' ')" |
| [[ $? -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="fuchsia/test_data/fuzzing/${fuzzer}" |
| } |
| |
| record() { |
| if [[ -z "$1" ]] ; then |
| return |
| fi |
| query "$1" |
| |
| mkdir -p "${output}/${fuzzer}" |
| [[ $? -eq 0 ]] || fatal "failed to make directory: ${output}/${fuzzer}" |
| pushd "${output}/${fuzzer}" >/dev/null |
| |
| if [[ ${status} == "RUNNING" ]] && [[ ! -h active ]] ; then |
| local results="$(date +%F-%T)" |
| mkdir ${results} |
| [[ $? -eq 0 ]] || fatal "failed to make directory: ${results}" |
| ln -s ${results} active |
| pushd active >/dev/null |
| |
| elif [[ ${status} == "STOPPED" ]] && [[ -h active ]] ; then |
| rm -f latest |
| mv active latest |
| pushd latest >/dev/null |
| |
| else |
| # Already started, or already processed |
| return |
| fi |
| |
| # Copy log files. |
| i=0 |
| while [[ $i -ge 0 ]] ; do |
| local log="fuzz-${i}.log" |
| fx-command-run scp "[${device}]:${data_path}${log}" . 2>/dev/null |
| if [[ $? -eq 0 ]] ; then |
| i=$((${i}+1)) |
| else |
| i=-1 |
| fi |
| done |
| |
| if [[ ${status} == "RUNNING" ]] ; then |
| # Start log collection |
| ${ZIRCON_TOOLS_DIR}/loglistener >zircon.log & |
| echo $! >.loglistener.pid |
| else |
| # 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-filter \ |
| ${FUCHSIA_BUILD_DIR}/ids.txt <zircon.log >symbolized.log |
| fi |
| |
| # Transfer the fuzz logs |
| for log in fuzz-*.log ; do |
| if [[ -f ${log} ]] ; then |
| for unit in $(grep 'Test unit written to ' ${log} | sed 's/.* //') ; do |
| fx-command-run scp "[${device}]:${unit}" . |
| done |
| fx-command-run ssh ${device} rm "${data_path}${log}" |
| fi |
| done |
| |
| # Alert the user |
| echo "Results written to $(pwd -P)" |
| fi |
| popd >/dev/null |
| popd >/dev/null |
| } |
| |
| fetch() { |
| [[ "${status}" == "STOPPED" ]] || fatal "fuzzer must be stopped to run this command" |
| |
| local version="$1" |
| if [[ -z "${version}" ]] ; then |
| version="latest" |
| fi |
| |
| local corpus="${staging}/${fuzzer}/corpus" |
| mkdir -p "${corpus}" |
| [[ $? -eq 0 ]] || fatal "failed to create local directory: ${corpus}" |
| |
| ${CIPD} install ${cipd_path} ${version} --root ${corpus} |
| if [[ $? -ne 0 ]] && [[ "${version}" != "latest" ]] ; then |
| fatal "unable to find ${cipd_path}:${version}" |
| 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} |
| popd |
| } |
| |
| monitor() { |
| # Wait for completion |
| while [[ "${status}" == "RUNNING" ]] ; do |
| sleep 10 |
| query "${fuzzer}" |
| done |
| |
| # Notify user |
| local title="${fuzzer} has stopped" |
| local body=$(record "${fuzzer}") |
| if [[ -z "${body}" ]] ; then |
| body="No results produced." |
| fi |
| 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 |
| |
| # Determine parts relevant for fuzzing |
| local bootfs_manifest="build-${FUCHSIA_ARCH}-asan/bootfs.manifest" |
| local fuzz_manifest="build-${FUCHSIA_ARCH}-asan/fuzz.manifest" |
| egrep '({core}|{libs}|{test})(lib/asan/|test/fuzz/)' "${bootfs_manifest}" > "${fuzz_manifest}" |
| |
| # Copy fuzzers into Fuchsia |
| mv "${image}" "${image}.orig" |
| zbi -o "${image}" "${image}.orig" "${fuzz_manifest}" |
| popd |
| echo "Zircon fuzzers added to ${image}" |
| } |
| |
| # Main |
| main() { |
| fx-config-read |
| # Parse options |
| while [[ "$1" == "-"* ]] ; do |
| case $1 in |
| -o|--output) |
| output="$2" |
| ;; |
| -s|--staging) |
| keep=1 |
| staging="$2" |
| ;; |
| *) |
| fatal "unknown option: $1" |
| ;; |
| esac |
| if [[ -z "$2" ]] || [[ "$2" == "-"* ]] ; then |
| fatal "missing value for $1" |
| fi |
| shift |
| shift |
| 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} |
| record "${fuzzer}" |
| ;; |
| start) |
| fx-command-run ssh ${device} fuzz ${command} ${fuzzer} ${args} & |
| sleep 1 |
| record "${fuzzer}" |
| ;; |
| fetch|store) |
| set_staging |
| query "${fuzzer}" |
| ${command} ${args} |
| ;; |
| watch) |
| query "${fuzzer}" |
| monitor & |
| ;; |
| 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." |
| echo |
| add_to_zbi |
| ;; |
| *) |
| # "Automatic" mode |
| fuzzer="${command}" |
| args="${@:2}" |
| echo "Command omitted; starting fuzzer '${fuzzer}' in automatic mode." |
| set_staging |
| query "${fuzzer}" |
| if [[ ${corpus_size} == "0" ]] ; then |
| fetch |
| fi |
| fx-command-run ssh ${device} fuzz start ${fuzzer} ${args} & |
| sleep 1 |
| record "${fuzzer}" |
| monitor & |
| sleep 1 |
| echo |
| echo "Fuzzer started; you should be notified when it stops." |
| echo "To check its progress, use 'fx fuzz check'." |
| ;; |
| esac |
| } |
| |
| main "$@" |