Skip to content

Commit b5afc6b

Browse files
author
Mina Farid
authored
Run fuzzing as a travis cron job (#1662)
* Added fuzzing script to run all fuzzing targets on cron jobs. * Added travis fuzzing script and `FUZZING_DURATION` to control fuzzing time. * Given a total fuzzing duration, select 1 target to run for half of the duration, and divide the remaining duration over the rest of targets equally.
1 parent 10cc4f2 commit b5afc6b

File tree

4 files changed

+179
-16
lines changed

4 files changed

+179
-16
lines changed

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ jobs:
178178
script:
179179
- ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD
180180

181+
# Run fuzz tests only on cron jobs.
182+
- stage: fuzz_test
183+
env:
184+
- PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild
185+
before_install:
186+
- ./scripts/if_cron.sh ./scripts/install_prereqs.sh
187+
script:
188+
- ./scripts/if_cron.sh ./scripts/fuzzing_ci.sh
189+
181190
# TODO(varconst): UBSan for CMake. UBSan failures are non-fatal by default,
182191
# need to make them fatal for the purposes of the test run.
183192

Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_FuzzTests_iOS.xcscheme

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
value = "$(FUZZING_TARGET)"
5757
isEnabled = "YES">
5858
</EnvironmentVariable>
59+
<EnvironmentVariable
60+
key = "FUZZING_DURATION"
61+
value = "$(FUZZING_DURATION)"
62+
isEnabled = "YES">
63+
</EnvironmentVariable>
5964
</EnvironmentVariables>
6065
<AdditionalOptions>
6166
</AdditionalOptions>

Firestore/Example/FuzzTests/FSTFuzzTestsPrincipal.mm

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,27 @@
1414
* limitations under the License.
1515
*/
1616

17-
#import <Foundation/NSObject.h>
18-
#include <string>
17+
#import <Foundation/Foundation.h>
1918

20-
#include "LibFuzzer/FuzzerDefs.h"
19+
#include <cstdlib>
20+
#include <string>
21+
#include <unordered_map>
22+
#include <vector>
2123

2224
#include "Firestore/Example/FuzzTests/FuzzingTargets/FSTFuzzTestFieldPath.h"
2325
#include "Firestore/Example/FuzzTests/FuzzingTargets/FSTFuzzTestSerializer.h"
24-
2526
#include "Firestore/core/src/firebase/firestore/util/log.h"
2627
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
28+
#include "LibFuzzer/FuzzerDefs.h"
29+
#include "absl/strings/str_join.h"
2730

2831
namespace {
2932

3033
using firebase::firestore::util::MakeString;
3134
namespace fuzzing = firebase::firestore::fuzzing;
3235

33-
// A list of targets to fuzz test. Should be kept in sync with the method
34-
// GetFuzzingTarget().
36+
// A list of targets to fuzz test. Should be kept in sync with
37+
// GetFuzzingTarget() fuzzing_target_names map object.
3538
enum class FuzzingTarget { kNone, kSerializer, kFieldPath };
3639

3740
// Directory to which crashing inputs are written. Must include the '/' at the
@@ -43,9 +46,14 @@
4346
// Default target is kNone if the environment variable is empty, not set, or
4447
// could not be interpreted. Should be kept in sync with FuzzingTarget.
4548
FuzzingTarget GetFuzzingTarget() {
46-
char *fuzzing_target_env = std::getenv("FUZZING_TARGET");
49+
std::unordered_map<std::string, FuzzingTarget> fuzzing_target_names;
50+
fuzzing_target_names["NONE"] = FuzzingTarget::kNone;
51+
fuzzing_target_names["SERIALIZER"] = FuzzingTarget::kSerializer;
52+
fuzzing_target_names["FIELDPATH"] = FuzzingTarget::kFieldPath;
53+
54+
const char *fuzzing_target_env = std::getenv("FUZZING_TARGET");
4755

48-
if (fuzzing_target_env == nullptr) {
56+
if (!fuzzing_target_env) {
4957
LOG_WARN("No value provided for FUZZING_TARGET environment variable.");
5058
return FuzzingTarget::kNone;
5159
}
@@ -56,19 +64,39 @@ FuzzingTarget GetFuzzingTarget() {
5664
LOG_WARN("No value provided for FUZZING_TARGET environment variable.");
5765
return FuzzingTarget::kNone;
5866
}
59-
if (fuzzing_target == "NONE") {
60-
return FuzzingTarget::kNone;
61-
}
62-
if (fuzzing_target == "SERIALIZER") {
63-
return FuzzingTarget::kSerializer;
67+
68+
// Return the value of the fuzzing_target key if it exists in the
69+
// fuzzing_target_names map.
70+
if (fuzzing_target_names.find(fuzzing_target) != fuzzing_target_names.end()) {
71+
return fuzzing_target_names[fuzzing_target];
6472
}
65-
if (fuzzing_target == "FIELDPATH") {
66-
return FuzzingTarget::kFieldPath;
73+
74+
// If the target is not found, print an error message with all available targets.
75+
// The targets must be enclosed in curly brackets and separated by spaces. This format
76+
// is needed by the script /firebase-ios-sdk/script/fuzzing_travis.sh, which parses
77+
// this message to retrieve a list of the available targets.
78+
std::vector<std::string> all_keys;
79+
for (const auto &kv : fuzzing_target_names) {
80+
all_keys.push_back(kv.first);
6781
}
68-
LOG_WARN("Invalid fuzzing target: %s", fuzzing_target);
82+
const std::string all_keys_str = absl::StrJoin(all_keys, " ");
83+
LOG_WARN("Invalid fuzzing target: %s. Available targets: { %s }.", fuzzing_target, all_keys_str);
6984
return FuzzingTarget::kNone;
7085
}
7186

87+
// Retrieves fuzzing duration from the FUZZING_DURATION environment variable.
88+
// Defaults to "0" if the environment variable is empty, which corresponds to
89+
// running indefinitely.
90+
std::string GetFuzzingDuration() {
91+
const char *fuzzing_duration_env = std::getenv("FUZZING_DURATION");
92+
93+
if (!fuzzing_duration_env) {
94+
return "0";
95+
}
96+
97+
return std::string{fuzzing_duration_env};
98+
}
99+
72100
// Simulates calling the main() function of libFuzzer (FuzzerMain.cpp).
73101
// Uses GetFuzzingTarget() to get the fuzzing target and sets libFuzzer's args
74102
// accordingly. It also calls an appropriate LLVMFuzzerTestOneInput-like method
@@ -121,13 +149,17 @@ int RunFuzzTestingMain() {
121149
// The directory in which libFuzzer writes crashing inputs.
122150
std::string prefix_arg = std::string("-artifact_prefix=") + MakeString(kCrashingInputsDirectory);
123151

152+
// Run fuzzing for the defined fuzzing duration.
153+
std::string time_arg = "-max_total_time=" + GetFuzzingDuration();
154+
124155
// Arguments to libFuzzer main() function should be added to this array,
125156
// e.g., dictionaries, corpus, number of runs, jobs, etc. The FuzzerDriver of
126157
// libFuzzer expects the non-const argument 'char ***argv' and it does not
127158
// modify it throughout the method.
128159
char *program_args[] = {
129160
const_cast<char *>("RunFuzzTestingMain"), // First arg is program name.
130161
const_cast<char *>(prefix_arg.c_str()), // Crashing inputs directory.
162+
const_cast<char *>(time_arg.c_str()), // Maximum total time.
131163
const_cast<char *>(dict_arg.c_str()), // Dictionary arg.
132164
const_cast<char *>(corpus_arg.c_str()) // Corpus must be the last arg.
133165
};

scripts/fuzzing_ci.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env bash
2+
##
3+
# Copyright 2018 Google
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
# Run fuzz testing using xcodebuild. Given a total fuzzing duration, select
18+
# a fuzzing target to run for half of the fuzzing duration. The remaining
19+
# duration is split equally over the rest of the fuzzing targets. Must be run
20+
# from the project root directory. The script is intended to execute on travis
21+
# cron jobs, but can run locally (on a macOS) from the project root.
22+
23+
# Total time allowed for fuzzing in seconds (40 minutes for now).
24+
readonly ALLOWED_TIME=2400
25+
26+
# An invalid target that is used to retrieve the list of available targets in
27+
# the returned error message.
28+
readonly INVALID_TARGET="INVALID_TARGET"
29+
# The NONE target that does not perform fuzzing. It is valid but useless.
30+
readonly NONE_TARGET="NONE"
31+
32+
# Script exit codes.
33+
readonly EXIT_SUCCESS=0
34+
readonly EXIT_FAILURE=1
35+
36+
# Get day of the year to use it when selecting a main target.
37+
readonly DOY="$(date +%j)"
38+
39+
# Run fuzz testing only if executing on xcode >= 9 because fuzzing requires
40+
# Clang that supports -fsanitize-coverage=trace-pc-guard.
41+
xcode_version="$(xcodebuild -version | head -n 1)"
42+
xcode_version="${xcode_version/Xcode /}"
43+
xcode_major="${xcode_version/.*/}"
44+
45+
if [[ "${xcode_major}" -lt 9 ]]; then
46+
echo "Unsupported version of xcode."
47+
exit ${EXIT_SUCCESS}
48+
fi
49+
50+
# Helper to run fuzz tests using xcodebuild. Takes the fuzzing target as the
51+
# first argument and the fuzzing duration as the second argument.
52+
function run_xcode_fuzzing() {
53+
xcodebuild \
54+
-workspace 'Firestore/Example/Firestore.xcworkspace' \
55+
-scheme 'Firestore_FuzzTests_iOS' \
56+
-sdk 'iphonesimulator' \
57+
-destination 'platform=iOS Simulator,name=iPhone 7' \
58+
FUZZING_TARGET="$1" \
59+
FUZZING_DURATION="$2" \
60+
test
61+
}
62+
63+
# Run fuzz testing with an INVALID_TARGET and grep the response message line
64+
# that contains the string 'Available targets: {'.
65+
fuzzing_targets_line="$(run_xcode_fuzzing ${INVALID_TARGET} "0" \
66+
| grep "Available targets: {")"
67+
68+
# The fuzzing_targets_line string contains { TARGET1 TARGET2 ... TARGETN }.
69+
# The following command extracts the targets and splits them into an array
70+
# in the variable all_fuzzing_targets.
71+
read -r -a all_fuzzing_targets <<< "$(echo ${fuzzing_targets_line} \
72+
| cut -d "{" -f2 | cut -d "}" -f1)"
73+
74+
# Remove the NONE target, if it exists.
75+
for i in "${!all_fuzzing_targets[@]}"; do
76+
if [[ "${all_fuzzing_targets[${i}]}" == ${NONE_TARGET} ]]; then
77+
unset all_fuzzing_targets[${i}]
78+
fi
79+
done
80+
81+
# Count all targets.
82+
readonly fuzzing_targets_count="${#all_fuzzing_targets[@]}"
83+
84+
# Select a primary target to fuzz test for longer. Changes daily. The variable
85+
# todays_primary_target contains the index of the target selected as primary.
86+
todays_primary_target="$(( ${DOY} % ${fuzzing_targets_count} ))"
87+
88+
# Spend half of allowed time on the selected primary target and half on the
89+
# remaining targets.
90+
primary_target_time="$(( ${ALLOWED_TIME} / 2 ))"
91+
other_targets_time="$(( ${ALLOWED_TIME} / 2 / $(( ${fuzzing_targets_count} - 1)) ))"
92+
93+
script_return=${EXIT_SUCCESS}
94+
for i in "${!all_fuzzing_targets[@]}"; do
95+
fuzzing_duration="${other_targets_time}"
96+
if [[ "${i}" -eq "${todays_primary_target}" ]]; then
97+
fuzzing_duration="${primary_target_time}"
98+
fi
99+
fuzzing_target="${all_fuzzing_targets[${i}]}"
100+
101+
echo "Running fuzzing target ${fuzzing_target} for ${fuzzing_duration} seconds..."
102+
fuzzing_results="$(run_xcode_fuzzing "${fuzzing_target}" "${fuzzing_duration}")"
103+
# Get the last line of the fuzzing results.
104+
fuzzing_results_last_line="$(echo "${fuzzing_results}" | tail -n1)"
105+
# If fuzzing succeeded, the last line will start with "Done"; otherwise,
106+
# print all fuzzing results.
107+
if [[ "${fuzzing_results_last_line}" == Done* ]]; then
108+
echo "Fuzz testing for target ${fuzzing_target} succeeded. Ignore the ** TEST FAILED ** message."
109+
else
110+
echo "Error: Fuzz testing for target ${fuzzing_target} failed."
111+
script_return=${EXIT_FAILURE}
112+
echo "Fuzzing logs:"
113+
echo "${fuzzing_results}"
114+
fi
115+
done
116+
117+
exit ${script_return}

0 commit comments

Comments
 (0)