1#!/bin/bash -u 2# Copyright 2018 Google Inc. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16################################################################################ 17cd $OUT 18 19if (( $# > 0 )); then 20 FUZZ_TARGETS="$@" 21else 22 FUZZ_TARGETS="$(find . -maxdepth 1 -type f -executable -printf '%P\n' | \ 23 grep -v -x -F \ 24 -e 'llvm-symbolizer' \ 25 -e 'jazzer_agent_deploy.jar' \ 26 -e 'jazzer_driver' \ 27 -e 'jazzer_driver_with_sanitizer')" 28fi 29 30COVERAGE_OUTPUT_DIR=${COVERAGE_OUTPUT_DIR:-$OUT} 31 32DUMPS_DIR="$COVERAGE_OUTPUT_DIR/dumps" 33FUZZER_STATS_DIR="$COVERAGE_OUTPUT_DIR/fuzzer_stats" 34LOGS_DIR="$COVERAGE_OUTPUT_DIR/logs" 35REPORT_ROOT_DIR="$COVERAGE_OUTPUT_DIR/report" 36REPORT_PLATFORM_DIR="$COVERAGE_OUTPUT_DIR/report/linux" 37 38for directory in $DUMPS_DIR $FUZZER_STATS_DIR $LOGS_DIR $REPORT_ROOT_DIR \ 39 $REPORT_PLATFORM_DIR; do 40 rm -rf $directory 41 mkdir -p $directory 42done 43 44PROFILE_FILE="$DUMPS_DIR/merged.profdata" 45SUMMARY_FILE="$REPORT_PLATFORM_DIR/summary.json" 46 47# Use path mapping, as $SRC directory from the builder is copied into $OUT/$SRC. 48PATH_EQUIVALENCE_ARGS="-path-equivalence=/,$OUT" 49 50# It's important to use $COVERAGE_EXTRA_ARGS as the last argument, because it 51# can contain paths to source files / directories which are positional args. 52LLVM_COV_COMMON_ARGS="$PATH_EQUIVALENCE_ARGS \ 53 -ignore-filename-regex=.*src/libfuzzer/.* $COVERAGE_EXTRA_ARGS" 54 55# Timeout for running a single fuzz target. 56TIMEOUT=1h 57 58# This will be used by llvm-cov command to generate the actual report. 59objects="" 60 61# Number of CPUs available, this is needed for running tests in parallel. 62NPROC=$(nproc) 63 64CORPUS_DIR=${CORPUS_DIR:-"/corpus"} 65 66function run_fuzz_target { 67 local target=$1 68 69 # '%1m' will produce separate dump files for every object. For example, if a 70 # fuzz target loads a shared library, we will have dumps for both of them. 71 local profraw_file="$DUMPS_DIR/$target.%1m.profraw" 72 local profraw_file_mask="$DUMPS_DIR/$target.*.profraw" 73 local profdata_file="$DUMPS_DIR/$target.profdata" 74 local corpus_real="$CORPUS_DIR/${target}" 75 76 # -merge=1 requires an output directory, create a new, empty dir for that. 77 local corpus_dummy="$OUT/dummy_corpus_dir_for_${target}" 78 rm -rf $corpus_dummy && mkdir -p $corpus_dummy 79 80 # Use -merge=1 instead of -runs=0 because merge is crash resistant and would 81 # let to get coverage using all corpus files even if there are crash inputs. 82 # Merge should not introduce any significant overhead compared to -runs=0, 83 # because (A) corpuses are already minimized; (B) we do not use sancov, and so 84 # libFuzzer always finishes merge with an empty output dir. 85 # Use 100s timeout instead of 25s as code coverage builds can be very slow. 86 local args="-merge=1 -timeout=100 $corpus_dummy $corpus_real" 87 88 export LLVM_PROFILE_FILE=$profraw_file 89 timeout $TIMEOUT $OUT/$target $args &> $LOGS_DIR/$target.log 90 if (( $? != 0 )); then 91 echo "Error occured while running $target:" 92 cat $LOGS_DIR/$target.log 93 fi 94 95 rm -rf $corpus_dummy 96 97 if (( $(du -c $profraw_file_mask | tail -n 1 | cut -f 1) == 0 )); then 98 # Skip fuzz targets that failed to produce profile dumps. 99 return 0 100 fi 101 102 # If necessary translate to latest profraw version. 103 profraw_update.py $OUT/$target $profraw_file_mask tmp.profraw 104 mv tmp.profraw $profraw_file_mask 105 llvm-profdata merge -j=1 -sparse $profraw_file_mask -o $profdata_file 106 107 # Delete unnecessary and (potentially) large .profraw files. 108 rm $profraw_file_mask 109 110 shared_libraries=$(coverage_helper shared_libs -build-dir=$OUT -object=$target) 111 112 llvm-cov export -summary-only -instr-profile=$profdata_file -object=$target \ 113 $shared_libraries $LLVM_COV_COMMON_ARGS > $FUZZER_STATS_DIR/$target.json 114 115 if [ -n "${FULL_SUMMARY_PER_TARGET-}" ]; then 116 # This is needed for dataflow strategy analysis, can be removed later. See 117 # - https://github.com/google/oss-fuzz/pull/3306 118 # - https://github.com/google/oss-fuzz/issues/1632 119 # Intentionally writing these to the logs dir in order to hide the dumps 120 # from the ClusterFuzz cron job. 121 llvm-cov export -instr-profile=$profdata_file -object=$target \ 122 $shared_libraries $LLVM_COV_COMMON_ARGS > $LOGS_DIR/$target.json 123 fi 124} 125 126function run_go_fuzz_target { 127 local target=$1 128 129 echo "Running go target $target" 130 export FUZZ_CORPUS_DIR="$CORPUS_DIR/${target}/" 131 export FUZZ_PROFILE_NAME="$DUMPS_DIR/$target.perf" 132 $OUT/$target -test.coverprofile $DUMPS_DIR/$target.profdata &> $LOGS_DIR/$target.log 133 # translate from golangish paths to current absolute paths 134 cat $OUT/$target.gocovpath | while read i; do sed -i $i $DUMPS_DIR/$target.profdata; done 135 # cf PATH_EQUIVALENCE_ARGS 136 sed -i 's=/='$OUT'/=' $DUMPS_DIR/$target.profdata 137 $SYSGOPATH/bin/gocovsum $DUMPS_DIR/$target.profdata > $FUZZER_STATS_DIR/$target.json 138} 139 140function run_java_fuzz_target { 141 local target=$1 142 143 local exec_file="$DUMPS_DIR/$target.exec" 144 local class_dump_dir="$DUMPS_DIR/${target}_classes/" 145 mkdir "$class_dump_dir" 146 local corpus_real="$CORPUS_DIR/${target}" 147 148 # -merge=1 requires an output directory, create a new, empty dir for that. 149 local corpus_dummy="$OUT/dummy_corpus_dir_for_${target}" 150 rm -rf $corpus_dummy && mkdir -p $corpus_dummy 151 152 # Use 100s timeout instead of 25s as code coverage builds can be very slow. 153 local jacoco_args="destfile=$exec_file,classdumpdir=$class_dump_dir,excludes=com.code_intelligence.jazzer.*" 154 local args="-merge=1 -timeout=100 --nohooks \ 155 --additional_jvm_args=-javaagent:/opt/jacoco-agent.jar=$jacoco_args \ 156 $corpus_dummy $corpus_real" 157 158 timeout $TIMEOUT $OUT/$target $args &> $LOGS_DIR/$target.log 159 if (( $? != 0 )); then 160 echo "Error occured while running $target:" 161 cat $LOGS_DIR/$target.log 162 fi 163 164 if (( $(du -c $exec_file | tail -n 1 | cut -f 1) == 0 )); then 165 # Skip fuzz targets that failed to produce .exec files. 166 return 0 167 fi 168 169 # Generate XML report only as input to jacoco_report_converter. 170 # Source files are not needed for the summary. 171 local xml_report="$DUMPS_DIR/${target}.xml" 172 local summary_file="$FUZZER_STATS_DIR/$target.json" 173 java -jar /opt/jacoco-cli.jar report $exec_file \ 174 --xml $xml_report \ 175 --classfiles $class_dump_dir 176 177 # Write llvm-cov summary file. 178 jacoco_report_converter.py $xml_report $summary_file 179} 180 181export SYSGOPATH=$GOPATH 182export GOPATH=$OUT/$GOPATH 183# Run each fuzz target, generate raw coverage dumps. 184for fuzz_target in $FUZZ_TARGETS; do 185 # Test if fuzz target is a golang one. 186 if [[ $FUZZING_LANGUAGE == "go" ]]; then 187 # Continue if not a fuzz target. 188 if [[ $FUZZING_ENGINE != "none" ]]; then 189 grep "FUZZ_CORPUS_DIR" $fuzz_target > /dev/null 2>&1 || continue 190 fi 191 run_go_fuzz_target $fuzz_target & 192 elif [[ $FUZZING_LANGUAGE == "jvm" ]]; then 193 # Continue if not a fuzz target. 194 if [[ $FUZZING_ENGINE != "none" ]]; then 195 grep "LLVMFuzzerTestOneInput" $fuzz_target > /dev/null 2>&1 || continue 196 fi 197 198 echo "Running $fuzz_target" 199 run_java_fuzz_target $fuzz_target & 200 else 201 # Continue if not a fuzz target. 202 if [[ $FUZZING_ENGINE != "none" ]]; then 203 grep "LLVMFuzzerTestOneInput" $fuzz_target > /dev/null 2>&1 || continue 204 fi 205 206 echo "Running $fuzz_target" 207 run_fuzz_target $fuzz_target & 208 209 if [[ -z $objects ]]; then 210 # The first object needs to be passed without -object= flag. 211 objects="$fuzz_target" 212 else 213 objects="$objects -object=$fuzz_target" 214 fi 215 fi 216 217 # Do not spawn more processes than the number of CPUs available. 218 n_child_proc=$(jobs -rp | wc -l) 219 while [ "$n_child_proc" -eq "$NPROC" ]; do 220 sleep 4 221 n_child_proc=$(jobs -rp | wc -l) 222 done 223done 224 225# Wait for background processes to finish. 226wait 227 228if [[ $FUZZING_LANGUAGE == "go" ]]; then 229 $SYSGOPATH/bin/gocovmerge $DUMPS_DIR/*.profdata > fuzz.cov 230 go tool cover -html=fuzz.cov -o $REPORT_ROOT_DIR/index.html 231 $SYSGOPATH/bin/gocovsum fuzz.cov > $SUMMARY_FILE 232 cp $REPORT_ROOT_DIR/index.html $REPORT_PLATFORM_DIR/index.html 233 $SYSGOPATH/bin/pprof-merge $DUMPS_DIR/*.perf.cpu.prof 234 mv merged.data $REPORT_ROOT_DIR/cpu.prof 235 $SYSGOPATH/bin/pprof-merge $DUMPS_DIR/*.perf.heap.prof 236 mv merged.data $REPORT_ROOT_DIR/heap.prof 237 #TODO some proxy for go tool pprof -http=127.0.0.1:8001 $DUMPS_DIR/cpu.prof 238 echo "Finished generating code coverage report for Go fuzz targets." 239elif [[ $FUZZING_LANGUAGE == "jvm" ]]; then 240 241 # From this point on the script does not tolerate any errors. 242 set -e 243 244 # Merge .exec files from the individual targets. 245 jacoco_merged_exec=$DUMPS_DIR/jacoco.merged.exec 246 java -jar /opt/jacoco-cli.jar merge $DUMPS_DIR/*.exec \ 247 --destfile $jacoco_merged_exec 248 249 # Merge .class files from the individual targets. 250 classes_dir=$DUMPS_DIR/classes 251 mkdir $classes_dir 252 for fuzz_target in $FUZZ_TARGETS; do 253 cp -r $DUMPS_DIR/${fuzz_target}_classes/* $classes_dir/ 254 done 255 256 # Heuristically determine source directories based on Maven structure. 257 # Always include the $SRC root as it likely contains the fuzzer sources. 258 sourcefiles_args=(--sourcefiles $OUT/$SRC) 259 source_dirs=$(find $OUT/$SRC -type d -name 'java') 260 for source_dir in $source_dirs; do 261 sourcefiles_args+=(--sourcefiles "$source_dir") 262 done 263 264 # Generate HTML and XML reports. 265 xml_report=$REPORT_PLATFORM_DIR/index.xml 266 java -jar /opt/jacoco-cli.jar report $jacoco_merged_exec \ 267 --html $REPORT_PLATFORM_DIR \ 268 --xml $xml_report \ 269 --classfiles $classes_dir \ 270 "${sourcefiles_args[@]}" 271 272 # Write llvm-cov summary file. 273 jacoco_report_converter.py $xml_report $SUMMARY_FILE 274 275 set +e 276else 277 278 # From this point on the script does not tolerate any errors. 279 set -e 280 281 # Merge all dumps from the individual targets. 282 rm -f $PROFILE_FILE 283 llvm-profdata merge -sparse $DUMPS_DIR/*.profdata -o $PROFILE_FILE 284 285 # TODO(mmoroz): add script from Chromium for rendering directory view reports. 286 # The first path in $objects does not have -object= prefix (llvm-cov format). 287 shared_libraries=$(coverage_helper shared_libs -build-dir=$OUT -object=$objects) 288 objects="$objects $shared_libraries" 289 290 # It's important to use $LLVM_COV_COMMON_ARGS as the last argument due to 291 # positional arguments (SOURCES) that can be passed via $COVERAGE_EXTRA_ARGS. 292 LLVM_COV_ARGS="-instr-profile=$PROFILE_FILE $objects $LLVM_COV_COMMON_ARGS" 293 294 # Generate HTML report. 295 llvm-cov show -format=html -output-dir=$REPORT_ROOT_DIR \ 296 -Xdemangler rcfilt $LLVM_COV_ARGS 297 298 # Export coverage summary in JSON format. 299 llvm-cov export -summary-only $LLVM_COV_ARGS > $SUMMARY_FILE 300 301 # Post process HTML report. 302 coverage_helper -v post_process -src-root-dir=/ -summary-file=$SUMMARY_FILE \ 303 -output-dir=$REPORT_ROOT_DIR $PATH_EQUIVALENCE_ARGS 304 305fi 306 307# Make sure report is readable. 308chmod -R +r $REPORT_ROOT_DIR 309find $REPORT_ROOT_DIR -type d -exec chmod +x {} + 310 311if [[ -n $HTTP_PORT ]]; then 312 # Serve the report locally. 313 echo "Serving the report on http://127.0.0.1:$HTTP_PORT/linux/index.html" 314 cd $REPORT_ROOT_DIR 315 python3 -m http.server $HTTP_PORT 316fi 317