1414"""Centipede engine interface."""
1515
1616from collections import namedtuple
17+ import csv
1718import os
1819import pathlib
1920import re
2021import shutil
22+ from typing import Dict
23+ from typing import List
24+ from typing import Optional
25+ from typing import Union
2126
2227from clusterfuzz ._internal .bot .fuzzers import dictionary_manager
2328from clusterfuzz ._internal .bot .fuzzers import engine_common
2833from clusterfuzz ._internal .system import environment
2934from clusterfuzz ._internal .system import new_process
3035from 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+
75136class 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