Skip to content

Commit 2e9c239

Browse files
committed
Extract environment setup and exception checking boilerplate logic
Changes: - Simplify exception handling in test harnesses via `handle_exception(e)` in the `except Exception as e:` block. - `setup_git_environment` is a step towards centralizing environment variable and logging configuration set up consistently across different fuzzing scripts. **Only applying it to a single test for now is an intentional choice in case it fails to work in the ClusterFuzz environment!** If it proves successful, a follow-up change set will be welcome.
1 parent 799b9ca commit 2e9c239

File tree

2 files changed

+95
-62
lines changed

2 files changed

+95
-62
lines changed

Diff for: fuzzing/fuzz-targets/fuzz_submodule.py

+9-61
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,17 @@
1-
# ruff: noqa: E402
21
import atheris
32
import sys
43
import os
5-
import traceback
64
import tempfile
75
from configparser import ParsingError
8-
from utils import get_max_filename_length
9-
import re
10-
11-
bundle_dir = os.path.dirname(os.path.abspath(__file__))
12-
13-
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): # pragma: no cover
14-
bundled_git_binary_path = os.path.join(bundle_dir, "git")
15-
os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = bundled_git_binary_path
16-
176
from git import Repo, GitCommandError, InvalidGitRepositoryError
7+
from utils import (
8+
setup_git_environment,
9+
handle_exception,
10+
get_max_filename_length,
11+
)
1812

19-
20-
def load_exception_list(file_path):
21-
"""Load and parse the exception list from a file."""
22-
try:
23-
with open(file_path, "r") as file:
24-
lines = file.readlines()
25-
exception_list = set()
26-
for line in lines:
27-
match = re.match(r"(.+):(\d+):", line)
28-
if match:
29-
file_path = match.group(1).strip()
30-
line_number = int(match.group(2).strip())
31-
exception_list.add((file_path, line_number))
32-
return exception_list
33-
except FileNotFoundError:
34-
print(f"File not found: {file_path}")
35-
return set()
36-
except Exception as e:
37-
print(f"Error loading exception list: {e}")
38-
return set()
39-
40-
41-
def match_exception_with_traceback(exception_list, exc_traceback):
42-
"""Match exception traceback with the entries in the exception list."""
43-
for filename, lineno, _, _ in traceback.extract_tb(exc_traceback):
44-
for file_pattern, line_pattern in exception_list:
45-
if re.fullmatch(file_pattern, filename) and re.fullmatch(line_pattern, str(lineno)):
46-
return True
47-
return False
48-
49-
50-
def check_exception_against_list(exception_list, exc_traceback):
51-
"""Check if the exception traceback matches any entry in the exception list."""
52-
return match_exception_with_traceback(exception_list, exc_traceback)
53-
54-
55-
if not sys.warnoptions: # pragma: no cover
56-
# The warnings filter below can be overridden by passing the -W option
57-
# to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
58-
import warnings
59-
import logging
60-
61-
# Fuzzing data causes some modules to generate a large number of warnings
62-
# which are not usually interesting and make the test output hard to read, so we ignore them.
63-
warnings.simplefilter("ignore")
64-
logging.getLogger().setLevel(logging.ERROR)
13+
# Setup the git environment
14+
setup_git_environment()
6515

6616

6717
def TestOneInput(data):
@@ -131,12 +81,10 @@ def TestOneInput(data):
13181
):
13282
return -1
13383
except Exception as e:
134-
exc_traceback = e.__traceback__
135-
exception_list = load_exception_list(os.path.join(bundle_dir, "explicit-exceptions-list.txt"))
136-
if check_exception_against_list(exception_list, exc_traceback):
84+
if isinstance(e, ValueError) and "embedded null byte" in str(e):
13785
return -1
13886
else:
139-
raise e
87+
return handle_exception(e)
14088

14189

14290
def main():

Diff for: fuzzing/fuzz-targets/utils.py

