44"""
55run_fuzz_target.py
66
7- Run OSS-Fuzz test targets in parallel with enhanced logging.
7+ Run OSS-Fuzz test targets in parallel with enhanced logging and precise exception handling .
88Uses multiprocessing.Pool and logging module for robust task management.
99
1010Usage: python3 run_fuzz_target.py [project_list_file] [--timeout seconds] [--workers N]
1616import subprocess
1717import argparse
1818import logging
19+ import time
20+ import shutil
1921from datetime import datetime
2022from pathlib import Path
2123from typing import List , Optional , Tuple
2830
2931def setup_logging (project_name : str ) -> logging .Logger :
3032 """Configure hierarchical logger with file and console handlers"""
31- LOG_DIR .mkdir (parents = True , exist_ok = True )
32- timestamp = datetime .now ().strftime ("%Y%m%d%H%M%S" )
33- log_file = LOG_DIR / f"run_{ project_name } _{ timestamp } .log"
34-
35- # Create process-specific logger
36- logger = logging .getLogger (f"{ project_name } .{ current_process ().name } " )
37- logger .setLevel (logging .DEBUG )
38-
39- # File handler (all levels)
40- file_handler = logging .FileHandler (log_file , encoding = "utf-8" )
41- file_handler .setLevel (logging .DEBUG )
42- file_formatter = logging .Formatter (
43- "%(asctime)s [PID:%(process)d] %(levelname)s: %(message)s" ,
44- datefmt = "%Y-%m-%d %H:%M:%S"
45- )
46- file_handler .setFormatter (file_formatter )
47-
48- # Console handler (INFO+ only)
49- console_handler = logging .StreamHandler ()
50- console_handler .setLevel (logging .INFO )
51- console_formatter = logging .Formatter (
52- "[%(levelname)s] %(message)s"
53- )
54- console_handler .setFormatter (console_formatter )
55-
56- logger .addHandler (file_handler )
57- logger .addHandler (console_handler )
58-
59- # Capture uncaught exceptions
60- sys .excepthook = lambda exc_type , exc_value , exc_traceback : (
61- logger .critical ("Unhandled exception" , exc_info = (exc_type , exc_value , exc_traceback ))
62- )
63-
64- return logger
33+ try :
34+ LOG_DIR .mkdir (parents = True , exist_ok = True )
35+ timestamp = datetime .now ().strftime ("%Y%m%d%H%M%S" )
36+ log_file = LOG_DIR / f"run_{ project_name } _{ timestamp } .log"
37+
38+ # Create process-specific logger
39+ logger = logging .getLogger (f"{ project_name } .{ current_process ().name } " )
40+ logger .setLevel (logging .DEBUG )
41+
42+ # File handler (all levels)
43+ file_handler = logging .FileHandler (log_file , encoding = "utf-8" )
44+ file_handler .setLevel (logging .DEBUG )
45+ file_formatter = logging .Formatter (
46+ "%(asctime)s [PID:%(process)d] %(levelname)s: %(message)s" ,
47+ datefmt = "%Y-%m-%d %H:%M:%S"
48+ )
49+ file_handler .setFormatter (file_formatter )
50+
51+ # Console handler (INFO+ only)
52+ console_handler = logging .StreamHandler ()
53+ console_handler .setLevel (logging .INFO )
54+ console_formatter = logging .Formatter (
55+ "[%(levelname)s] %(message)s"
56+ )
57+ console_handler .setFormatter (console_formatter )
58+
59+ logger .addHandler (file_handler )
60+ logger .addHandler (console_handler )
61+
62+ # Capture uncaught exceptions
63+ sys .excepthook = lambda exc_type , exc_value , exc_traceback : (
64+ logger .critical ("Unhandled exception" , exc_info = (exc_type , exc_value , exc_traceback ))
65+ )
66+
67+ return logger
68+ except (PermissionError , OSError ) as e :
69+ print (f"❌ Critical logging setup error: { e } " )
70+ sys .exit (1 )
6571
6672def run_command (
6773 cmd : str ,
6874 log_msg : str ,
6975 logger : logging .Logger ,
70- allowed_exit_codes : Optional [List [int ]] = None
76+ allowed_exit_codes : Optional [List [int ]] = None ,
77+ timeout : int = 3600 # 1 hour default timeout
7178) -> bool :
72- """Execute command with real-time logging"""
79+ """Execute command with real-time logging and precise error handling """
7380 allowed_exit_codes = allowed_exit_codes or []
7481 logger .info (f"▶️ { log_msg } ..." )
7582 logger .debug (f" $ { cmd } " )
7683
84+ process = None
7785 try :
86+ # Start process with explicit timeout handling
7887 process = subprocess .Popen (
7988 cmd ,
8089 shell = True ,
@@ -85,68 +94,124 @@ def run_command(
8594 errors = "replace"
8695 )
8796
88- # Stream output to logger
89- if process .stdout :
90- for line in iter (process .stdout .readline , "" ):
91- logger .debug (line .strip ())
92- else :
93- logger .warning ("Process stdout is None" )
94-
95- process .wait ()
97+ # Stream output to logger with timeout control
98+ start_time = time .time ()
99+ while process .poll () is None :
100+ if time .time () - start_time > timeout :
101+ logger .error (f"⌛ Command timed out after { timeout } seconds" )
102+ process .terminate ()
103+ try :
104+ process .wait (timeout = 5 )
105+ except subprocess .TimeoutExpired :
106+ process .kill ()
107+ return False
108+
109+ # Read available output
110+ if process .stdout :
111+ line = process .stdout .readline ()
112+ if line :
113+ logger .debug (line .strip ())
114+ else :
115+ logger .warning ("Process stdout is None" )
116+ time .sleep (0.1 )
96117
97- if process .returncode not in [0 , * allowed_exit_codes ]:
98- logger .error (f"Command failed with exit code: { process .returncode } " )
118+ # Check exit code
119+ exit_code = process .returncode
120+ if exit_code not in [0 , * allowed_exit_codes ]:
121+ logger .error (f"❌ Command failed with exit code: { exit_code } " )
99122 return False
100123 return True
101- except Exception as e :
102- logger .exception (f"💥 Execution exception: { e } " )
124+
125+ except FileNotFoundError as e :
126+ logger .error (f"🔍 Command not found: { cmd .split ()[0 ]} " )
103127 return False
128+ except PermissionError as e :
129+ logger .error (f"🔒 Permission denied for command: { cmd } " )
130+ return False
131+ except subprocess .SubprocessError as e :
132+ logger .exception (f"💥 Subprocess error: { e } " )
133+ return False
134+ except OSError as e :
135+ logger .exception (f"💥 OS error during command execution: { e } " )
136+ return False
137+ finally :
138+ # Ensure process is cleaned up
139+ if process and process .poll () is None :
140+ try :
141+ process .terminate ()
142+ process .wait (timeout = 5 )
143+ except :
144+ pass
104145
105146def discover_targets (project_name : str , logger : logging .Logger ) -> List [str ]:
106- """Discover available Fuzz targets with error handling"""
147+ """Discover available Fuzz targets with precise error handling"""
107148 out_dir = OSS_FUZZ_DIR / "build" / "out" / project_name
108149 targets = []
150+
109151 try :
110- if out_dir .exists ():
111- for f in out_dir .iterdir ():
152+ if not out_dir .exists ():
153+ logger .warning (f"⚠️ Build directory not found: { out_dir } " )
154+ return targets
155+
156+ for f in out_dir .iterdir ():
157+ try :
112158 if f .is_file () and f .name .startswith ("fuzz_" ) and os .access (f , os .X_OK ):
113159 targets .append (f .name )
114- else :
115- logger .warning (f"Build directory not found: { out_dir } " )
116- except Exception as e :
117- logger .exception (f"Target discovery failed: { e } " )
160+ except OSError as e :
161+ logger .warning (f"⚠️ Skipping file { f .name } due to access error: { e } " )
162+
163+ except FileNotFoundError :
164+ logger .error (f"❌ Directory not found: { out_dir } " )
165+ except PermissionError :
166+ logger .error (f"🔒 Permission denied accessing: { out_dir } " )
167+ except OSError as e :
168+ logger .exception (f"💥 OS error during target discovery: { e } " )
169+
118170 return targets
119171
120172def run_project (project_name : str , timeout : int ) -> Tuple [bool , str ]:
121- """Testing workflow for a single project with logging"""
122- logger = setup_logging (project_name )
123- os .chdir (OSS_FUZZ_DIR )
173+ """Testing workflow for a single project with precise error handling"""
174+ try :
175+ logger = setup_logging (project_name )
176+ os .chdir (OSS_FUZZ_DIR )
177+ except (OSError , PermissionError ) as e :
178+ print (f"❌ Critical error initializing project { project_name } : { e } " )
179+ return (False , project_name )
124180
125181 logger .info ("=" * 60 )
126182 logger .info (f"🚀 Starting testing for project: { project_name } " )
127183 logger .debug (f"📝 Log path: { [h .baseFilename for h in logger .handlers if isinstance (h , logging .FileHandler )]} " )
128184 logger .info ("=" * 60 )
129185
130186 # 1. Discover test targets
131- targets = discover_targets (project_name , logger )
132- if not targets :
133- logger .error ("⚠️ No test targets found" )
187+ try :
188+ targets = discover_targets (project_name , logger )
189+ if not targets :
190+ logger .error ("⚠️ No test targets found" )
191+ return (False , project_name )
192+ logger .info (f"🔍 Discovered { len (targets )} test targets: { ', ' .join (targets )} " )
193+ except Exception as e :
194+ logger .exception (f"💥 Target discovery failed unexpectedly: { e } " )
134195 return (False , project_name )
135- logger .info (f"🔍 Discovered { len (targets )} test targets: { ', ' .join (targets )} " )
136196
137197 # 2. Run all targets
138198 all_success = True
139199 for i , target in enumerate (targets , 1 ):
140- cmd = f"python3 infra/helper.py run_fuzzer { project_name } { target } -- -max_total_time={ timeout } "
141- success = run_command (
142- cmd ,
143- f"Running target [{ i } /{ len (targets )} ] { target } (timeout={ timeout } s)" ,
144- logger ,
145- allowed_exit_codes = [1 , 124 ] # Allow timeout exit codes
146- )
147- all_success &= success
148- if not success :
149- logger .error (f"❌ Target failed: { target } " )
200+ try :
201+ cmd = f"python3 infra/helper.py run_fuzzer { project_name } { target } -- -max_total_time={ timeout } "
202+ success = run_command (
203+ cmd ,
204+ f"Running target [{ i } /{ len (targets )} ] { target } (timeout={ timeout } s)" ,
205+ logger ,
206+ allowed_exit_codes = [1 , 124 ], # Allow timeout exit codes
207+ timeout = timeout + 300 # Add buffer for setup/teardown
208+ )
209+ all_success &= success
210+ if not success :
211+ logger .error (f"❌ Target failed: { target } " )
212+ except Exception as e :
213+ logger .exception (f"💥 Unexpected error running target { target } : { e } " )
214+ all_success = False
150215
151216 # 3. Final status
152217 if all_success :
@@ -172,24 +237,63 @@ def main():
172237
173238 logger .info (f"🚀 Starting parallel fuzzing (workers={ args .workers } , timeout={ args .timeout } s)" )
174239
175- # Read project list
240+ # Read project list with precise error handling
176241 try :
177- with open (args .project_list ) as f :
242+ project_path = Path (args .project_list )
243+ if not project_path .exists ():
244+ raise FileNotFoundError (f"Project list file not found: { project_path } " )
245+
246+ if not project_path .is_file ():
247+ raise ValueError (f"Path is not a file: { project_path } " )
248+
249+ with open (project_path , "r" ) as f :
178250 projects = [line .strip () for line in f if line .strip ()]
179- logger .info (f"📋 Loaded { len (projects )} projects from { args .project_list } " )
180- except Exception as e :
181- logger .exception (f"❌ Failed to read project list: { e } " )
251+
252+ logger .info (f"📋 Loaded { len (projects )} projects from { project_path } " )
253+ except FileNotFoundError as e :
254+ logger .error (f"❌ { e } " )
255+ sys .exit (1 )
256+ except PermissionError as e :
257+ logger .error (f"🔒 Permission denied: { e } " )
258+ sys .exit (1 )
259+ except (OSError , ValueError ) as e :
260+ logger .exception (f"💥 Error reading project list: { e } " )
182261 sys .exit (1 )
183262
184- # Parallel execution
263+ # Parallel execution with error isolation
185264 with Pool (args .workers ) as pool :
186- results = pool .starmap (run_project , [(p , args .timeout ) for p in projects ])
265+ results = []
266+ for p in projects :
267+ try :
268+ results .append (pool .apply_async (run_project , (p , args .timeout )))
269+ except Exception as e :
270+ logger .error (f"💥 Failed to schedule project { p } : { e } " )
271+ results .append ((False , p ))
272+
273+ # Collect results with timeout
274+ final_results = []
275+ for res in results :
276+ try :
277+ final_results .append (res .get (timeout = args .timeout * 2 ))
278+ except TimeoutError :
279+ logger .error ("⌛ Project execution timed out" )
280+ final_results .append ((False , "unknown" ))
281+ except Exception as e :
282+ logger .error (f"💥 Error collecting result: { e } " )
283+ final_results .append ((False , "unknown" ))
187284
188285 # Output results
189- failed = [p for success , p in results if not success ]
286+ failed = [p for success , p in final_results if not success ]
190287 logger .info (f"\n 📊 Testing completed: Success { len (projects )- len (failed )} /{ len (projects )} " )
191288 if failed :
192289 logger .error ("❌ Failed projects: " + ", " .join (failed ))
193290
194291if __name__ == "__main__" :
195- main ()
292+ try :
293+ main ()
294+ except KeyboardInterrupt :
295+ print ("\n 🛑 Operation cancelled by user" )
296+ sys .exit (1 )
297+ except Exception as e :
298+ print (f"💥 Critical error in main: { e } " )
299+ sys .exit (1 )
0 commit comments