Skip to content

Commit 2a9c6fb

Browse files
committed
[centipede] add support for gathering stats for centipede runs
This PR adds supports so that CF supports Centipede stats. This will help understand better how centipede fuzzers are performing on ClusterFuzz.
1 parent 13fb6f2 commit 2a9c6fb

File tree

4 files changed

+575
-7
lines changed

4 files changed

+575
-7
lines changed

src/clusterfuzz/_internal/bot/fuzzers/centipede/engine.py

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
"""Centipede engine interface."""
1515

1616
from collections import namedtuple
17+
import csv
1718
import os
1819
import pathlib
1920
import re
2021
import shutil
22+
from typing import Dict
23+
from typing import List
24+
from typing import Optional
25+
from typing import Union
2126

2227
from clusterfuzz._internal.bot.fuzzers import dictionary_manager
2328
from clusterfuzz._internal.bot.fuzzers import engine_common
@@ -28,6 +33,7 @@
2833
from clusterfuzz._internal.system import environment
2934
from clusterfuzz._internal.system import new_process
3035
from clusterfuzz.fuzz import engine
36+
from clusterfuzz.stacktraces import constants as stacktraces_constants
3137

3238
_CLEAN_EXIT_SECS = 10
3339

@@ -72,9 +78,68 @@ def _set_sanitizer_options(fuzzer_path):
7278
environment.set_memory_tool_options(sanitizer_options_var, sanitizer_options)
7379

7480

81+
def _parse_centipede_stats(
82+
stats_file: str) -> Optional[Dict[str, Union[int, float]]]:
83+
"""Parses the Centipede stats file and returns a dictionary with labels
84+
and their respective values.
85+
86+
Args:
87+
stats_file: the path to Centipede stats file.
88+
89+
Returns:
90+
a dictionary containing the stats.
91+
"""
92+
if not os.path.exists(stats_file):
93+
return None
94+
with open(stats_file, 'r') as statsfile:
95+
csvreader = csv.reader(statsfile)
96+
l = list(csvreader)
97+
# If the binary could not run at all, the file will be empty or with only
98+
# the column description line.
99+
if len(l) <= 1:
100+
return None
101+
return {
102+
l[0][i]: float(l[-1][i]) if '.' in l[-1][i] else int(l[-1][i])
103+
for i in range(0,
104+
len(l[0]) - 1)
105+
}
106+
107+
108+
def _parse_centipede_logs(log_lines: List[str]) -> Dict[str, int]:
109+
"""Parses Centipede outputs and generates stats for it.
110+
111+
Args:
112+
log_lines: the log lines.
113+
114+
Returns:
115+
the stats.
116+
"""
117+
stats = {
118+
'crash_count': 0,
119+
'timeout_count': 0,
120+
'oom_count': 0,
121+
'leak_count': 0,
122+
}
123+
for line in log_lines:
124+
if re.search(stacktraces_constants.CENTIPEDE_TIMEOUT_REGEX, line):
125+
stats['timeout_count'] = 1
126+
continue
127+
if re.search(stacktraces_constants.OUT_OF_MEMORY_REGEX, line):
128+
stats['oom_count'] = 1
129+
continue
130+
if re.search(CRASH_REGEX, line):
131+
stats['crash_count'] = 1
132+
continue
133+
return stats
134+
135+
75136
class Engine(engine.Engine):
76137
"""Centipede engine implementation."""
77138

139+
def __init__(self):
140+
super().__init__()
141+
self.workdir = self._create_temp_dir('workdir')
142+
78143
@property
79144
def name(self):
80145
return 'centipede'
@@ -126,8 +191,7 @@ def prepare(self, corpus_dir, target_path, build_dir):
126191
# 1. Centipede-readable corpus file;
127192
# 2. Centipede-readable feature file;
128193
# 3. Crash reproducing inputs.
129-
workdir = self._create_temp_dir('workdir')
130-
arguments[constants.WORKDIR_FLAGNAME] = str(workdir)
194+
arguments[constants.WORKDIR_FLAGNAME] = str(self.workdir)
131195

132196
# Directory corpus_dir saves the corpus files required by ClusterFuzz.
133197
arguments[constants.CORPUS_DIR_FLAGNAME] = corpus_dir
@@ -214,6 +278,7 @@ def fuzz(self, target_path, options, reproducers_dir, max_time): # pylint: disa
214278
timeout = max_time + _CLEAN_EXIT_SECS
215279
fuzz_result = runner.run_and_wait(
216280
additional_args=options.arguments, timeout=timeout)
281+
log_lines = fuzz_result.output.splitlines()
217282
fuzz_result.output = Engine.trim_logs(fuzz_result.output)
218283

219284
reproducer_path = _get_reproducer_path(fuzz_result.output, reproducers_dir)
@@ -224,8 +289,20 @@ def fuzz(self, target_path, options, reproducers_dir, max_time): # pylint: disa
224289
str(reproducer_path), fuzz_result.output, [],
225290
int(fuzz_result.time_executed)))
226291

227-
# Stats report is not available in Centipede yet.
228-
stats = None
292+
stats_filename = f'fuzzing-stats-{os.path.basename(target_path)}.000000.csv'
293+
stats_file = os.path.join(self.workdir, stats_filename)
294+
stats = _parse_centipede_stats(stats_file)
295+
if not stats:
296+
stats = {}
297+
actual_duration = int(
298+
stats.get('FuzzTimeSec_Avg', fuzz_result.time_executed or 0.0))
299+
fuzzing_time_percent = 100 * actual_duration / float(max_time)
300+
stats.update({
301+
'expected_duration': int(max_time),
302+
'actual_duration': actual_duration,
303+
'fuzzing_time_percent': fuzzing_time_percent,
304+
})
305+
stats.update(_parse_centipede_logs(log_lines))
229306
return engine.FuzzResult(fuzz_result.output, fuzz_result.command, crashes,
230307
stats, fuzz_result.time_executed)
231308

@@ -412,10 +489,9 @@ def minimize_testcase(self, target_path, arguments, input_path, output_path,
412489
TimeoutError: If the testcase minimization exceeds max_time.
413490
"""
414491
runner = _get_runner(target_path)
415-
workdir = self._create_temp_dir('workdir')
416492
args = [
417493
f'--binary={target_path}',
418-
f'--workdir={workdir}',
494+
f'--workdir={self.workdir}',
419495
f'--minimize_crash={input_path}',
420496
f'--num_runs={constants.NUM_RUNS_PER_MINIMIZATION}',
421497
'--seed=1',
@@ -425,7 +501,7 @@ def minimize_testcase(self, target_path, arguments, input_path, output_path,
425501
logs.warning(
426502
'Testcase minimization timed out.', fuzzer_output=result.output)
427503
raise TimeoutError('Minimization timed out.')
428-
minimum_testcase = self._get_smallest_crasher(workdir)
504+
minimum_testcase = self._get_smallest_crasher(self.workdir)
429505
if minimum_testcase:
430506
shutil.copyfile(minimum_testcase, output_path)
431507
else:

0 commit comments

Comments
 (0)