+86-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import atheris # pragma: no cover
22
import os # pragma: no cover
3-
from typing import List # pragma: no cover
3+
import re # pragma: no cover
4+
import traceback # pragma: no cover
5+
import sys # pragma: no cover
6+
from typing import Set, Tuple, List # pragma: no cover
47

58

69
@atheris.instrument_func
@@ -35,3 +38,85 @@ def get_max_filename_length(path: str) -> int: # pragma: no cover
3538
int: The maximum filename length.
3639
"""
3740
return os.pathconf(path, "PC_NAME_MAX")
41+
42+
43+
@atheris.instrument_func
44+
def read_lines_from_file(file_path: str) -> list:
45+
"""Read lines from a file and return them as a list."""
46+
try:
47+
with open(file_path, "r") as f:
48+
return [line.strip() for line in f if line.strip()]
49+
except FileNotFoundError:
50+
print(f"File not found: {file_path}")
51+
return []
52+
except IOError as e:
53+
print(f"Error reading file {file_path}: {e}")
54+
return []
55+
56+
57+
@atheris.instrument_func
58+
def load_exception_list(file_path: str = "explicit-exceptions-list.txt") -> Set[Tuple[str, str]]:
59+
"""Load and parse the exception list from a default or specified file."""
60+
try:
61+
bundle_dir = os.path.dirname(os.path.abspath(__file__))
62+
full_path = os.path.join(bundle_dir, file_path)
63+
lines = read_lines_from_file(full_path)
64+
exception_list: Set[Tuple[str, str]] = set()
65+
for line in lines:
66+
match = re.match(r"(.+):(\d+):", line)
67+
if match:
68+
file_path: str = match.group(1).strip()
69+
line_number: str = str(match.group(2).strip())
70+
exception_list.add((file_path, line_number))
71+
return exception_list
72+
except Exception as e:
73+
print(f"Error loading exception list: {e}")
74+
return set()
75+
76+
77+
@atheris.instrument_func
78+
def match_exception_with_traceback(exception_list: Set[Tuple[str, str]], exc_traceback) -> bool:
79+
"""Match exception traceback with the entries in the exception list."""
80+
for filename, lineno, _, _ in traceback.extract_tb(exc_traceback):
81+
for file_pattern, line_pattern in exception_list:
82+
# Ensure filename and line_number are strings for regex matching
83+
if re.fullmatch(file_pattern, filename) and re.fullmatch(line_pattern, str(lineno)):
84+
return True
85+
return False
86+
87+
88+
@atheris.instrument_func
89+
def check_exception_against_list(exc_traceback, exception_file: str = "explicit-exceptions-list.txt") -> bool:
90+
"""Check if the exception traceback matches any entry in the exception list."""
91+
exception_list = load_exception_list(exception_file)
92+
return match_exception_with_traceback(exception_list, exc_traceback)
93+
94+
95+
@atheris.instrument_func
96+
def handle_exception(e: Exception) -> int:
97+
"""Encapsulate exception handling logic for reusability."""
98+
exc_traceback = e.__traceback__
99+
if check_exception_against_list(exc_traceback):
100+
return -1
101+
else:
102+
raise e
103+
104+
105+
@atheris.instrument_func
106+
def setup_git_environment() -> None:
107+
"""Set up the environment variables for Git."""
108+
bundle_dir = os.path.dirname(os.path.abspath(__file__))
109+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): # pragma: no cover
110+
bundled_git_binary_path = os.path.join(bundle_dir, "git")
111+
os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = bundled_git_binary_path
112+
113+
if not sys.warnoptions: # pragma: no cover
114+
# The warnings filter below can be overridden by passing the -W option
115+
# to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
116+
import warnings
117+
import logging
118+
119+
# Fuzzing data causes some modules to generate a large number of warnings
120+
# which are not usually interesting and make the test output hard to read, so we ignore them.
121+
warnings.simplefilter("ignore")
122+
logging.getLogger().setLevel(logging.ERROR)

0 commit comments

Comments
 (0)