fuzzprep 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fuzzprep/__init__.py +3 -0
- fuzzprep/__main__.py +4 -0
- fuzzprep/agents/harness_builder/SKILL.md +156 -0
- fuzzprep/agents/library_builder/SKILL.md +147 -0
- fuzzprep/agents/scripts/check_build.sh +87 -0
- fuzzprep/agents/scripts/check_build_in_container.sh +73 -0
- fuzzprep/agents/scripts/check_dockerfile_from_scratch.sh +33 -0
- fuzzprep/cli.py +1109 -0
- fuzzprep/core/__init__.py +0 -0
- fuzzprep/core/agent_stream.py +304 -0
- fuzzprep/core/files.py +35 -0
- fuzzprep/core/paths.py +25 -0
- fuzzprep/core/reporting.py +218 -0
- fuzzprep/core/repos.py +217 -0
- fuzzprep/core/resources.py +41 -0
- fuzzprep/core/subprocesses.py +197 -0
- fuzzprep/feature_extractor/__init__.py +0 -0
- fuzzprep/feature_extractor/benchmark_yaml.py +92 -0
- fuzzprep/feature_extractor/extraction.py +184 -0
- fuzzprep/feature_extractor/models.py +96 -0
- fuzzprep/feature_extractor/native/.clang-format +1 -0
- fuzzprep/feature_extractor/native/CMakeLists.txt +90 -0
- fuzzprep/feature_extractor/native/include/feature_extractor.hpp +136 -0
- fuzzprep/feature_extractor/native/src/extraction_action.cpp +294 -0
- fuzzprep/feature_extractor/native/src/json_writer.cpp +154 -0
- fuzzprep/feature_extractor/native/src/macro_callbacks.cpp +75 -0
- fuzzprep/feature_extractor/native/src/main.cpp +130 -0
- fuzzprep/feature_extractor/native_build.py +99 -0
- fuzzprep/library_builder/__init__.py +0 -0
- fuzzprep/library_builder/agents.py +472 -0
- fuzzprep/library_builder/analysis.py +145 -0
- fuzzprep/library_builder/build_parameters.py +158 -0
- fuzzprep/library_builder/dependency_resolution.py +139 -0
- fuzzprep/library_builder/environments/__init__.py +0 -0
- fuzzprep/library_builder/environments/base.py +89 -0
- fuzzprep/library_builder/environments/gate.py +96 -0
- fuzzprep/library_builder/environments/local.py +205 -0
- fuzzprep/library_builder/environments/oss_fuzz.py +485 -0
- fuzzprep/library_builder/environments/verification.py +125 -0
- fuzzprep/library_builder/exploration.py +217 -0
- fuzzprep/library_builder/generation.py +325 -0
- fuzzprep/library_builder/harness_explorer.py +257 -0
- fuzzprep/library_builder/models.py +169 -0
- fuzzprep/library_builder/package_names.json +33 -0
- fuzzprep/library_builder/package_names.py +40 -0
- fuzzprep/library_builder/scripts.py +389 -0
- fuzzprep/library_builder/stats.py +102 -0
- fuzzprep/library_builder/symbol_patterns.json +65 -0
- fuzzprep/library_builder/timeouts.py +14 -0
- fuzzprep/library_builder/workspace.py +250 -0
- fuzzprep-0.1.0.dist-info/METADATA +255 -0
- fuzzprep-0.1.0.dist-info/RECORD +56 -0
- fuzzprep-0.1.0.dist-info/WHEEL +4 -0
- fuzzprep-0.1.0.dist-info/entry_points.txt +3 -0
- fuzzprep-0.1.0.dist-info/licenses/LICENSE +202 -0
- fuzzprep-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fuzzprep.core.repos import RepoSource
|
|
7
|
+
from fuzzprep.library_builder.models import (
|
|
8
|
+
AnalysisResult,
|
|
9
|
+
AutotoolsSetup,
|
|
10
|
+
BuildSystem,
|
|
11
|
+
Language,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
_C_HEADER_EXTENSIONS: frozenset[str] = frozenset({".h", ".hpp", ".hxx", ".hh"})
|
|
16
|
+
_C_SOURCE_EXTENSIONS: frozenset[str] = frozenset({".c"})
|
|
17
|
+
# `.C` is C++ by convention, and Path.suffix keeps the case the file was stored with.
|
|
18
|
+
_CPP_SOURCE_EXTENSIONS: frozenset[str] = frozenset({".cc", ".cpp", ".cxx", ".c++", ".C"})
|
|
19
|
+
_VCS_DIRS: frozenset[str] = frozenset({".git", ".hg", ".svn"})
|
|
20
|
+
_READ_CHUNK_BYTES = 1 << 20
|
|
21
|
+
|
|
22
|
+
# Bare Makefile is the last-resort signal: a project shipping meson.build and a convenience
|
|
23
|
+
# top-level Makefile is a meson project.
|
|
24
|
+
_BUILD_SYSTEM_CHECKS: list[tuple[BuildSystem, list[str]]] = [
|
|
25
|
+
(BuildSystem.CMAKE, ["CMakeLists.txt"]),
|
|
26
|
+
(BuildSystem.AUTOTOOLS, ["configure.ac", "configure.in", "configure"]),
|
|
27
|
+
(BuildSystem.MESON, ["meson.build"]),
|
|
28
|
+
(BuildSystem.MAKEFILE, ["Makefile", "makefile"]),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnsupportedRepositoryError(Exception):
|
|
33
|
+
"""Repository has no recognizable C/C++ signals."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def analyze(source: RepoSource) -> AnalysisResult:
|
|
37
|
+
"""Run deterministic static analysis on a repository directory."""
|
|
38
|
+
build_system, build_files = _detect_build_system(source.source_path)
|
|
39
|
+
has_headers, c_lines, cpp_lines = _scan_sources(source.source_path)
|
|
40
|
+
warnings: list[str] = []
|
|
41
|
+
language = _select_language(c_lines, cpp_lines, warnings)
|
|
42
|
+
|
|
43
|
+
if not build_files and not has_headers:
|
|
44
|
+
raise UnsupportedRepositoryError(
|
|
45
|
+
f"No C/C++ signals found in {source.source_path}: "
|
|
46
|
+
"no recognized build system files and no C/C++ headers."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if build_system == BuildSystem.UNKNOWN:
|
|
50
|
+
warnings.append(f"No recognized build system found in {source.source_path}.")
|
|
51
|
+
|
|
52
|
+
if not has_headers:
|
|
53
|
+
warnings.append(f"No C/C++ header files found in {source.source_path}.")
|
|
54
|
+
|
|
55
|
+
autotools_setup = (
|
|
56
|
+
_detect_autotools_setup(source.source_path)
|
|
57
|
+
if build_system == BuildSystem.AUTOTOOLS
|
|
58
|
+
else None
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return AnalysisResult(
|
|
62
|
+
project_name=source.project_name,
|
|
63
|
+
source_path=source.source_path,
|
|
64
|
+
build_system=build_system,
|
|
65
|
+
language=language,
|
|
66
|
+
clone_url=source.clone_url,
|
|
67
|
+
repo_ref=source.repo_ref,
|
|
68
|
+
warnings=warnings,
|
|
69
|
+
autotools_setup=autotools_setup,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _detect_autotools_setup(root: Path) -> AutotoolsSetup:
|
|
74
|
+
"""Detect how to bootstrap autotools for this repository.
|
|
75
|
+
|
|
76
|
+
Priority: an existing configure > autogen.sh > bootstrap > autoreconf from configure.ac.
|
|
77
|
+
Only reached once the tree is known to be autotools, so a bare `bootstrap` here is the
|
|
78
|
+
gnulib-style wrapper rather than some other project's setup script.
|
|
79
|
+
"""
|
|
80
|
+
if (root / "configure").exists():
|
|
81
|
+
return AutotoolsSetup.CONFIGURE
|
|
82
|
+
if (root / "autogen.sh").exists():
|
|
83
|
+
return AutotoolsSetup.AUTOGEN
|
|
84
|
+
if (root / "bootstrap").exists():
|
|
85
|
+
return AutotoolsSetup.BOOTSTRAP
|
|
86
|
+
return AutotoolsSetup.AUTORECONF
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _detect_build_system(root: Path) -> tuple[BuildSystem, list[Path]]:
|
|
90
|
+
"""Detect the primary build system in priority order."""
|
|
91
|
+
for build_system, filenames in _BUILD_SYSTEM_CHECKS:
|
|
92
|
+
found = [root / name for name in filenames if (root / name).exists()]
|
|
93
|
+
if found:
|
|
94
|
+
return build_system, found
|
|
95
|
+
return BuildSystem.UNKNOWN, []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _scan_sources(root: Path) -> tuple[bool, int, int]:
|
|
99
|
+
"""Walk root once for header presence and C/C++ source line counts.
|
|
100
|
+
|
|
101
|
+
Returns whether a C/C++ header exists, the C line count, and the C++ line count. Files under
|
|
102
|
+
a VCS directory do not count. Headers stay out of the line counts because `.h` belongs to
|
|
103
|
+
both languages.
|
|
104
|
+
"""
|
|
105
|
+
has_headers = False
|
|
106
|
+
c_lines = 0
|
|
107
|
+
cpp_lines = 0
|
|
108
|
+
for path in root.rglob("*"):
|
|
109
|
+
if any(part in _VCS_DIRS for part in path.relative_to(root).parts) or not path.is_file():
|
|
110
|
+
continue
|
|
111
|
+
suffix = path.suffix
|
|
112
|
+
if suffix in _C_HEADER_EXTENSIONS:
|
|
113
|
+
has_headers = True
|
|
114
|
+
elif suffix in _C_SOURCE_EXTENSIONS:
|
|
115
|
+
c_lines += _count_lines(path)
|
|
116
|
+
elif suffix in _CPP_SOURCE_EXTENSIONS:
|
|
117
|
+
cpp_lines += _count_lines(path)
|
|
118
|
+
return has_headers, c_lines, cpp_lines
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _count_lines(path: Path) -> int:
|
|
122
|
+
"""Count the lines in a file, or 0 when it cannot be read."""
|
|
123
|
+
try:
|
|
124
|
+
with path.open("rb") as handle:
|
|
125
|
+
return sum(
|
|
126
|
+
chunk.count(b"\n") for chunk in iter(lambda: handle.read(_READ_CHUNK_BYTES), b"")
|
|
127
|
+
)
|
|
128
|
+
except OSError as error:
|
|
129
|
+
logger.debug("Skipping %s while counting lines: %s", path, error)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _select_language(c_lines: int, cpp_lines: int, warnings: list[str]) -> Language:
|
|
134
|
+
"""Pick the dominant language from the C and C++ source line counts.
|
|
135
|
+
|
|
136
|
+
Falls back to C++ with a warning when the tree holds neither: C++ is the safe guess, since a
|
|
137
|
+
C library compiled by a C++ driver links while the reverse does not.
|
|
138
|
+
"""
|
|
139
|
+
logger.debug("C LoC: %d, CPP LoC %d", c_lines, cpp_lines)
|
|
140
|
+
if c_lines == 0 and cpp_lines == 0:
|
|
141
|
+
warnings.append(
|
|
142
|
+
"Could not determine the source language (no C or C++ sources); assuming C++."
|
|
143
|
+
)
|
|
144
|
+
return Language.CPP
|
|
145
|
+
return Language.CPP if cpp_lines > c_lines else Language.C
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Compiler settings used while preparing and emitting a local fuzzing build."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
_DEFAULT_HARNESS_FLAGS = "-fsanitize=fuzzer"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class BuildParameters:
|
|
15
|
+
"""Separate compiler settings for library and final-harness compilation."""
|
|
16
|
+
|
|
17
|
+
cc: str
|
|
18
|
+
cxx: str
|
|
19
|
+
library_cflags: str
|
|
20
|
+
library_cxxflags: str
|
|
21
|
+
harness_cflags: str
|
|
22
|
+
harness_cxxflags: str
|
|
23
|
+
# Build-system-level configure options: cmake cache variables, meson options, autotools
|
|
24
|
+
# --enable-* switches, make variables. Baked into the generated script rather than passed
|
|
25
|
+
# through the environment, since cmake and meson have no environment equivalent.
|
|
26
|
+
library_configure_args: tuple[str, ...] = ()
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_args(cls, args: object) -> BuildParameters:
|
|
30
|
+
"""Resolve CLI arguments against the caller's existing compiler environment."""
|
|
31
|
+
return cls(
|
|
32
|
+
cc=_argument_or_environment(args, "cc", "CC", "clang"),
|
|
33
|
+
cxx=_argument_or_environment(args, "cxx", "CXX", "clang++"),
|
|
34
|
+
library_cflags=_argument_or_environment(args, "library_cflags", "CFLAGS", ""),
|
|
35
|
+
library_cxxflags=_argument_or_environment(args, "library_cxxflags", "CXXFLAGS", ""),
|
|
36
|
+
harness_cflags=_argument_or_default(args, "harness_cflags", _DEFAULT_HARNESS_FLAGS),
|
|
37
|
+
harness_cxxflags=_argument_or_default(args, "harness_cxxflags", _DEFAULT_HARNESS_FLAGS),
|
|
38
|
+
library_configure_args=_repeated_argument(args, "library_configure_args"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def defaults(cls) -> BuildParameters:
|
|
43
|
+
"""The settings a run with no CLI arguments would use.
|
|
44
|
+
|
|
45
|
+
For a caller that builds outside a `generate` invocation and still needs the compiler
|
|
46
|
+
resolution rules the CLI applies.
|
|
47
|
+
"""
|
|
48
|
+
return cls.from_args(_NO_ARGUMENTS)
|
|
49
|
+
|
|
50
|
+
@contextmanager
|
|
51
|
+
def library_environment(self) -> Iterator[None]:
|
|
52
|
+
"""Expose library settings to build commands and repair agents."""
|
|
53
|
+
with _temporary_environment(
|
|
54
|
+
{
|
|
55
|
+
"CC": self.cc,
|
|
56
|
+
"CXX": self.cxx,
|
|
57
|
+
"CFLAGS": self.library_cflags,
|
|
58
|
+
"CXXFLAGS": self.library_cxxflags,
|
|
59
|
+
}
|
|
60
|
+
):
|
|
61
|
+
yield
|
|
62
|
+
|
|
63
|
+
@contextmanager
|
|
64
|
+
def harness_environment(self) -> Iterator[None]:
|
|
65
|
+
"""Expose final-harness settings while validating and emitting its compiler."""
|
|
66
|
+
with _temporary_environment(
|
|
67
|
+
{
|
|
68
|
+
"CC": self.cc,
|
|
69
|
+
"CXX": self.cxx,
|
|
70
|
+
"CFLAGS": self.harness_cflags,
|
|
71
|
+
"CXXFLAGS": self.harness_cxxflags,
|
|
72
|
+
}
|
|
73
|
+
):
|
|
74
|
+
yield
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, str | list[str]]:
|
|
77
|
+
"""Return the effective settings suitable for published run metadata."""
|
|
78
|
+
return {
|
|
79
|
+
"cc": self.cc,
|
|
80
|
+
"cxx": self.cxx,
|
|
81
|
+
"library_cflags": self.library_cflags,
|
|
82
|
+
"library_cxxflags": self.library_cxxflags,
|
|
83
|
+
"harness_cflags": self.harness_cflags,
|
|
84
|
+
"harness_cxxflags": self.harness_cxxflags,
|
|
85
|
+
"library_configure_args": list(self.library_configure_args),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# A sentinel with no attributes, so every getattr in from_args misses and each field falls
|
|
90
|
+
# back to the environment-or-default rules.
|
|
91
|
+
_NO_ARGUMENTS = object()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _argument_or_environment(args: object, argument: str, environment: str, default: str) -> str:
|
|
95
|
+
value = getattr(args, argument, None)
|
|
96
|
+
return value if isinstance(value, str) else os.environ.get(environment, default)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _argument_or_default(args: object, argument: str, default: str) -> str:
|
|
100
|
+
value = getattr(args, argument, None)
|
|
101
|
+
return value if isinstance(value, str) else default
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _repeated_argument(args: object, argument: str) -> tuple[str, ...]:
|
|
105
|
+
value = getattr(args, argument, None)
|
|
106
|
+
if not isinstance(value, list):
|
|
107
|
+
return ()
|
|
108
|
+
return tuple(str(item) for item in value)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_COMPILER_ENVIRONMENT_NAMES = ("CC", "CXX", "CFLAGS", "CXXFLAGS")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@contextmanager
|
|
115
|
+
def neutral_compiler_environment() -> Iterator[None]:
|
|
116
|
+
"""Unset CC/CXX/CFLAGS/CXXFLAGS for the duration.
|
|
117
|
+
|
|
118
|
+
For anything that runs both the library build and the harness compile in one invocation:
|
|
119
|
+
the build gate, and a repair agent, which reaches the gate through check_build.sh and so
|
|
120
|
+
passes on whatever its own shell exports. Each generated script bakes in its own settings,
|
|
121
|
+
so with nothing exported both get the right ones. Leaving one stage's environment in place
|
|
122
|
+
applies its flags to both, and the harness flags in particular (`-fsanitize=fuzzer`
|
|
123
|
+
supplies its own main) make cmake's compiler check and autotools' configure link test fail.
|
|
124
|
+
"""
|
|
125
|
+
original = {name: os.environ.pop(name, None) for name in _COMPILER_ENVIRONMENT_NAMES}
|
|
126
|
+
try:
|
|
127
|
+
yield
|
|
128
|
+
finally:
|
|
129
|
+
for name, previous in original.items():
|
|
130
|
+
if previous is not None:
|
|
131
|
+
os.environ[name] = previous
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@contextmanager
|
|
135
|
+
def compile_commands_capture_environment() -> Iterator[None]:
|
|
136
|
+
"""Ask CMake to write build/compile_commands.json for the duration.
|
|
137
|
+
|
|
138
|
+
CMake reads this as the default for the cache entry, so the flag never has to appear in the
|
|
139
|
+
generated build_library.sh -- capture stays something the harness applies, and the shipped
|
|
140
|
+
script is unaffected. Meson's Ninja backend writes the file unconditionally; Make and
|
|
141
|
+
Autotools have no equivalent and rely on the gate's `bear` wrap instead.
|
|
142
|
+
"""
|
|
143
|
+
with _temporary_environment({"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"}):
|
|
144
|
+
yield
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@contextmanager
|
|
148
|
+
def _temporary_environment(values: dict[str, str]) -> Iterator[None]:
|
|
149
|
+
original = {name: os.environ.get(name) for name in values}
|
|
150
|
+
os.environ.update(values)
|
|
151
|
+
try:
|
|
152
|
+
yield
|
|
153
|
+
finally:
|
|
154
|
+
for name, previous in original.items():
|
|
155
|
+
if previous is None:
|
|
156
|
+
os.environ.pop(name, None)
|
|
157
|
+
else:
|
|
158
|
+
os.environ[name] = previous
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Single dependency-resolution/merge point for library_builder.
|
|
2
|
+
|
|
3
|
+
Every dependency, whatever discovered it, becomes a `LibraryDependency` tagged with a
|
|
4
|
+
`DependencySource` and reaches the persisted state through one `merge()`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import itertools
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DependencySource(StrEnum):
|
|
17
|
+
LINKER = "linker"
|
|
18
|
+
LIBRARY_AGENT = "library_agent"
|
|
19
|
+
HARNESS_AGENT = "harness_agent"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class LibraryDependency:
|
|
24
|
+
"""One dependency, possibly only partially resolved. Only `source` is required."""
|
|
25
|
+
|
|
26
|
+
source: DependencySource
|
|
27
|
+
name: str | None = None
|
|
28
|
+
apt_package: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DependencyState:
|
|
33
|
+
"""Persisted dependency-resolution state — the on-disk shape of state.json."""
|
|
34
|
+
|
|
35
|
+
version: int = 1
|
|
36
|
+
apt_packages: list[str] = field(default_factory=list)
|
|
37
|
+
sources: dict[str, list[str]] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_state(path: Path) -> DependencyState:
|
|
41
|
+
"""Load state.json for a project; return an empty DependencyState if absent/malformed."""
|
|
42
|
+
if not path.exists():
|
|
43
|
+
return DependencyState()
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(path.read_text())
|
|
46
|
+
except (json.JSONDecodeError, OSError):
|
|
47
|
+
return DependencyState()
|
|
48
|
+
|
|
49
|
+
state = DependencyState()
|
|
50
|
+
if isinstance(data.get("apt_packages"), list):
|
|
51
|
+
state.apt_packages = [str(p) for p in data["apt_packages"]]
|
|
52
|
+
if isinstance(data.get("sources"), dict):
|
|
53
|
+
state.sources = {
|
|
54
|
+
k: [str(p) for p in v] for k, v in data["sources"].items() if isinstance(v, list)
|
|
55
|
+
}
|
|
56
|
+
return state
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_state(path: Path, state: DependencyState) -> None:
|
|
60
|
+
"""Write state.json for a project."""
|
|
61
|
+
path.write_text(
|
|
62
|
+
json.dumps(
|
|
63
|
+
{
|
|
64
|
+
"version": state.version,
|
|
65
|
+
"apt_packages": state.apt_packages,
|
|
66
|
+
"sources": state.sources,
|
|
67
|
+
},
|
|
68
|
+
indent=2,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def merge(state: DependencyState, dependencies: list[LibraryDependency]) -> None:
|
|
74
|
+
"""Union dependencies into state in-place, deduplicating while preserving order.
|
|
75
|
+
|
|
76
|
+
The only function permitted to mutate DependencyState.apt_packages/sources, and
|
|
77
|
+
idempotent: merging the same list twice leaves the same state as merging it once.
|
|
78
|
+
"""
|
|
79
|
+
new_apt: list[str] = []
|
|
80
|
+
new_apt_by_source: dict[str, list[str]] = {}
|
|
81
|
+
|
|
82
|
+
for dep in dependencies:
|
|
83
|
+
if dep.apt_package is not None:
|
|
84
|
+
new_apt.append(dep.apt_package)
|
|
85
|
+
new_apt_by_source.setdefault(dep.source.value, []).append(dep.apt_package)
|
|
86
|
+
|
|
87
|
+
state.apt_packages = list(dict.fromkeys(state.apt_packages + new_apt))
|
|
88
|
+
for source_tag, apt_names in new_apt_by_source.items():
|
|
89
|
+
existing = state.sources.get(source_tag, [])
|
|
90
|
+
state.sources[source_tag] = list(dict.fromkeys(existing + apt_names))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def from_deterministic_probe(
|
|
94
|
+
missing_system_libs: list[str], transitive_link_flags: list[str]
|
|
95
|
+
) -> list[LibraryDependency]:
|
|
96
|
+
"""Translate the deterministic harness-link probe's outputs into LibraryDependency entries.
|
|
97
|
+
|
|
98
|
+
Unions the libs the linker reported missing with the bare names in transitive_link_flags,
|
|
99
|
+
which it resolved silently because the exploration host already had them. Each name is
|
|
100
|
+
translated on its own, so a system library — which translate() drops without recording it
|
|
101
|
+
as unknown — is excluded rather than misclassified.
|
|
102
|
+
"""
|
|
103
|
+
from fuzzprep.library_builder.harness_explorer import lib_names_from_link_flags
|
|
104
|
+
from fuzzprep.library_builder.package_names import translate
|
|
105
|
+
|
|
106
|
+
names_from_flags = lib_names_from_link_flags(transitive_link_flags)
|
|
107
|
+
names = list(dict.fromkeys(missing_system_libs + names_from_flags))
|
|
108
|
+
|
|
109
|
+
dependencies: list[LibraryDependency] = []
|
|
110
|
+
for name in names:
|
|
111
|
+
translation = translate([name])
|
|
112
|
+
if not (translation.apt_packages or translation.unknown_libs):
|
|
113
|
+
continue # a system library, which translate() drops silently
|
|
114
|
+
dependencies.append(
|
|
115
|
+
LibraryDependency(
|
|
116
|
+
source=DependencySource.LINKER,
|
|
117
|
+
name=name,
|
|
118
|
+
apt_package=translation.apt_packages[0] if translation.apt_packages else None,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return dependencies
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def from_agent_report(
|
|
125
|
+
missing_libs: list[str],
|
|
126
|
+
missing_apt_packages: list[str],
|
|
127
|
+
*,
|
|
128
|
+
source: DependencySource,
|
|
129
|
+
) -> list[LibraryDependency]:
|
|
130
|
+
"""Wrap an agent's self-reported dependency lists into LibraryDependency entries.
|
|
131
|
+
|
|
132
|
+
Zips the two lists positionally: index i's name and apt package are only *assumed* to
|
|
133
|
+
describe the same dependency. Nothing guarantees that when an agent reports more than one
|
|
134
|
+
dependency in a run — a known limitation.
|
|
135
|
+
"""
|
|
136
|
+
return [
|
|
137
|
+
LibraryDependency(source=source, name=name, apt_package=apt)
|
|
138
|
+
for name, apt in itertools.zip_longest(missing_libs, missing_apt_packages)
|
|
139
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
6
|
+
|
|
7
|
+
from fuzzprep.library_builder.timeouts import DEFAULT_BUILD_TIMEOUT_SECONDS
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from fuzzprep.library_builder.build_parameters import BuildParameters
|
|
11
|
+
from fuzzprep.library_builder.models import (
|
|
12
|
+
AnalysisResult,
|
|
13
|
+
BuildExplorationResult,
|
|
14
|
+
HarnessExplorationResult,
|
|
15
|
+
Language,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Environment(Enum):
|
|
20
|
+
LOCAL = "local"
|
|
21
|
+
OSS_FUZZ = "oss-fuzz"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def harness_probe_command(self) -> list[str]:
|
|
25
|
+
"""How to recompile the harnesses in this environment.
|
|
26
|
+
|
|
27
|
+
The generated scripts are environment-independent; entering them is not. OSS-Fuzz goes
|
|
28
|
+
through the base image's `compile`, which assembles what the image only half-provides:
|
|
29
|
+
it resolves SANITIZER_FLAGS into CFLAGS/CXXFLAGS, and exports
|
|
30
|
+
LIB_FUZZING_ENGINE=-fsanitize=fuzzer in place of the deprecated archive path the
|
|
31
|
+
image's ENV names. Running compile_harnesses.sh directly there fails on that missing
|
|
32
|
+
archive, or — if only the engine flag is patched — links an uninstrumented harness.
|
|
33
|
+
|
|
34
|
+
`compile` runs build.sh, so it re-enters the library build too. build_library.sh's
|
|
35
|
+
skip-if-already-built guard keeps that cheap enough to repeat per discovery attempt,
|
|
36
|
+
and the real link line is what makes discovery faithful: sanitizer flags change which
|
|
37
|
+
symbols come out undefined.
|
|
38
|
+
|
|
39
|
+
Hence --base-image accepts OSS-Fuzz-style images only, and OssFuzzExecutor checks for
|
|
40
|
+
`compile` before probing.
|
|
41
|
+
"""
|
|
42
|
+
if self is Environment.OSS_FUZZ:
|
|
43
|
+
return ["bash", "-c", "compile"]
|
|
44
|
+
return ["bash", "compile_harnesses.sh"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EnvironmentUnavailableError(Exception):
|
|
48
|
+
"""The selected environment itself is not usable (e.g. Docker daemon unreachable).
|
|
49
|
+
|
|
50
|
+
Not a build failure: callers must not route it to agent fallback, since no edit to a
|
|
51
|
+
build script fixes an unavailable environment.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str, environment: Environment) -> None:
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.environment = environment
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EnvironmentExecutor(Protocol):
|
|
60
|
+
"""Runs the two generate pipeline stages in a specific target environment."""
|
|
61
|
+
|
|
62
|
+
def run_library_build(
|
|
63
|
+
self,
|
|
64
|
+
analysis: AnalysisResult,
|
|
65
|
+
workdir: Path,
|
|
66
|
+
*,
|
|
67
|
+
timeout: int = DEFAULT_BUILD_TIMEOUT_SECONDS,
|
|
68
|
+
parameters: BuildParameters | None = None,
|
|
69
|
+
) -> BuildExplorationResult: ...
|
|
70
|
+
|
|
71
|
+
def run_harness_compile( # noqa: PLR0913 -- paths and build configuration are independent inputs
|
|
72
|
+
self,
|
|
73
|
+
install_dir: Path,
|
|
74
|
+
workdir: Path,
|
|
75
|
+
language: Language,
|
|
76
|
+
*,
|
|
77
|
+
extra_include_paths: list[str] | None = None,
|
|
78
|
+
extra_library_paths: list[str] | None = None,
|
|
79
|
+
parameters: BuildParameters | None = None,
|
|
80
|
+
library_llm_used: bool = False,
|
|
81
|
+
) -> HarnessExplorationResult: ...
|
|
82
|
+
|
|
83
|
+
def check_availability(self, parameters: BuildParameters | None = None) -> None:
|
|
84
|
+
"""Raise EnvironmentUnavailableError if this environment cannot be used.
|
|
85
|
+
|
|
86
|
+
`parameters` is the run's resolved compiler settings, since what an environment needs
|
|
87
|
+
can depend on them: the local host must provide the harness toolchain itself.
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Applying the shared build gate to a stage's probe result.
|
|
2
|
+
|
|
3
|
+
Both executors reach the same conclusion the same way, so the decision lives here once:
|
|
4
|
+
run the gate when the probe found something worth gating, and otherwise report the command
|
|
5
|
+
that would reproduce the failure without paying to rerun it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from fuzzprep.library_builder import workspace
|
|
14
|
+
from fuzzprep.library_builder.build_parameters import (
|
|
15
|
+
compile_commands_capture_environment,
|
|
16
|
+
neutral_compiler_environment,
|
|
17
|
+
)
|
|
18
|
+
from fuzzprep.library_builder.environments import verification
|
|
19
|
+
from fuzzprep.library_builder.environments.base import Environment
|
|
20
|
+
from fuzzprep.library_builder.models import HarnessExplorationResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def apply_to_harness_result( # noqa: PLR0913 -- 4 keyword-only inputs, each independently meaningful
|
|
24
|
+
harness_result: HarnessExplorationResult,
|
|
25
|
+
workdir: Path,
|
|
26
|
+
*,
|
|
27
|
+
environment: Environment,
|
|
28
|
+
project_name: str,
|
|
29
|
+
library_llm_used: bool = False,
|
|
30
|
+
bypass_scratch_validation: bool = False,
|
|
31
|
+
) -> HarnessExplorationResult:
|
|
32
|
+
"""Gate a harness probe's outcome with agents/scripts/check_build.sh.
|
|
33
|
+
|
|
34
|
+
Skipped when the probe already failed or had nothing to link against: the gate would only
|
|
35
|
+
reconfirm the same failure at the cost of a full rebuild. Its command is still reported, so
|
|
36
|
+
the diagnostic says how to reproduce it.
|
|
37
|
+
|
|
38
|
+
The gate rebuilds the library from nothing only when something changed since the last
|
|
39
|
+
cold build, which is exactly when a repair agent produced this one. On the deterministic
|
|
40
|
+
lane explore() already deleted build/ and install/ before building, so a rebuild here
|
|
41
|
+
would repeat that build to reach the same state -- the run's single largest avoidable
|
|
42
|
+
cost. bypass_scratch_validation drops the rebuild on the agent lane too, trading the
|
|
43
|
+
from-scratch guarantee for speed at the caller's explicit request.
|
|
44
|
+
"""
|
|
45
|
+
keep_artifacts = verification.gate_keeps_artifacts(
|
|
46
|
+
library_llm_used=library_llm_used,
|
|
47
|
+
bypass_scratch_validation=bypass_scratch_validation,
|
|
48
|
+
)
|
|
49
|
+
command = verification.verification_command(
|
|
50
|
+
workdir,
|
|
51
|
+
environment=environment,
|
|
52
|
+
project_name=project_name,
|
|
53
|
+
keep_artifacts=keep_artifacts,
|
|
54
|
+
)
|
|
55
|
+
if not harness_result.static_libs or not harness_result.succeeded:
|
|
56
|
+
return dataclasses.replace(
|
|
57
|
+
harness_result, command=command, gate_keeps_artifacts=keep_artifacts
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# The gate builds the library and compiles harnesses in one invocation, so no stage's
|
|
61
|
+
# compiler environment may be in effect: each generated script bakes in its own.
|
|
62
|
+
with neutral_compiler_environment(), compile_commands_capture_environment():
|
|
63
|
+
result = verification.run_verification(
|
|
64
|
+
workdir,
|
|
65
|
+
environment=environment,
|
|
66
|
+
project_name=project_name,
|
|
67
|
+
keep_artifacts=keep_artifacts,
|
|
68
|
+
)
|
|
69
|
+
_localize_compile_commands(workdir, environment=environment)
|
|
70
|
+
return dataclasses.replace(
|
|
71
|
+
harness_result,
|
|
72
|
+
succeeded=result.passed,
|
|
73
|
+
command=result.command,
|
|
74
|
+
stdout=result.stdout,
|
|
75
|
+
stderr=result.stderr,
|
|
76
|
+
exit_code=0 if result.passed else 1,
|
|
77
|
+
duration_seconds=result.duration_seconds,
|
|
78
|
+
gate_keeps_artifacts=keep_artifacts,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _localize_compile_commands(workdir: Path, *, environment: Environment) -> None:
|
|
83
|
+
"""Rewrite a container-built capture's /src paths back to the host workspace.
|
|
84
|
+
|
|
85
|
+
Done here because here is where the container build happens, so every lane -- deterministic
|
|
86
|
+
or agent-repaired -- gets the same treatment. Keyed to the library-build result instead, it
|
|
87
|
+
was skipped whenever a repair agent produced the build, publishing container paths.
|
|
88
|
+
"""
|
|
89
|
+
if environment is not Environment.OSS_FUZZ:
|
|
90
|
+
return
|
|
91
|
+
# Imported here: oss_fuzz imports this module, so a module-level import would be circular.
|
|
92
|
+
from fuzzprep.library_builder.environments.oss_fuzz import rewrite_compile_commands_paths
|
|
93
|
+
|
|
94
|
+
captured = workspace.find_compile_commands(workdir)
|
|
95
|
+
if captured is not None:
|
|
96
|
+
rewrite_compile_commands_paths(captured, workdir)
|