Skip to content

Commit 2345c1a

Browse files
authored
Merge pull request #1919 from DaveLak/improve-fuzzer-coverage
Add Submodules Fuzz Target
2 parents 14066e2 + 6d52bdb commit 2345c1a

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import atheris
2+
import sys
3+
import os
4+
import tempfile
5+
from configparser import ParsingError
6+
from utils import is_expected_exception_message
7+
8+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
9+
path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
10+
os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
11+
12+
with atheris.instrument_imports():
13+
from git import Repo, GitCommandError, InvalidGitRepositoryError
14+
15+
16+
def TestOneInput(data):
17+
fdp = atheris.FuzzedDataProvider(data)
18+
19+
with tempfile.TemporaryDirectory() as repo_temp_dir:
20+
repo = Repo.init(path=repo_temp_dir)
21+
repo.index.commit("Initial commit")
22+
23+
try:
24+
with tempfile.TemporaryDirectory() as submodule_temp_dir:
25+
sub_repo = Repo.init(submodule_temp_dir, bare=fdp.ConsumeBool())
26+
sub_repo.index.commit(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512)))
27+
28+
submodule_name = f"submodule_{fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))}"
29+
submodule_path = os.path.join(repo.working_tree_dir, submodule_name)
30+
submodule_url = sub_repo.git_dir
31+
32+
submodule = repo.create_submodule(submodule_name, submodule_path, url=submodule_url)
33+
repo.index.commit(f"Added submodule {submodule_name}")
34+
35+
with submodule.config_writer() as writer:
36+
key_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes()))
37+
value_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes()))
38+
39+
writer.set_value(
40+
fdp.ConsumeUnicodeNoSurrogates(key_length), fdp.ConsumeUnicodeNoSurrogates(value_length)
41+
)
42+
writer.release()
43+
44+
submodule.update(init=fdp.ConsumeBool(), dry_run=fdp.ConsumeBool(), force=fdp.ConsumeBool())
45+
46+
submodule_repo = submodule.module()
47+
new_file_path = os.path.join(
48+
submodule_repo.working_tree_dir,
49+
f"new_file_{fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))}",
50+
)
51+
with open(new_file_path, "wb") as new_file:
52+
new_file.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512)))
53+
submodule_repo.index.add([new_file_path])
54+
submodule_repo.index.commit("Added new file to submodule")
55+
56+
repo.submodule_update(recursive=fdp.ConsumeBool())
57+
submodule_repo.head.reset(commit="HEAD~1", working_tree=fdp.ConsumeBool(), head=fdp.ConsumeBool())
58+
# Use fdp.PickValueInList to ensure at least one of 'module' or 'configuration' is True
59+
module_option_value, configuration_option_value = fdp.PickValueInList(
60+
[(True, False), (False, True), (True, True)]
61+
)
62+
submodule.remove(
63+
module=module_option_value,
64+
configuration=configuration_option_value,
65+
dry_run=fdp.ConsumeBool(),
66+
force=fdp.ConsumeBool(),
67+
)
68+
repo.index.commit(f"Removed submodule {submodule_name}")
69+
70+
except (ParsingError, GitCommandError, InvalidGitRepositoryError, FileNotFoundError, BrokenPipeError):
71+
return -1
72+
except (ValueError, OSError) as e:
73+
expected_messages = [
74+
"SHA is empty",
75+
"Reference at",
76+
"embedded null byte",
77+
"This submodule instance does not exist anymore",
78+
"cmd stdin was empty",
79+
"File name too long",
80+
]
81+
if is_expected_exception_message(e, expected_messages):
82+
return -1
83+
else:
84+
raise e
85+
86+
87+
def main():
88+
atheris.Setup(sys.argv, TestOneInput)
89+
atheris.Fuzz()
90+
91+
92+
if __name__ == "__main__":
93+
main()

fuzzing/fuzz-targets/utils.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import atheris # pragma: no cover
2+
from typing import List # pragma: no cover
3+
4+
5+
@atheris.instrument_func
6+
def is_expected_exception_message(exception: Exception, error_message_list: List[str]) -> bool: # pragma: no cover
7+
"""
8+
Checks if the message of a given exception matches any of the expected error messages, case-insensitively.
9+
10+
Args:
11+
exception (Exception): The exception object raised during execution.
12+
error_message_list (List[str]): A list of error message substrings to check against the exception's message.
13+
14+
Returns:
15+
bool: True if the exception's message contains any of the substrings from the error_message_list,
16+
case-insensitively, otherwise False.
17+
"""
18+
exception_message = str(exception).lower()
19+
for error in error_message_list:
20+
if error.lower() in exception_message:
21+
return True
22+
return False

0 commit comments

Comments
 (0)