Skip to content

Commit 15a7095

Browse files
committed
use precise exception log info
1 parent 02f9269 commit 15a7095

1 file changed

Lines changed: 186 additions & 82 deletions

File tree

fuzz/run_fuzz_target.py

Lines changed: 186 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55
run_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.
88
Uses multiprocessing.Pool and logging module for robust task management.
99
1010
Usage: python3 run_fuzz_target.py [project_list_file] [--timeout seconds] [--workers N]
@@ -16,6 +16,8 @@
1616
import subprocess
1717
import argparse
1818
import logging
19+
import time
20+
import shutil
1921
from datetime import datetime
2022
from pathlib import Path
2123
from typing import List, Optional, Tuple
@@ -28,53 +30,60 @@
2830

2931
def 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

6672
def 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

105146
def 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

120172
def 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

194291
if __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

Comments
 (0)