fuzzer_runner: split fuzzer's dependencies (#320)

Before this commit, the fuzzer dependend on clusterfuzz, which uses
protobuf 3.20. Since fuzzer-bot depended on common subpackage as well
and also common (and the other packages) require protobuf, we had to use
protobuf 3.20 everywhere. This old dependency however means that a lot
of packages can't be used in their "newer" versions, because they depend
on newer protobuf versions.

This commit splits the fuzzer into a separate fuzzer-runner that is
executed in a separate process inside a separate venv. fuzzer-runner
executes the clusterfuzz-heavy operations (e.g. fuzzing) and isolates the
clusterfuzz dependency, so that the rest of the system can use newer
protobuf version.

* split `fuzzer-bot` in `fuzzer-runner` and `fuzzer-bot`
* have a "full" optional dependency group in `common`, including `openlit`
  and `protobuf`, so that `fuzzer-runner` can use the lite version of `common`
  without bringing those heavy deps
* move `FuzzConfiguration`/`BuildConfiguration` in a separate common file
  that doesn't require to load all protobuf files. Again, in this way
  other components can just depend on the `common` "lite" version and not
  require protobuf stuff
* add `fuzzer-runner` as a separate venv inside the `fuzzer-bot` container
* add `RunnerProxy` class in fuzzer package to provide an interface to
  interact with the fuzzer-runner binary.
This commit is contained in:
Riccardo Schirone
2025-09-10 09:52:08 +02:00
committed by GitHub
parent 7739f7907d
commit 143a59c236
49 changed files with 5790 additions and 3182 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
**/.pdm-build/ **/.pdm-build/
**/.pdm-python **/.pdm-python
**/pdm.lock **/pdm.lock
fuzzer/dockerfiles/runner_image.Dockerfile fuzzer/Dockerfile
orchestrator/Dockerfile orchestrator/Dockerfile
patcher/Dockerfile patcher/Dockerfile
seed-gen/Dockerfile seed-gen/Dockerfile
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- component: orchestrator - component: orchestrator
dockerfile: ./orchestrator/Dockerfile dockerfile: ./orchestrator/Dockerfile
- component: fuzzer - component: fuzzer
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
- component: patcher - component: patcher
dockerfile: ./patcher/Dockerfile dockerfile: ./patcher/Dockerfile
- component: seed-gen - component: seed-gen
+6 -2
View File
@@ -150,7 +150,7 @@ send-libpng-task:
# Development targets # Development targets
lint: lint:
@echo "Linting all Python code..." @echo "Linting all Python code..."
@set -e; for component in common orchestrator fuzzer program-model seed-gen patcher; do \ @set -e; for component in common orchestrator fuzzer fuzzer_runner program-model seed-gen patcher; do \
make --no-print-directory lint-component COMPONENT=$$component; \ make --no-print-directory lint-component COMPONENT=$$component; \
done done
@@ -169,7 +169,7 @@ lint-component:
reformat: reformat:
@echo "Reformatting all Python code..." @echo "Reformatting all Python code..."
@for component in common orchestrator fuzzer program-model seed-gen patcher; do \ @for component in common orchestrator fuzzer fuzzer_runner program-model seed-gen patcher; do \
make --no-print-directory reformat-component COMPONENT=$$component; \ make --no-print-directory reformat-component COMPONENT=$$component; \
done done
@@ -187,6 +187,10 @@ undeploy:
@echo "Cleaning up deployment..." @echo "Cleaning up deployment..."
cd deployment && make down cd deployment && make down
undeploy-k8s:
@echo "Cleaning up kubernetes resources..."
cd deployment && make down-k8s
clean-local: clean-local:
@echo "Cleaning up local environment..." @echo "Cleaning up local environment..."
minikube delete || true minikube delete || true
+10 -7
View File
@@ -6,20 +6,23 @@ authors = [{ name = "Trail of Bits", email = "opensource@trailofbits.com" }]
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
requires-python = ">=3.12,<3.13" requires-python = ">=3.12,<3.13"
dependencies = [ dependencies = [
"protobuf ==3.20.3", # Pin exactly for stability
"pydantic-settings ~=2.7.1", "pydantic-settings ~=2.7.1",
"pymongo ~=4.10.1", "pymongo ~=4.10.1",
"redis ~=5.2.1", "redis ~=5.2.1",
"langchain-core ~=0.3.32", "langchain-core ~=0.3.74",
"langchain-openai ~=0.3.2", "langchain-openai ~=0.3.30",
"langchain ~=0.3.27",
"langfuse ~=2.59.2", "langfuse ~=2.59.2",
"langchain ~=0.3.18",
"six ~=1.17.0", "six ~=1.17.0",
"openlit ==1.32.12", # 1.33 currently fails to import opentelemetry.sdk._events
"langchain-community ~=0.3.20", # implicit dep of openlit if you instrument langchain
"langchain-text-splitters ~=0.3.7", # implicit dep of openlit if you instrument langchain
] ]
[project.optional-dependencies]
full = [
"protobuf>=5.0",
"openlit ==1.35.0",
]
[project.urls] [project.urls]
Repository = "https://github.com/trailofbits/buttercup" Repository = "https://github.com/trailofbits/buttercup"
Issues = "https://github.com/trailofbits/buttercup/issues" Issues = "https://github.com/trailofbits/buttercup/issues"
File diff suppressed because one or more lines are too long
@@ -4,299 +4,315 @@ from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
ACCEPTED: SubmissionResult
COVERAGE: BuildType
DEADLINE_EXCEEDED: SubmissionResult
DESCRIPTOR: _descriptor.FileDescriptor DESCRIPTOR: _descriptor.FileDescriptor
ERRORED: SubmissionResult
FAILED: SubmissionResult class BuildType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
FUZZER: _ClassVar[BuildType]
COVERAGE: _ClassVar[BuildType]
TRACER_NO_DIFF: _ClassVar[BuildType]
PATCH: _ClassVar[BuildType]
class SubmissionResult(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
NONE: _ClassVar[SubmissionResult]
ACCEPTED: _ClassVar[SubmissionResult]
PASSED: _ClassVar[SubmissionResult]
FAILED: _ClassVar[SubmissionResult]
DEADLINE_EXCEEDED: _ClassVar[SubmissionResult]
ERRORED: _ClassVar[SubmissionResult]
INCONCLUSIVE: _ClassVar[SubmissionResult]
FUZZER: BuildType FUZZER: BuildType
INCONCLUSIVE: SubmissionResult COVERAGE: BuildType
NONE: SubmissionResult
PASSED: SubmissionResult
PATCH: BuildType
TRACER_NO_DIFF: BuildType TRACER_NO_DIFF: BuildType
PATCH: BuildType
NONE: SubmissionResult
ACCEPTED: SubmissionResult
PASSED: SubmissionResult
FAILED: SubmissionResult
DEADLINE_EXCEEDED: SubmissionResult
ERRORED: SubmissionResult
INCONCLUSIVE: SubmissionResult
class BuildOutput(_message.Message): class Task(_message.Message):
__slots__ = ["apply_diff", "build_type", "engine", "internal_patch_id", "sanitizer", "task_dir", "task_id"] __slots__ = ("message_id", "message_time", "task_id", "task_type", "sources", "deadline", "cancelled", "project_name", "focus", "metadata")
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int] class TaskType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int] __slots__ = ()
ENGINE_FIELD_NUMBER: _ClassVar[int] TASK_TYPE_FULL: _ClassVar[Task.TaskType]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int] TASK_TYPE_DELTA: _ClassVar[Task.TaskType]
SANITIZER_FIELD_NUMBER: _ClassVar[int] TASK_TYPE_FULL: Task.TaskType
TASK_DIR_FIELD_NUMBER: _ClassVar[int] TASK_TYPE_DELTA: Task.TaskType
class MetadataEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
MESSAGE_TIME_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int] TASK_ID_FIELD_NUMBER: _ClassVar[int]
apply_diff: bool TASK_TYPE_FIELD_NUMBER: _ClassVar[int]
build_type: BuildType SOURCES_FIELD_NUMBER: _ClassVar[int]
engine: str DEADLINE_FIELD_NUMBER: _ClassVar[int]
internal_patch_id: str CANCELLED_FIELD_NUMBER: _ClassVar[int]
sanitizer: str PROJECT_NAME_FIELD_NUMBER: _ClassVar[int]
task_dir: str FOCUS_FIELD_NUMBER: _ClassVar[int]
METADATA_FIELD_NUMBER: _ClassVar[int]
message_id: str
message_time: int
task_id: str task_id: str
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., internal_patch_id: _Optional[str] = ...) -> None: ... task_type: Task.TaskType
sources: _containers.RepeatedCompositeFieldContainer[SourceDetail]
class BuildRequest(_message.Message): deadline: int
__slots__ = ["apply_diff", "build_type", "engine", "internal_patch_id", "patch", "sanitizer", "task_dir", "task_id"] cancelled: bool
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int] project_name: str
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int] focus: str
ENGINE_FIELD_NUMBER: _ClassVar[int] metadata: _containers.ScalarMap[str, str]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int] def __init__(self, message_id: _Optional[str] = ..., message_time: _Optional[int] = ..., task_id: _Optional[str] = ..., task_type: _Optional[_Union[Task.TaskType, str]] = ..., sources: _Optional[_Iterable[_Union[SourceDetail, _Mapping]]] = ..., deadline: _Optional[int] = ..., cancelled: bool = ..., project_name: _Optional[str] = ..., focus: _Optional[str] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ...
PATCH_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
apply_diff: bool
build_type: BuildType
engine: str
internal_patch_id: str
patch: str
sanitizer: str
task_dir: str
task_id: str
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
class Bundle(_message.Message):
__slots__ = ["bundle_id", "competition_patch_id", "competition_pov_id", "competition_sarif_id", "task_id"]
BUNDLE_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_SARIF_ID_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
bundle_id: str
competition_patch_id: str
competition_pov_id: str
competition_sarif_id: str
task_id: str
def __init__(self, task_id: _Optional[str] = ..., competition_pov_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., competition_sarif_id: _Optional[str] = ..., bundle_id: _Optional[str] = ...) -> None: ...
class ConfirmedVulnerability(_message.Message):
__slots__ = ["crashes", "internal_patch_id"]
CRASHES_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
crashes: _containers.RepeatedCompositeFieldContainer[TracedCrash]
internal_patch_id: str
def __init__(self, crashes: _Optional[_Iterable[_Union[TracedCrash, _Mapping]]] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
class Crash(_message.Message):
__slots__ = ["crash_input_path", "crash_token", "harness_name", "stacktrace", "target"]
CRASH_INPUT_PATH_FIELD_NUMBER: _ClassVar[int]
CRASH_TOKEN_FIELD_NUMBER: _ClassVar[int]
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
STACKTRACE_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
crash_input_path: str
crash_token: str
harness_name: str
stacktrace: str
target: BuildOutput
def __init__(self, target: _Optional[_Union[BuildOutput, _Mapping]] = ..., harness_name: _Optional[str] = ..., crash_input_path: _Optional[str] = ..., stacktrace: _Optional[str] = ..., crash_token: _Optional[str] = ...) -> None: ...
class CrashWithId(_message.Message):
__slots__ = ["competition_pov_id", "crash", "result"]
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
CRASH_FIELD_NUMBER: _ClassVar[int]
RESULT_FIELD_NUMBER: _ClassVar[int]
competition_pov_id: str
crash: TracedCrash
result: SubmissionResult
def __init__(self, crash: _Optional[_Union[TracedCrash, _Mapping]] = ..., competition_pov_id: _Optional[str] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
class FunctionCoverage(_message.Message):
__slots__ = ["covered_lines", "function_name", "function_paths", "total_lines"]
COVERED_LINES_FIELD_NUMBER: _ClassVar[int]
FUNCTION_NAME_FIELD_NUMBER: _ClassVar[int]
FUNCTION_PATHS_FIELD_NUMBER: _ClassVar[int]
TOTAL_LINES_FIELD_NUMBER: _ClassVar[int]
covered_lines: int
function_name: str
function_paths: _containers.RepeatedScalarFieldContainer[str]
total_lines: int
def __init__(self, function_name: _Optional[str] = ..., function_paths: _Optional[_Iterable[str]] = ..., total_lines: _Optional[int] = ..., covered_lines: _Optional[int] = ...) -> None: ...
class IndexOutput(_message.Message):
__slots__ = ["build_type", "package_name", "sanitizer", "task_dir", "task_id"]
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
build_type: BuildType
package_name: str
sanitizer: str
task_dir: str
task_id: str
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
class IndexRequest(_message.Message):
__slots__ = ["build_type", "package_name", "sanitizer", "task_dir", "task_id"]
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
build_type: BuildType
package_name: str
sanitizer: str
task_dir: str
task_id: str
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
class POVReproduceRequest(_message.Message):
__slots__ = ["harness_name", "internal_patch_id", "pov_path", "sanitizer", "task_id"]
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
POV_PATH_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
harness_name: str
internal_patch_id: str
pov_path: str
sanitizer: str
task_id: str
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., harness_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., pov_path: _Optional[str] = ...) -> None: ...
class POVReproduceResponse(_message.Message):
__slots__ = ["did_crash", "request"]
DID_CRASH_FIELD_NUMBER: _ClassVar[int]
REQUEST_FIELD_NUMBER: _ClassVar[int]
did_crash: bool
request: POVReproduceRequest
def __init__(self, request: _Optional[_Union[POVReproduceRequest, _Mapping]] = ..., did_crash: bool = ...) -> None: ...
class Patch(_message.Message):
__slots__ = ["internal_patch_id", "patch", "task_id"]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
PATCH_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
internal_patch_id: str
patch: str
task_id: str
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., patch: _Optional[str] = ...) -> None: ...
class SourceDetail(_message.Message): class SourceDetail(_message.Message):
__slots__ = ["sha256", "source_type", "url"] __slots__ = ("sha256", "source_type", "url")
class SourceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): class SourceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = [] __slots__ = ()
SHA256_FIELD_NUMBER: _ClassVar[int] SOURCE_TYPE_REPO: _ClassVar[SourceDetail.SourceType]
SOURCE_TYPE_DIFF: SourceDetail.SourceType SOURCE_TYPE_FUZZ_TOOLING: _ClassVar[SourceDetail.SourceType]
SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int] SOURCE_TYPE_DIFF: _ClassVar[SourceDetail.SourceType]
SOURCE_TYPE_FUZZ_TOOLING: SourceDetail.SourceType
SOURCE_TYPE_REPO: SourceDetail.SourceType SOURCE_TYPE_REPO: SourceDetail.SourceType
SOURCE_TYPE_FUZZ_TOOLING: SourceDetail.SourceType
SOURCE_TYPE_DIFF: SourceDetail.SourceType
SHA256_FIELD_NUMBER: _ClassVar[int]
SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int]
URL_FIELD_NUMBER: _ClassVar[int] URL_FIELD_NUMBER: _ClassVar[int]
sha256: str sha256: str
source_type: SourceDetail.SourceType source_type: SourceDetail.SourceType
url: str url: str
def __init__(self, sha256: _Optional[str] = ..., source_type: _Optional[_Union[SourceDetail.SourceType, str]] = ..., url: _Optional[str] = ...) -> None: ... def __init__(self, sha256: _Optional[str] = ..., source_type: _Optional[_Union[SourceDetail.SourceType, str]] = ..., url: _Optional[str] = ...) -> None: ...
class SubmissionEntry(_message.Message):
__slots__ = ["bundles", "crashes", "patch_idx", "patch_submission_attempts", "patches", "stop"]
BUNDLES_FIELD_NUMBER: _ClassVar[int]
CRASHES_FIELD_NUMBER: _ClassVar[int]
PATCHES_FIELD_NUMBER: _ClassVar[int]
PATCH_IDX_FIELD_NUMBER: _ClassVar[int]
PATCH_SUBMISSION_ATTEMPTS_FIELD_NUMBER: _ClassVar[int]
STOP_FIELD_NUMBER: _ClassVar[int]
bundles: _containers.RepeatedCompositeFieldContainer[Bundle]
crashes: _containers.RepeatedCompositeFieldContainer[CrashWithId]
patch_idx: int
patch_submission_attempts: int
patches: _containers.RepeatedCompositeFieldContainer[SubmissionEntryPatch]
stop: bool
def __init__(self, stop: bool = ..., crashes: _Optional[_Iterable[_Union[CrashWithId, _Mapping]]] = ..., bundles: _Optional[_Iterable[_Union[Bundle, _Mapping]]] = ..., patches: _Optional[_Iterable[_Union[SubmissionEntryPatch, _Mapping]]] = ..., patch_idx: _Optional[int] = ..., patch_submission_attempts: _Optional[int] = ...) -> None: ...
class SubmissionEntryPatch(_message.Message):
__slots__ = ["build_outputs", "competition_patch_id", "internal_patch_id", "patch", "result"]
BUILD_OUTPUTS_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
PATCH_FIELD_NUMBER: _ClassVar[int]
RESULT_FIELD_NUMBER: _ClassVar[int]
build_outputs: _containers.RepeatedCompositeFieldContainer[BuildOutput]
competition_patch_id: str
internal_patch_id: str
patch: str
result: SubmissionResult
def __init__(self, patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., build_outputs: _Optional[_Iterable[_Union[BuildOutput, _Mapping]]] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
class Task(_message.Message):
__slots__ = ["cancelled", "deadline", "focus", "message_id", "message_time", "metadata", "project_name", "sources", "task_id", "task_type"]
class TaskType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
class MetadataEntry(_message.Message):
__slots__ = ["key", "value"]
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
CANCELLED_FIELD_NUMBER: _ClassVar[int]
DEADLINE_FIELD_NUMBER: _ClassVar[int]
FOCUS_FIELD_NUMBER: _ClassVar[int]
MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
MESSAGE_TIME_FIELD_NUMBER: _ClassVar[int]
METADATA_FIELD_NUMBER: _ClassVar[int]
PROJECT_NAME_FIELD_NUMBER: _ClassVar[int]
SOURCES_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
TASK_TYPE_DELTA: Task.TaskType
TASK_TYPE_FIELD_NUMBER: _ClassVar[int]
TASK_TYPE_FULL: Task.TaskType
cancelled: bool
deadline: int
focus: str
message_id: str
message_time: int
metadata: _containers.ScalarMap[str, str]
project_name: str
sources: _containers.RepeatedCompositeFieldContainer[SourceDetail]
task_id: str
task_type: Task.TaskType
def __init__(self, message_id: _Optional[str] = ..., message_time: _Optional[int] = ..., task_id: _Optional[str] = ..., task_type: _Optional[_Union[Task.TaskType, str]] = ..., sources: _Optional[_Iterable[_Union[SourceDetail, _Mapping]]] = ..., deadline: _Optional[int] = ..., cancelled: bool = ..., project_name: _Optional[str] = ..., focus: _Optional[str] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ...
class TaskDelete(_message.Message):
__slots__ = ["all", "received_at", "task_id"]
ALL_FIELD_NUMBER: _ClassVar[int]
RECEIVED_AT_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
all: bool
received_at: float
task_id: str
def __init__(self, task_id: _Optional[str] = ..., all: bool = ..., received_at: _Optional[float] = ...) -> None: ...
class TaskDownload(_message.Message): class TaskDownload(_message.Message):
__slots__ = ["task"] __slots__ = ("task",)
TASK_FIELD_NUMBER: _ClassVar[int] TASK_FIELD_NUMBER: _ClassVar[int]
task: Task task: Task
def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ... def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ...
class TaskReady(_message.Message): class TaskReady(_message.Message):
__slots__ = ["task"] __slots__ = ("task",)
TASK_FIELD_NUMBER: _ClassVar[int] TASK_FIELD_NUMBER: _ClassVar[int]
task: Task task: Task
def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ... def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ...
class TaskDelete(_message.Message):
__slots__ = ("task_id", "all", "received_at")
TASK_ID_FIELD_NUMBER: _ClassVar[int]
ALL_FIELD_NUMBER: _ClassVar[int]
RECEIVED_AT_FIELD_NUMBER: _ClassVar[int]
task_id: str
all: bool
received_at: float
def __init__(self, task_id: _Optional[str] = ..., all: bool = ..., received_at: _Optional[float] = ...) -> None: ...
class BuildRequest(_message.Message):
__slots__ = ("engine", "sanitizer", "task_dir", "task_id", "build_type", "apply_diff", "patch", "internal_patch_id")
ENGINE_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
PATCH_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
engine: str
sanitizer: str
task_dir: str
task_id: str
build_type: BuildType
apply_diff: bool
patch: str
internal_patch_id: str
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
class BuildOutput(_message.Message):
__slots__ = ("engine", "sanitizer", "task_dir", "task_id", "build_type", "apply_diff", "internal_patch_id")
ENGINE_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
engine: str
sanitizer: str
task_dir: str
task_id: str
build_type: BuildType
apply_diff: bool
internal_patch_id: str
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
class WeightedHarness(_message.Message):
__slots__ = ("weight", "package_name", "harness_name", "task_id")
WEIGHT_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
weight: float
package_name: str
harness_name: str
task_id: str
def __init__(self, weight: _Optional[float] = ..., package_name: _Optional[str] = ..., harness_name: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
class Crash(_message.Message):
__slots__ = ("target", "harness_name", "crash_input_path", "stacktrace", "crash_token")
TARGET_FIELD_NUMBER: _ClassVar[int]
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
CRASH_INPUT_PATH_FIELD_NUMBER: _ClassVar[int]
STACKTRACE_FIELD_NUMBER: _ClassVar[int]
CRASH_TOKEN_FIELD_NUMBER: _ClassVar[int]
target: BuildOutput
harness_name: str
crash_input_path: str
stacktrace: str
crash_token: str
def __init__(self, target: _Optional[_Union[BuildOutput, _Mapping]] = ..., harness_name: _Optional[str] = ..., crash_input_path: _Optional[str] = ..., stacktrace: _Optional[str] = ..., crash_token: _Optional[str] = ...) -> None: ...
class TracedCrash(_message.Message): class TracedCrash(_message.Message):
__slots__ = ["crash", "tracer_stacktrace"] __slots__ = ("crash", "tracer_stacktrace")
CRASH_FIELD_NUMBER: _ClassVar[int] CRASH_FIELD_NUMBER: _ClassVar[int]
TRACER_STACKTRACE_FIELD_NUMBER: _ClassVar[int] TRACER_STACKTRACE_FIELD_NUMBER: _ClassVar[int]
crash: Crash crash: Crash
tracer_stacktrace: str tracer_stacktrace: str
def __init__(self, crash: _Optional[_Union[Crash, _Mapping]] = ..., tracer_stacktrace: _Optional[str] = ...) -> None: ... def __init__(self, crash: _Optional[_Union[Crash, _Mapping]] = ..., tracer_stacktrace: _Optional[str] = ...) -> None: ...
class WeightedHarness(_message.Message): class ConfirmedVulnerability(_message.Message):
__slots__ = ["harness_name", "package_name", "task_id", "weight"] __slots__ = ("crashes", "internal_patch_id")
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int] CRASHES_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int] INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
crashes: _containers.RepeatedCompositeFieldContainer[TracedCrash]
internal_patch_id: str
def __init__(self, crashes: _Optional[_Iterable[_Union[TracedCrash, _Mapping]]] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
class Patch(_message.Message):
__slots__ = ("task_id", "internal_patch_id", "patch")
TASK_ID_FIELD_NUMBER: _ClassVar[int] TASK_ID_FIELD_NUMBER: _ClassVar[int]
WEIGHT_FIELD_NUMBER: _ClassVar[int] INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
harness_name: str PATCH_FIELD_NUMBER: _ClassVar[int]
package_name: str
task_id: str task_id: str
weight: float internal_patch_id: str
def __init__(self, weight: _Optional[float] = ..., package_name: _Optional[str] = ..., harness_name: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ... patch: str
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., patch: _Optional[str] = ...) -> None: ...
class BuildType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): class IndexRequest(_message.Message):
__slots__ = [] __slots__ = ("build_type", "package_name", "sanitizer", "task_dir", "task_id")
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
build_type: BuildType
package_name: str
sanitizer: str
task_dir: str
task_id: str
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
class SubmissionResult(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): class IndexOutput(_message.Message):
__slots__ = [] __slots__ = ("build_type", "package_name", "sanitizer", "task_dir", "task_id")
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
build_type: BuildType
package_name: str
sanitizer: str
task_dir: str
task_id: str
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
class FunctionCoverage(_message.Message):
__slots__ = ("function_name", "function_paths", "total_lines", "covered_lines")
FUNCTION_NAME_FIELD_NUMBER: _ClassVar[int]
FUNCTION_PATHS_FIELD_NUMBER: _ClassVar[int]
TOTAL_LINES_FIELD_NUMBER: _ClassVar[int]
COVERED_LINES_FIELD_NUMBER: _ClassVar[int]
function_name: str
function_paths: _containers.RepeatedScalarFieldContainer[str]
total_lines: int
covered_lines: int
def __init__(self, function_name: _Optional[str] = ..., function_paths: _Optional[_Iterable[str]] = ..., total_lines: _Optional[int] = ..., covered_lines: _Optional[int] = ...) -> None: ...
class SubmissionEntryPatch(_message.Message):
__slots__ = ("patch", "internal_patch_id", "competition_patch_id", "build_outputs", "result")
PATCH_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
BUILD_OUTPUTS_FIELD_NUMBER: _ClassVar[int]
RESULT_FIELD_NUMBER: _ClassVar[int]
patch: str
internal_patch_id: str
competition_patch_id: str
build_outputs: _containers.RepeatedCompositeFieldContainer[BuildOutput]
result: SubmissionResult
def __init__(self, patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., build_outputs: _Optional[_Iterable[_Union[BuildOutput, _Mapping]]] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
class Bundle(_message.Message):
__slots__ = ("task_id", "competition_pov_id", "competition_patch_id", "competition_sarif_id", "bundle_id")
TASK_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_SARIF_ID_FIELD_NUMBER: _ClassVar[int]
BUNDLE_ID_FIELD_NUMBER: _ClassVar[int]
task_id: str
competition_pov_id: str
competition_patch_id: str
competition_sarif_id: str
bundle_id: str
def __init__(self, task_id: _Optional[str] = ..., competition_pov_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., competition_sarif_id: _Optional[str] = ..., bundle_id: _Optional[str] = ...) -> None: ...
class CrashWithId(_message.Message):
__slots__ = ("crash", "competition_pov_id", "result")
CRASH_FIELD_NUMBER: _ClassVar[int]
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
RESULT_FIELD_NUMBER: _ClassVar[int]
crash: TracedCrash
competition_pov_id: str
result: SubmissionResult
def __init__(self, crash: _Optional[_Union[TracedCrash, _Mapping]] = ..., competition_pov_id: _Optional[str] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
class SubmissionEntry(_message.Message):
__slots__ = ("stop", "crashes", "bundles", "patches", "patch_idx", "patch_submission_attempts")
STOP_FIELD_NUMBER: _ClassVar[int]
CRASHES_FIELD_NUMBER: _ClassVar[int]
BUNDLES_FIELD_NUMBER: _ClassVar[int]
PATCHES_FIELD_NUMBER: _ClassVar[int]
PATCH_IDX_FIELD_NUMBER: _ClassVar[int]
PATCH_SUBMISSION_ATTEMPTS_FIELD_NUMBER: _ClassVar[int]
stop: bool
crashes: _containers.RepeatedCompositeFieldContainer[CrashWithId]
bundles: _containers.RepeatedCompositeFieldContainer[Bundle]
patches: _containers.RepeatedCompositeFieldContainer[SubmissionEntryPatch]
patch_idx: int
patch_submission_attempts: int
def __init__(self, stop: bool = ..., crashes: _Optional[_Iterable[_Union[CrashWithId, _Mapping]]] = ..., bundles: _Optional[_Iterable[_Union[Bundle, _Mapping]]] = ..., patches: _Optional[_Iterable[_Union[SubmissionEntryPatch, _Mapping]]] = ..., patch_idx: _Optional[int] = ..., patch_submission_attempts: _Optional[int] = ...) -> None: ...
class POVReproduceRequest(_message.Message):
__slots__ = ("task_id", "internal_patch_id", "harness_name", "sanitizer", "pov_path")
TASK_ID_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
POV_PATH_FIELD_NUMBER: _ClassVar[int]
task_id: str
internal_patch_id: str
harness_name: str
sanitizer: str
pov_path: str
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., harness_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., pov_path: _Optional[str] = ...) -> None: ...
class POVReproduceResponse(_message.Message):
__slots__ = ("request", "did_crash")
REQUEST_FIELD_NUMBER: _ClassVar[int]
DID_CRASH_FIELD_NUMBER: _ClassVar[int]
request: POVReproduceRequest
did_crash: bool
def __init__(self, request: _Optional[_Union[POVReproduceRequest, _Mapping]] = ..., did_crash: bool = ...) -> None: ...
+33 -27
View File
@@ -1,19 +1,24 @@
import os import os
from opentelemetry._logs import set_logger_provider try:
from opentelemetry._logs import set_logger_provider
if os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") == "grpc": if os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") == "grpc":
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
else: else:
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter # type: ignore from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter # type: ignore
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
_opentelemetry_enabled = True
except ImportError:
_opentelemetry_enabled = False
import logging import logging
import tempfile import tempfile
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from buttercup.common.telemetry import crs_instance_id, service_instance_id from buttercup.common.telemetry import crs_instance_id, service_instance_id
_is_initialized = False _is_initialized = False
@@ -48,26 +53,27 @@ def setup_package_logger(
root.removeHandler(handler) root.removeHandler(handler)
# Create resource with service and environment information # Create resource with service and environment information
resource = Resource.create(
attributes={
"service.name": application_name,
"service.instance.id": service_instance_id,
"crs.instance.id": crs_instance_id,
},
)
# Initialize the LoggerProvider with the created resource.
logger_provider = LoggerProvider(resource=resource)
# Configure the span exporter and processor based on whether the endpoint is effectively set.
otlp_handler = None otlp_handler = None
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"): if _opentelemetry_enabled:
set_logger_provider(logger_provider) resource = Resource.create(
exporter = OTLPLogExporter() attributes={
"service.name": application_name,
"service.instance.id": service_instance_id,
"crs.instance.id": crs_instance_id,
}
)
# add the batch processors to the trace provider # Initialize the LoggerProvider with the created resource.
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) logger_provider = LoggerProvider(resource=resource)
otlp_handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
# Configure the span exporter and processor based on whether the endpoint is effectively set.
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
set_logger_provider(logger_provider)
exporter = OTLPLogExporter()
# add the batch processors to the trace provider
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
otlp_handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
persistent_log_dir = os.getenv("PERSISTENT_LOG_DIR", None) persistent_log_dir = os.getenv("PERSISTENT_LOG_DIR", None)
-16
View File
@@ -444,19 +444,3 @@ class QueueFactory:
queue_args.update(kwargs) queue_args.update(kwargs)
return ReliableQueue(**queue_args) # type: ignore[arg-type] return ReliableQueue(**queue_args) # type: ignore[arg-type]
@dataclass
class FuzzConfiguration:
corpus_dir: str
target_path: str
engine: str
sanitizer: str
@dataclass
class BuildConfiguration:
project_id: str
engine: str
sanitizer: str
source_path: str | None
+24 -21
View File
@@ -4,28 +4,22 @@ import uuid
from enum import Enum from enum import Enum
from typing import Any from typing import Any
import openlit
import opentelemetry.attributes
from langchain_core.prompt_values import ChatPromptValue
from opentelemetry import trace
from opentelemetry.trace import Span, Status, StatusCode, Tracer
# Monkey patch the _clean_attribute function to handle ChatPromptValue
_clean_attribute_orig = opentelemetry.attributes._clean_attribute
def _clean_attribute_wrapper(key: str, value: Any, max_len: int | None = None) -> Any:
"""Wrapper around _clean_attribute to add custom behavior"""
if isinstance(value, ChatPromptValue):
value = value.to_string()
return _clean_attribute_orig(key, value, max_len)
opentelemetry.attributes._clean_attribute = _clean_attribute_wrapper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try:
import openlit
from opentelemetry import trace
from opentelemetry.trace import Span, Status, StatusCode, Tracer
_opentelemetry_enabled = True
except ImportError:
logger.warning("OpenTelemetry is not installed, skipping telemetry")
Span: type = Any # type: ignore[no-redef]
Tracer: type = Any # type: ignore[no-redef]
_opentelemetry_enabled = False
crs_instance_id = os.getenv("CRS_INSTANCE_ID", str(uuid.uuid4())) crs_instance_id = os.getenv("CRS_INSTANCE_ID", str(uuid.uuid4()))
service_instance_id = str(uuid.uuid4()) service_instance_id = str(uuid.uuid4())
@@ -47,6 +41,9 @@ class CRSActionCategory(Enum):
def init_telemetry(application_name: str) -> None: def init_telemetry(application_name: str) -> None:
"""Initialize the telemetry for the application.""" """Initialize the telemetry for the application."""
if not _opentelemetry_enabled:
return
logger.info("Initializing telemetry for %s", application_name) logger.info("Initializing telemetry for %s", application_name)
if not os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"): if not os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
logger.error("OTEL_EXPORTER_OTLP_ENDPOINT not set, disabling telemetry") logger.error("OTEL_EXPORTER_OTLP_ENDPOINT not set, disabling telemetry")
@@ -75,6 +72,9 @@ def set_crs_attributes(
task_metadata: dict, task_metadata: dict,
extra_attributes: dict | None = None, extra_attributes: dict | None = None,
) -> None: ) -> None:
if not _opentelemetry_enabled:
return
extra_attributes = extra_attributes or {} extra_attributes = extra_attributes or {}
span.set_attribute("crs.action.category", crs_action_category.value) span.set_attribute("crs.action.category", crs_action_category.value)
span.set_attribute("crs.action.name", crs_action_name) span.set_attribute("crs.action.name", crs_action_name)
@@ -95,6 +95,9 @@ def log_crs_action_ok(
task_metadata: dict, task_metadata: dict,
extra_attributes: dict | None = None, extra_attributes: dict | None = None,
) -> None: ) -> None:
if not _opentelemetry_enabled:
return
extra_attributes = extra_attributes or {} extra_attributes = extra_attributes or {}
with tracer.start_as_current_span(crs_action_name) as span: with tracer.start_as_current_span(crs_action_name) as span:
span.set_attribute("crs.action.category", crs_action_category.value) span.set_attribute("crs.action.category", crs_action_category.value)
+9
View File
@@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass
class FuzzConfiguration:
corpus_dir: str
target_path: str
engine: str
sanitizer: str
+3 -1
View File
@@ -25,7 +25,9 @@ def copyanything(src: PathLike, dst: PathLike, **kwargs: Any) -> None:
""" """
src, dst = Path(src), Path(dst) src, dst = Path(src), Path(dst)
try: try:
shutil.copytree(src, dst, dirs_exist_ok=True, **kwargs) shutil.copytree(src, dst, dirs_exist_ok=True, ignore_dangling_symlinks=True, **kwargs)
except shutil.Error:
logger.exception(f"Some errors occurred while copying {src} to {dst}, continuing anyway...")
except OSError as exc: # python >2.5 except OSError as exc: # python >2.5
if exc.errno in (errno.ENOTDIR, errno.EINVAL): if exc.errno in (errno.ENOTDIR, errno.EINVAL):
shutil.copy(src, dst) shutil.copy(src, dst)
+509 -405
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -134,7 +134,7 @@ up() {
fi fi
docker build $ORCHESTRATOR_BUILD_ARGS -f "$SCRIPT_DIR"/../orchestrator/Dockerfile -t localhost/orchestrator:latest "$SCRIPT_DIR"/.. docker build $ORCHESTRATOR_BUILD_ARGS -f "$SCRIPT_DIR"/../orchestrator/Dockerfile -t localhost/orchestrator:latest "$SCRIPT_DIR"/..
docker build $FUZZER_BUILD_ARGS -f "$SCRIPT_DIR"/../fuzzer/dockerfiles/runner_image.Dockerfile -t localhost/fuzzer:latest "$SCRIPT_DIR"/.. docker build $FUZZER_BUILD_ARGS -f "$SCRIPT_DIR"/../fuzzer/Dockerfile -t localhost/fuzzer:latest "$SCRIPT_DIR"/..
docker build $SEED_GEN_BUILD_ARGS -f "$SCRIPT_DIR"/../seed-gen/Dockerfile -t localhost/seed-gen:latest "$SCRIPT_DIR"/.. docker build $SEED_GEN_BUILD_ARGS -f "$SCRIPT_DIR"/../seed-gen/Dockerfile -t localhost/seed-gen:latest "$SCRIPT_DIR"/..
docker build $PATCHER_BUILD_ARGS -f "$SCRIPT_DIR"/../patcher/Dockerfile -t localhost/patcher:latest "$SCRIPT_DIR"/.. docker build $PATCHER_BUILD_ARGS -f "$SCRIPT_DIR"/../patcher/Dockerfile -t localhost/patcher:latest "$SCRIPT_DIR"/..
docker build $PROGRAM_MODEL_BUILD_ARGS -f "$SCRIPT_DIR"/../program-model/Dockerfile -t localhost/program-model:latest "$SCRIPT_DIR"/.. docker build $PROGRAM_MODEL_BUILD_ARGS -f "$SCRIPT_DIR"/../program-model/Dockerfile -t localhost/program-model:latest "$SCRIPT_DIR"/..
@@ -21,7 +21,7 @@ spec:
- name: merger-bot - name: merger-bot
image: "{{ .Values.global.fuzzerImage.repository }}:{{ .Values.global.fuzzerImage.tag }}" image: "{{ .Values.global.fuzzerImage.repository }}:{{ .Values.global.fuzzerImage.tag }}"
imagePullPolicy: {{ .Values.global.fuzzerImage.pullPolicy }} imagePullPolicy: {{ .Values.global.fuzzerImage.pullPolicy }}
command: ["buttercup-corpus-merger", "--crs_scratch_dir", "{{ include "buttercup.nodeLocalCrsScratchPath" . }}", "--redis_url", "{{ include "buttercup.core.redisUrl" . }}", "--timer", "{{ .Values.timer }}", "--timeout", "{{ .Values.timeout }}", "--crash_dir_count_limit", "{{ include "buttercup.core.crashDirCountLimit" . }}", "--max_local_files", "{{ .Values.max_local_files }}"] command: ["buttercup-corpus-merger", "--crs_scratch_dir", "{{ include "buttercup.nodeLocalCrsScratchPath" . }}", "--redis_url", "{{ include "buttercup.core.redisUrl" . }}", "--timer", "{{ .Values.timer }}", "--timeout", "{{ .Values.timeout }}", "--crash_dir_count_limit", "{{ include "buttercup.core.crashDirCountLimit" . }}", "--max_local_files", "{{ .Values.max_local_files }}", "--runner_path", "/app/fuzzer_runner/runner.sh"]
env: env:
{{- include "buttercup.env.nodeData" . | nindent 8 }} {{- include "buttercup.env.nodeData" . | nindent 8 }}
{{- include "buttercup.env.telemetry" . | nindent 8 }} {{- include "buttercup.env.telemetry" . | nindent 8 }}
+2
View File
@@ -336,6 +336,8 @@ Service-specific environment variables that utilize the standardized variables
value: "{{ include "buttercup.core.logMaxLineLength" . }}" value: "{{ include "buttercup.core.logMaxLineLength" . }}"
- name: BUTTERCUP_FUZZER_MAX_POV_SIZE - name: BUTTERCUP_FUZZER_MAX_POV_SIZE
value: {{ int .Values.global.maxPovSize | quote }} value: {{ int .Values.global.maxPovSize | quote }}
- name: BUTTERCUP_FUZZER_RUNNER_PATH
value: "/app/fuzzer_runner/runner.sh"
{{- end }} {{- end }}
{{- define "buttercup.povReproducerEnv" }} {{- define "buttercup.povReproducerEnv" }}
+8 -8
View File
@@ -40,7 +40,7 @@ services:
stimulate-fuzzer-test: stimulate-fuzzer-test:
build: build:
context: ../../ context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
command: /fuzzer/.venv/bin/python -m buttercup.fuzzing_infra.stimulate_build_bot --build_type fuzzer --ossfuzz /crs_scratch/oss-fuzz --engine libfuzzer --sanitizer address --target_package ${TARGET_PACKAGE} --redis_url redis://redis:6379 --source_path /crs_scratch/${TARGET_PACKAGE} --task_id 123e4567-e89b-12d3-a456-426614174000 command: /fuzzer/.venv/bin/python -m buttercup.fuzzing_infra.stimulate_build_bot --build_type fuzzer --ossfuzz /crs_scratch/oss-fuzz --engine libfuzzer --sanitizer address --target_package ${TARGET_PACKAGE} --redis_url redis://redis:6379 --source_path /crs_scratch/${TARGET_PACKAGE} --task_id 123e4567-e89b-12d3-a456-426614174000
profiles: profiles:
- fuzzer-test - fuzzer-test
@@ -63,7 +63,7 @@ services:
orchestrator-sim: orchestrator-sim:
build: build:
context: ../../ context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
command: /fuzzer/.venv/bin/python -m buttercup.fuzzing_infra.orchestrator --redis_url redis://redis:6379 command: /fuzzer/.venv/bin/python -m buttercup.fuzzing_infra.orchestrator --redis_url redis://redis:6379
profiles: profiles:
- fuzzer-test - fuzzer-test
@@ -140,7 +140,7 @@ services:
command: ["buttercup-coverage-bot", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379"] command: ["buttercup-coverage-bot", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379"]
build: build:
context: ../../ context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
volumes: volumes:
- ../../crs_scratch:/crs_scratch - ../../crs_scratch:/crs_scratch
- ../../tasks_storage:/tasks_storage - ../../tasks_storage:/tasks_storage
@@ -156,7 +156,7 @@ services:
command: ["buttercup-fuzzer-builder", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379", "--timer", "5000"] command: ["buttercup-fuzzer-builder", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379", "--timer", "5000"]
build: build:
context: ../../ context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
volumes: volumes:
- ../../crs_scratch:/crs_scratch - ../../crs_scratch:/crs_scratch
- ../../tasks_storage:/tasks_storage - ../../tasks_storage:/tasks_storage
@@ -172,7 +172,7 @@ services:
command: ["buttercup-tracer-bot", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379"] command: ["buttercup-tracer-bot", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379"]
build: build:
context: ../../ context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
volumes: volumes:
- ../../crs_scratch:/crs_scratch - ../../crs_scratch:/crs_scratch
- ../../tasks_storage:/tasks_storage - ../../tasks_storage:/tasks_storage
@@ -187,8 +187,8 @@ services:
fuzzer-bot: fuzzer-bot:
build: build:
context: ../../ context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile dockerfile: ./fuzzer/Dockerfile
command: ["buttercup-fuzzer", "--crs_scratch_dir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379", "--timeout", "900", "--timer", "5000", "--crash_dir_count_limit", "100"] command: ["buttercup-fuzzer"]
env_file: env.dev.compose env_file: env.dev.compose
volumes: volumes:
- ../../crs_scratch:/crs_scratch - ../../crs_scratch:/crs_scratch
@@ -312,7 +312,7 @@ services:
dockerfile: ./orchestrator/Dockerfile dockerfile: ./orchestrator/Dockerfile
command: ["buttercup-ui"] command: ["buttercup-ui"]
ports: ports:
- "127.0.0.1:1323:1323" - "127.0.0.1:31323:1323"
env_file: env.dev.compose env_file: env.dev.compose
+6
View File
@@ -46,10 +46,15 @@ BUTTERCUP_PROGRAM_MODEL_LOG_LEVEL=debug
BUTTERCUP_PROGRAM_MODEL_SCRATCH_DIR=/node_data/crs_scratch BUTTERCUP_PROGRAM_MODEL_SCRATCH_DIR=/node_data/crs_scratch
BUTTERCUP_PROGRAM_MODEL_GRAPHDB_ENABLED=false BUTTERCUP_PROGRAM_MODEL_GRAPHDB_ENABLED=false
BUTTERCUP_FUZZER_REDIS_URL=redis://redis:6379
BUTTERCUP_FUZZER_WDIR=/node_data/crs_scratch BUTTERCUP_FUZZER_WDIR=/node_data/crs_scratch
BUTTERCUP_FUZZER_CRS_SCRATCH_DIR=/node_data/crs_scratch
BUTTERCUP_FUZZER_CRASH_DIR_COUNT_LIMIT=100
BUTTERCUP_FUZZER_SAMPLE_SIZE=200 BUTTERCUP_FUZZER_SAMPLE_SIZE=200
BUTTERCUP_FUZZER_LOG_LEVEL=debug BUTTERCUP_FUZZER_LOG_LEVEL=debug
BUTTERCUP_FUZZER_MAX_POV_SIZE=2097152 BUTTERCUP_FUZZER_MAX_POV_SIZE=2097152
BUTTERCUP_FUZZER_RUNNER_PATH=/app/fuzzer_runner/runner.sh
BUTTERCUP_FUZZER_TIMEOUT=60
BUTTERCUP_SEED_GEN_LOG_LEVEL=debug BUTTERCUP_SEED_GEN_LOG_LEVEL=debug
BUTTERCUP_SEED_GEN_SERVER__REDIS_URL=redis://redis:6379 BUTTERCUP_SEED_GEN_SERVER__REDIS_URL=redis://redis:6379
@@ -86,6 +91,7 @@ OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
OSS_FUZZ_CONTAINER_ORG=aixcc-afc OSS_FUZZ_CONTAINER_ORG=aixcc-afc
BUTTERCUP_UI_HOST=buttercup-ui BUTTERCUP_UI_HOST=buttercup-ui
BUTTERCUP_UI_EXTERNAL_HOST=buttercup-ui
BUTTERCUP_UI_CRS_BASE_URL="http://task-server:8000" BUTTERCUP_UI_CRS_BASE_URL="http://task-server:8000"
BUTTERCUP_UI_STORAGE_DIR="/tmp/buttercup-ui" BUTTERCUP_UI_STORAGE_DIR="/tmp/buttercup-ui"
BUTTERCUP_UI_LOG_LEVEL="info" BUTTERCUP_UI_LOG_LEVEL="info"
+68
View File
@@ -0,0 +1,68 @@
ARG BASE_IMAGE=gcr.io/oss-fuzz-base/base-runner
FROM $BASE_IMAGE AS base-image
COPY --from=ghcr.io/astral-sh/uv:0.5.20 /uv /uvx /bin/
ENV UV_LINK_MODE=copy
ENV UV_COMPILE_BYTECODE=1
ENV UV_PYTHON_DOWNLOADS=manual
RUN uv python install python3.12
FROM base-image AS runner-base
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN install -m 0755 -d /etc/apt/keyrings
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
RUN chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
RUN echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
&& rm -rf /var/lib/apt/lists/*
FROM base-image AS builder
WORKDIR /app
# Install dependencies for fuzzer-bot
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=common/uv.lock,target=common/uv.lock \
--mount=type=bind,source=common/pyproject.toml,target=common/pyproject.toml \
--mount=type=bind,source=common/README.md,target=common/README.md \
--mount=type=bind,source=fuzzer/uv.lock,target=fuzzer/uv.lock \
--mount=type=bind,source=fuzzer/pyproject.toml,target=fuzzer/pyproject.toml \
--mount=type=bind,source=fuzzer_runner/uv.lock,target=fuzzer_runner/uv.lock \
--mount=type=bind,source=fuzzer_runner/pyproject.toml,target=fuzzer_runner/pyproject.toml \
cd fuzzer && uv sync --frozen --no-install-project --no-editable && cd .. \
&& cd fuzzer_runner && uv sync --frozen --no-install-project --no-editable && cd ..
ADD common /app/common
ADD fuzzer /app/fuzzer
ADD fuzzer_runner /app/fuzzer_runner
RUN --mount=type=cache,target=/root/.cache/uv \
cd fuzzer && uv sync --frozen --no-editable
RUN --mount=type=cache,target=/root/.cache/uv \
cd fuzzer_runner && uv sync --frozen --no-editable
FROM runner-base AS runtime
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y git \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder --chown=app:app /app/fuzzer/.venv /app/fuzzer/.venv
COPY --from=builder --chown=app:app /app/fuzzer_runner/.venv /app/fuzzer_runner/.venv
COPY common/container-entrypoint.sh /container-entrypoint.sh
COPY fuzzer_runner/runner.sh /app/fuzzer_runner/runner.sh
ENV PATH=/app/fuzzer/.venv/bin:$PATH
ENTRYPOINT ["/container-entrypoint.sh"]
@@ -1,59 +0,0 @@
ARG BASE_IMAGE=gcr.io/oss-fuzz-base/base-runner
FROM $BASE_IMAGE AS base-image
COPY --from=ghcr.io/astral-sh/uv:0.5.20 /uv /uvx /bin/
ENV UV_LINK_MODE=copy
ENV UV_COMPILE_BYTECODE=1
ENV UV_PYTHON_DOWNLOADS=manual
RUN uv python install python3.12
FROM base-image AS runner-base
RUN apt-get update
# TODO(Ian): maybe we should have a different base image for the builder
RUN apt-get install -y ca-certificates curl
RUN install -m 0755 -d /etc/apt/keyrings
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
RUN chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
RUN echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN apt-get update
RUN apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
FROM base-image AS builder
WORKDIR /fuzzer
# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=common/uv.lock,target=/common/uv.lock \
--mount=type=bind,source=common/pyproject.toml,target=/common/pyproject.toml \
--mount=type=bind,source=common/README.md,target=/common/README.md \
--mount=type=bind,source=fuzzer/uv.lock,target=/fuzzer/uv.lock \
--mount=type=bind,source=fuzzer/pyproject.toml,target=/fuzzer/pyproject.toml \
cd /fuzzer && uv sync --frozen --no-install-project --no-editable
ADD ./common /common
ADD ./fuzzer /fuzzer
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-editable
FROM runner-base AS runtime
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y git \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder --chown=app:app /fuzzer/.venv /fuzzer/.venv
COPY common/container-entrypoint.sh /container-entrypoint.sh
ENV PATH=/fuzzer/.venv/bin:$PATH
ENTRYPOINT ["/container-entrypoint.sh"]
+1 -3
View File
@@ -6,10 +6,8 @@ authors = [{ name = "Trail of Bits", email = "opensource@trailofbits.com" }]
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
requires-python = ">=3.12,<3.13" requires-python = ">=3.12,<3.13"
dependencies = [ dependencies = [
"protobuf ==3.20.3", # Pin exactly for stability "common[full]",
"redis ~=5.2.1", "redis ~=5.2.1",
"clusterfuzz ==2.6.0", # Pin exactly for stability
"common",
"pydantic-settings ~=2.7.1", "pydantic-settings ~=2.7.1",
"beautifulsoup4 ~=4.13.3", "beautifulsoup4 ~=4.13.3",
"lxml ~=5.3.1", "lxml ~=5.3.1",
@@ -5,6 +5,7 @@ import random
import shutil import shutil
from dataclasses import dataclass from dataclasses import dataclass
from os import PathLike from os import PathLike
from pathlib import Path
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode from opentelemetry.trace import Status, StatusCode
@@ -21,7 +22,7 @@ from buttercup.common.maps import BuildMap, HarnessWeights
from buttercup.common.sets import MERGING_LOCK_TIMEOUT_SECONDS, FailedToAcquireLock, MergedCorpusSetLock from buttercup.common.sets import MERGING_LOCK_TIMEOUT_SECONDS, FailedToAcquireLock, MergedCorpusSetLock
from buttercup.common.telemetry import CRSActionCategory, init_telemetry, set_crs_attributes from buttercup.common.telemetry import CRSActionCategory, init_telemetry, set_crs_attributes
from buttercup.common.utils import serve_loop, setup_periodic_zombie_reaper from buttercup.common.utils import serve_loop, setup_periodic_zombie_reaper
from buttercup.fuzzing_infra.runner import Conf, FuzzConfiguration, Runner from buttercup.fuzzing_infra.runner_proxy import Conf, FuzzConfiguration, RunnerProxy
from buttercup.fuzzing_infra.settings import FuzzerBotSettings from buttercup.fuzzing_infra.settings import FuzzerBotSettings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -184,10 +185,11 @@ class MergerBot:
timeout_seconds: int, timeout_seconds: int,
python: str, python: str,
crs_scratch_dir: str, crs_scratch_dir: str,
runner_path: PathLike[str],
max_local_files: int = 500, max_local_files: int = 500,
): ):
self.redis = redis self.redis = redis
self.runner = Runner(Conf(timeout_seconds)) self.runner = RunnerProxy(Conf(timeout_seconds, Path(runner_path)))
self.python = python self.python = python
self.crs_scratch_dir = crs_scratch_dir self.crs_scratch_dir = crs_scratch_dir
self.harness_weights = HarnessWeights(redis) self.harness_weights = HarnessWeights(redis)
@@ -401,6 +403,7 @@ def main() -> None:
args.timeout, args.timeout,
args.python, args.python,
args.crs_scratch_dir, args.crs_scratch_dir,
args.runner_path,
args.max_local_files, args.max_local_files,
) )
merger.run() merger.run()
@@ -1,8 +1,8 @@
import logging import logging
import random import random
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from clusterfuzz.fuzz import engine
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.trace.status import Status, StatusCode from opentelemetry.trace.status import Status, StatusCode
from redis import Redis from redis import Redis
@@ -18,10 +18,14 @@ from buttercup.common.node_local import scratch_dir
from buttercup.common.queues import QueueFactory, QueueNames from buttercup.common.queues import QueueFactory, QueueNames
from buttercup.common.stack_parsing import CrashSet from buttercup.common.stack_parsing import CrashSet
from buttercup.common.telemetry import CRSActionCategory, init_telemetry, set_crs_attributes from buttercup.common.telemetry import CRSActionCategory, init_telemetry, set_crs_attributes
from buttercup.common.types import FuzzConfiguration
from buttercup.common.utils import setup_periodic_zombie_reaper from buttercup.common.utils import setup_periodic_zombie_reaper
from buttercup.fuzzing_infra.runner import Conf, FuzzConfiguration, Runner from buttercup.fuzzing_infra.runner_proxy import Conf, RunnerProxy
from buttercup.fuzzing_infra.settings import FuzzerBotSettings from buttercup.fuzzing_infra.settings import FuzzerBotSettings
if TYPE_CHECKING:
from clusterfuzz.fuzz import engine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,8 +39,9 @@ class FuzzerBot(TaskLoop):
crs_scratch_dir: str, crs_scratch_dir: str,
crash_dir_count_limit: int | None, crash_dir_count_limit: int | None,
max_pov_size: int, max_pov_size: int,
runner_path: Path,
): ):
self.runner = Runner(Conf(timeout_seconds)) self.runner = RunnerProxy(Conf(timeout_seconds, runner_path))
self.output_q = QueueFactory(redis).create(QueueNames.CRASH) self.output_q = QueueFactory(redis).create(QueueNames.CRASH)
self.python = python self.python = python
self.crs_scratch_dir = crs_scratch_dir self.crs_scratch_dir = crs_scratch_dir
@@ -141,6 +146,7 @@ def main() -> None:
setup_periodic_zombie_reaper() setup_periodic_zombie_reaper()
logger.info(f"Starting fuzzer (crs_scratch_dir: {args.crs_scratch_dir})") logger.info(f"Starting fuzzer (crs_scratch_dir: {args.crs_scratch_dir})")
logger.debug("Args: %s", args)
seconds_sleep = args.timer // 1000 seconds_sleep = args.timer // 1000
fuzzer = FuzzerBot( fuzzer = FuzzerBot(
@@ -151,6 +157,7 @@ def main() -> None:
args.crs_scratch_dir, args.crs_scratch_dir,
crash_dir_count_limit=(args.crash_dir_count_limit if args.crash_dir_count_limit > 0 else None), crash_dir_count_limit=(args.crash_dir_count_limit if args.crash_dir_count_limit > 0 else None),
max_pov_size=args.max_pov_size, max_pov_size=args.max_pov_size,
runner_path=args.runner_path,
) )
fuzzer.run() fuzzer.run()
@@ -0,0 +1,215 @@
import json
import logging
import os
import signal
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from buttercup.common.types import FuzzConfiguration
logger = logging.getLogger(__name__)
@dataclass
class Crash:
input_path: str
stacktrace: str
reproduce_args: list[str]
crash_time: float
@dataclass
class FuzzResult:
"""Result from a fuzzing operation"""
logs: str
command: str
crashes: list[Crash]
stats: dict
time_executed: float
timed_out: bool
@dataclass
class Conf:
# in seconds
timeout: int
runner_path: Path
@dataclass
class RunnerProxy:
conf: Conf
_timeout: int = field(init=False, default=5)
def _kill_process(self, process: subprocess.Popen) -> None:
try:
process.kill()
try:
process.wait(timeout=self._timeout)
except subprocess.TimeoutExpired:
logger.error("Process did not terminate after kill within 5 seconds")
except Exception as e:
logger.error(f"Error killing process: {e}")
def _kill_process_group(self, process: subprocess.Popen) -> None:
# Kill the entire process group
try:
if os.name != "nt": # Unix-like systems
# Kill the process group
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
# Wait a bit for graceful termination
try:
process.wait(timeout=self._timeout)
except subprocess.TimeoutExpired:
# Force kill if graceful termination failed
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
try:
process.wait(timeout=self._timeout)
except subprocess.TimeoutExpired:
logger.error("Process did not terminate after kill within 5 seconds")
else: # Windows
self._kill_process(process)
except (ProcessLookupError, OSError) as e:
logger.warning(f"Error killing process group: {e}")
self._kill_process(process)
def _run_subprocess_task(self, cmd: list[str], timeout: int, task_type: str) -> dict[str, Any]:
"""Run subprocess task and return result"""
# Enforce a timeout of runner_timeout + 5 minutes (in seconds)
subprocess_timeout = timeout + 300
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
try:
stdout, stderr = process.communicate(timeout=subprocess_timeout)
if process.returncode != 0:
error_msg = stderr.decode("utf-8") if stderr else "Unknown subprocess error"
logger.error(f"{task_type} task failed: {error_msg}")
return {
"status": "failed",
"error": f"Task failed: {error_msg}",
}
except subprocess.TimeoutExpired:
logger.error(f"{task_type} task timed out after {subprocess_timeout} seconds")
if process.poll() is None:
self._kill_process_group(process)
return {
"status": "failed",
"error": f"Task timed out after {subprocess_timeout} seconds",
}
# Parse the JSON output from the subprocess
try:
output_str = stdout.decode("utf-8").strip()
logger.debug(f"Subprocess output: {output_str}")
return json.loads(output_str) # type: ignore[no-any-return]
except json.JSONDecodeError as parse_error:
logger.error(f"Failed to parse JSON output for task {task_type}: {parse_error}")
logger.error(f"Raw output: {output_str}")
return {
"status": "failed",
"error": f"Failed to parse JSON output: {parse_error}",
}
except Exception as parse_error:
logger.error(f"Failed to parse subprocess output for task {task_type}: {parse_error}")
return {
"status": "failed",
"error": f"Failed to parse output: {parse_error}",
}
except Exception as e:
logger.error(f"Failed to start subprocess for task {task_type}: {e}")
return {
"status": "failed",
"error": f"Failed to start subprocess: {e}",
}
def run_fuzzer(self, conf: FuzzConfiguration) -> FuzzResult:
"""Run fuzzer via HTTP server and wait for completion"""
try:
logger.info(f"Starting fuzzer task {conf.engine} | {conf.sanitizer} | {conf.target_path}")
runner_timeout = self.conf.timeout if self.conf.timeout else 1000
cmd = [
str(self.conf.runner_path),
"--timeout",
str(runner_timeout),
"--corpusdir",
str(conf.corpus_dir),
"--engine",
conf.engine,
"--sanitizer",
conf.sanitizer,
str(conf.target_path),
"fuzz",
]
result = self._run_subprocess_task(cmd, runner_timeout, "fuzz")
except Exception as e:
logger.exception(f"Fuzzer task {conf.engine} | {conf.sanitizer} | {conf.target_path} failed: {str(e)}")
result = {
"status": "failed",
"error": str(e),
}
return self._dict_to_fuzz_result(result)
def merge_corpus(self, conf: FuzzConfiguration, output_dir: str) -> None:
"""Merge corpus via HTTP server and wait for completion"""
try:
logger.info(f"Starting merge corpus task {conf.engine} | {conf.sanitizer} | {conf.target_path}")
runner_timeout = self.conf.timeout if self.conf.timeout else 1000
cmd = [
str(self.conf.runner_path),
"--timeout",
str(runner_timeout),
"--corpusdir",
str(conf.corpus_dir),
"--engine",
conf.engine,
"--sanitizer",
conf.sanitizer,
str(conf.target_path),
"merge",
"--output-dir",
str(output_dir),
]
self._run_subprocess_task(cmd, runner_timeout, "merge")
except Exception as e:
logger.exception(
f"Merge corpus task {conf.engine} | {conf.sanitizer} | {conf.target_path} failed: {str(e)}"
)
def _dict_to_fuzz_result(self, result_dict: dict[str, Any]) -> FuzzResult:
"""Convert dictionary result back to FuzzResult object"""
# Handle both "logs" and "error" keys for backward compatibility
logs = result_dict.get("logs", result_dict.get("error", ""))
return FuzzResult(
logs=logs,
crashes=[
Crash(
input_path=crash.get("input_path", ""),
stacktrace=crash.get("stacktrace", ""),
reproduce_args=crash.get("reproduce_args", []),
crash_time=crash.get("crash_time", 0.0),
)
for crash in result_dict.get("crashes", [])
],
stats=result_dict.get("stats", {}),
time_executed=result_dict.get("time_executed", 0.0),
timed_out=result_dict.get("timed_out", False),
command=result_dict.get("command", ""),
)
@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Annotated from typing import Annotated
from pydantic import Field from pydantic import Field
@@ -44,6 +45,7 @@ class FuzzerBotSettings(WorkerSettings):
crash_dir_count_limit: Annotated[int, Field(default=0)] crash_dir_count_limit: Annotated[int, Field(default=0)]
max_local_files: Annotated[int, Field(default=500)] max_local_files: Annotated[int, Field(default=500)]
max_pov_size: Annotated[int, Field(default=2 * 1024 * 1024)] # 2 MiB max_pov_size: Annotated[int, Field(default=2 * 1024 * 1024)] # 2 MiB
runner_path: Path
class CoverageBotSettings(WorkerSettings, BuilderSettings): class CoverageBotSettings(WorkerSettings, BuilderSettings):
+5 -2
View File
@@ -1,6 +1,6 @@
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
from redis import Redis from redis import Redis
@@ -14,6 +14,8 @@ class TestMergerBot(unittest.TestCase):
def setUp(self): def setUp(self):
self.redis_mock = Mock(spec=Redis) self.redis_mock = Mock(spec=Redis)
self.runner_mock = Mock() self.runner_mock = Mock()
# Make merge_corpus an async mock since it's now an async method
self.runner_mock.merge_corpus = AsyncMock()
self.corpus_mock = Mock() self.corpus_mock = Mock()
self.lock_mock = MagicMock() self.lock_mock = MagicMock()
@@ -25,13 +27,14 @@ class TestMergerBot(unittest.TestCase):
self.max_local_files = 500 self.max_local_files = 500
# Create the MergerBot instance with mocked runner # Create the MergerBot instance with mocked runner
with patch("buttercup.fuzzing_infra.corpus_merger.Runner") as runner_class_mock: with patch("buttercup.fuzzing_infra.corpus_merger.RunnerProxy") as runner_class_mock:
runner_class_mock.return_value = self.runner_mock runner_class_mock.return_value = self.runner_mock
self.merger_bot = MergerBot( self.merger_bot = MergerBot(
self.redis_mock, self.redis_mock,
self.timeout_seconds, self.timeout_seconds,
self.python, self.python,
self.crs_scratch_dir, self.crs_scratch_dir,
"/path/to/runner",
self.max_local_files, self.max_local_files,
) )
+270
View File
@@ -0,0 +1,270 @@
import json
import subprocess
import time
from unittest.mock import Mock, patch
import pytest
from buttercup.common.types import FuzzConfiguration
from buttercup.fuzzing_infra.runner_proxy import Conf, Crash, FuzzResult, RunnerProxy
@pytest.fixture
def fuzz_config():
return FuzzConfiguration(
corpus_dir="/path/to/corpus", target_path="/path/to/target", engine="libfuzzer", sanitizer="address"
)
@patch("buttercup.fuzzing_infra.runner_proxy.subprocess.Popen")
def test_run_fuzzer_success(mock_popen, fuzz_config):
"""Test successful fuzzer execution via subprocess"""
# Create runner proxy
conf = Conf(
timeout=100,
runner_path="/path/to/runner",
)
runner_proxy = RunnerProxy(conf)
# Mock subprocess result
mock_result = {
"logs": "test logs",
"crashes": [
{
"input_path": "input1",
"stacktrace": "stacktrace1",
"reproduce_args": ["arg1", "arg2"],
"crash_time": 1.0,
}
],
"stats": {"execs_per_sec": 1000},
"time_executed": 10.0,
"timed_out": False,
"command": "test command",
}
# Mock the subprocess
mock_process = Mock()
mock_process.communicate.return_value = (json.dumps(mock_result).encode(), b"")
mock_process.returncode = 0
mock_process.pid = 12345
mock_popen.return_value = mock_process
# Run fuzzer
result = runner_proxy.run_fuzzer(fuzz_config)
# Verify subprocess was called with correct arguments
expected_cmd = [
"/path/to/runner",
"--timeout",
"100",
"--corpusdir",
"/path/to/corpus",
"--engine",
"libfuzzer",
"--sanitizer",
"address",
"/path/to/target",
"fuzz",
]
mock_popen.assert_called_once()
call_args = mock_popen.call_args[0][0] # First positional argument (cmd)
assert call_args == expected_cmd
# Verify result is a FuzzResult instance
assert isinstance(result, FuzzResult)
assert result.logs == "test logs"
assert result.crashes == [
Crash(
input_path="input1",
stacktrace="stacktrace1",
reproduce_args=["arg1", "arg2"],
crash_time=1.0,
),
]
assert result.stats == {"execs_per_sec": 1000}
assert result.time_executed == 10.0
assert not result.timed_out
assert result.command == "test command"
@patch("buttercup.fuzzing_infra.runner_proxy.subprocess.Popen")
def test_run_fuzzer_failure(mock_popen, fuzz_config):
"""Test fuzzer execution failure via subprocess"""
# Create runner proxy
conf = Conf(
timeout=100,
runner_path="/path/to/runner",
)
runner_proxy = RunnerProxy(conf)
# Mock the subprocess to return non-zero exit code
mock_process = Mock()
mock_process.communicate.return_value = (b"", b"Fuzzer crashed")
mock_process.returncode = 1
mock_process.pid = 12345
mock_popen.return_value = mock_process
# Run fuzzer and expect failure
res = runner_proxy.run_fuzzer(fuzz_config)
assert "Task failed: Fuzzer crashed" in res.logs
assert res.crashes == []
assert res.command == ""
@patch("buttercup.fuzzing_infra.runner_proxy.subprocess.Popen")
def test_run_fuzzer_timeout(mock_popen, fuzz_config):
"""Test fuzzer execution timeout"""
# Create runner proxy with very short timeout for testing
conf = Conf(
timeout=1, # 1 second timeout
runner_path="/path/to/runner",
)
runner_proxy = RunnerProxy(conf)
# Mock the subprocess to timeout
mock_process = Mock()
mock_process.communicate.side_effect = subprocess.TimeoutExpired("test", 1)
mock_process.returncode = None
mock_process.poll.return_value = None
mock_process.pid = 12345
mock_popen.return_value = mock_process
# Run fuzzer and expect timeout
start_time = time.time()
res = runner_proxy.run_fuzzer(fuzz_config)
assert "Task timed out" in res.logs
assert res.crashes == []
assert res.command == ""
# Verify it didn't take too long (should timeout quickly in test environment)
elapsed = time.time() - start_time
assert elapsed < 3.0 # Should timeout within 3 seconds
@patch("buttercup.fuzzing_infra.runner_proxy.subprocess.Popen")
def test_merge_corpus_success(mock_popen, fuzz_config):
"""Test successful corpus merge via subprocess"""
# Create runner proxy
conf = Conf(
timeout=100,
runner_path="/path/to/runner",
)
runner_proxy = RunnerProxy(conf)
# Mock the subprocess
mock_process = Mock()
mock_process.communicate.return_value = (b"", b"")
mock_process.returncode = 0
mock_process.pid = 12345
mock_popen.return_value = mock_process
# Run merge corpus
runner_proxy.merge_corpus(fuzz_config, "/path/to/output")
# Verify subprocess was called with correct arguments
expected_cmd = [
"/path/to/runner",
"--timeout",
"100",
"--corpusdir",
"/path/to/corpus",
"--engine",
"libfuzzer",
"--sanitizer",
"address",
"/path/to/target",
"merge",
"--output-dir",
"/path/to/output",
]
mock_popen.assert_called_once()
call_args = mock_popen.call_args[0][0] # First positional argument (cmd)
assert call_args == expected_cmd
@patch("buttercup.fuzzing_infra.runner_proxy.subprocess.Popen")
def test_subprocess_error_handling(mock_popen, fuzz_config):
"""Test subprocess error handling"""
# Create runner proxy
conf = Conf(
timeout=100,
runner_path="/path/to/runner",
)
runner_proxy = RunnerProxy(conf)
# Mock subprocess to raise an exception
mock_popen.side_effect = FileNotFoundError("Runner not found")
# Run fuzzer and expect error
res = runner_proxy.run_fuzzer(fuzz_config)
assert "Runner not found" in res.logs
assert res.crashes == []
assert res.command == ""
def test_runner_proxy_initialization():
"""Test that RunnerProxy initializes correctly"""
conf = Conf(timeout=100, runner_path="/path/to/runner")
proxy = RunnerProxy(conf)
# Verify configuration is set correctly
assert proxy.conf.timeout == 100
assert proxy.conf.runner_path == "/path/to/runner"
assert proxy._timeout == 5 # Default internal timeout
def test_fuzz_result_creation():
"""Test FuzzResult dataclass creation"""
result = FuzzResult(
logs="test logs",
command="fuzzer command",
crashes=[
Crash(
input_path="input1",
stacktrace="stacktrace1",
reproduce_args=["arg1", "arg2"],
crash_time=1.0,
),
Crash(
input_path="input2",
stacktrace="stacktrace2",
reproduce_args=["arg3", "arg4"],
crash_time=2.0,
),
],
stats={"execs_per_sec": 1000},
time_executed=5.5,
timed_out=False,
)
assert result.logs == "test logs"
assert result.crashes == [
Crash(
input_path="input1",
stacktrace="stacktrace1",
reproduce_args=["arg1", "arg2"],
crash_time=1.0,
),
Crash(
input_path="input2",
stacktrace="stacktrace2",
reproduce_args=["arg3", "arg4"],
crash_time=2.0,
),
]
assert result.stats == {"execs_per_sec": 1000}
assert result.time_executed == 5.5
assert not result.timed_out
assert result.command == "fuzzer command"
def test_conf_defaults():
"""Test Conf dataclass default values"""
from pathlib import Path
conf = Conf(timeout=60, runner_path=Path("/path/to/runner"))
assert conf.timeout == 60
assert conf.runner_path == Path("/path/to/runner")
+503 -850
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
fuzzing_infra/datastructures/fuzzer_msg*
*.pyc
+9
View File
@@ -0,0 +1,9 @@
# Fuzzer Runner
A libfuzzer-based fuzzer runner that can be used as a command-line tool.
It is useful to separate the clusterfuzz dependency (and its older indirect dependencies like protobuf==3.20) from the rest of the system.
## Features
- Run fuzzers with various engines and sanitizers
- Merge corpus files
+76
View File
@@ -0,0 +1,76 @@
[project]
name = "fuzzing-runner"
version = "0.1.0"
description = "libfuzzer-based fuzzer runner"
authors = [{ name = "Trail of Bits", email = "opensource@trailofbits.com" }]
license = "AGPL-3.0-only"
requires-python = ">=3.12,<3.13"
dependencies = [
"common",
"clusterfuzz ==2.6.0",
"pydantic ~=2.11.7",
]
[project.urls]
Repository = "https://github.com/trailofbits/buttercup"
Issues = "https://github.com/trailofbits/buttercup/issues"
[project.scripts]
buttercup-fuzzer-runner = "buttercup.fuzzer_runner.runner:main"
[tool.hatch.build.targets.wheel]
packages = ["src/buttercup"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv.sources]
common = { path = "../common", editable = true }
[dependency-groups]
dev = [
# Testing tools
"pytest ~=8.3.4",
"pytest-cov ~=6.0.0",
"httpx ~=0.27.0", # For FastAPI testing
# Linting and type checking
"ruff ~=0.12.8",
"mypy ~=1.17.1",
# Type stubs
"types-redis ~=4.6.0",
]
[tool.ruff]
line-length = 120
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "W", "UP"]
ignore = [
"COM812", "ISC001", # Formatter conflicts
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "D"] # Allow asserts, no docstrings in tests
[tool.mypy]
plugins = ["pydantic.mypy"]
mypy_path = "src"
packages = "buttercup"
allow_redefinition = true
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
show_error_codes = true
sqlite_cache = true
strict_equality = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
exclude = []
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
SCRIPT_DIR=$(dirname "$0")
if [ -d "$SCRIPT_DIR/.venv" ]; then
. "$SCRIPT_DIR/.venv/bin/activate"
else
echo "Virtual environment not found in $SCRIPT_DIR/.venv"
exit 1
fi
buttercup-fuzzer-runner $@ # we use $@ to pass all the arguments to the python script
@@ -1,4 +1,5 @@
import argparse import argparse
import json
import logging import logging
import os import os
import typing import typing
@@ -10,8 +11,8 @@ from clusterfuzz.fuzz.engine import Engine, FuzzOptions, FuzzResult
from buttercup.common.logger import setup_package_logger from buttercup.common.logger import setup_package_logger
from buttercup.common.node_local import scratch_dir from buttercup.common.node_local import scratch_dir
from buttercup.common.queues import FuzzConfiguration from buttercup.common.types import FuzzConfiguration
from buttercup.fuzzing_infra.temp_dir import patched_temp_dir, scratch_cwd from buttercup.fuzzer_runner.temp_dir import patched_temp_dir, scratch_cwd
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -67,21 +68,76 @@ class Runner:
) )
def run_fuzzer_command(args: argparse.Namespace, runner: Runner, fuzzconf: FuzzConfiguration) -> dict[str, typing.Any]:
"""Run fuzzer command"""
result = runner.run_fuzzer(fuzzconf)
result_dict = {
"logs": result.logs,
"command": result.command,
"crashes": [
{
"input_path": crash.input_path,
"stacktrace": crash.stacktrace,
"reproduce_args": crash.reproduce_args,
"crash_time": crash.crash_time,
}
for crash in result.crashes
],
"stats": result.stats,
"time_executed": result.time_executed,
"timed_out": result.timed_out,
}
return result_dict
def merge_corpus_command(
args: argparse.Namespace, runner: Runner, fuzzconf: FuzzConfiguration
) -> dict[str, typing.Any]:
"""Run merge corpus command"""
runner.merge_corpus(fuzzconf, args.output_dir)
result_dict = {
"status": "completed",
"output_dir": args.output_dir,
"message": "Corpus merge completed successfully",
}
return result_dict
def main() -> None: def main() -> None:
prsr = argparse.ArgumentParser("Fuzzer runner") prsr = argparse.ArgumentParser("Fuzzer runner")
prsr.add_argument("--timeout", required=True, type=int) prsr.add_argument("--timeout", required=True, type=int)
prsr.add_argument("--corpusdir", required=True) prsr.add_argument("--corpusdir", required=True)
prsr.add_argument("--engine", required=True) prsr.add_argument("--engine", required=True)
prsr.add_argument("--sanitizer", required=True) prsr.add_argument("--sanitizer", required=True)
prsr.add_argument("target") prsr.add_argument("target")
args = prsr.parse_args()
subparsers = prsr.add_subparsers(dest="command", help="Available commands")
# Fuzzer command
fuzzer_parser = subparsers.add_parser("fuzz", help="Run fuzzer")
fuzzer_parser.set_defaults(func=run_fuzzer_command)
# Merge corpus command
merge_parser = subparsers.add_parser("merge", help="Merge corpus")
merge_parser.add_argument("--output-dir", required=True)
merge_parser.set_defaults(func=merge_corpus_command)
setup_package_logger("fuzzer-runner", __name__, "DEBUG", None) setup_package_logger("fuzzer-runner", __name__, "DEBUG", None)
args = prsr.parse_args()
if not hasattr(args, "func"):
prsr.print_help()
return
conf = Conf(args.timeout) conf = Conf(args.timeout)
fuzzconf = FuzzConfiguration(args.corpusdir, args.target, args.engine, args.sanitizer) fuzzconf = FuzzConfiguration(args.corpusdir, args.target, args.engine, args.sanitizer)
runner = Runner(conf) runner = Runner(conf)
print(runner.run_fuzzer(fuzzconf))
result_dict = args.func(args, runner, fuzzconf)
print(json.dumps(result_dict))
if __name__ == "__main__": if __name__ == "__main__":
View File
@@ -5,7 +5,7 @@ import time
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from buttercup.fuzzing_infra.temp_dir import _scratch_path_var, get_temp_dir, patched_temp_dir from buttercup.fuzzer_runner.temp_dir import _scratch_path_var, get_temp_dir, patched_temp_dir
class TestGetTempDir(unittest.TestCase): class TestGetTempDir(unittest.TestCase):
@@ -20,7 +20,7 @@ class TestGetTempDir(unittest.TestCase):
"""Test get_temp_dir uses scratch path when available.""" """Test get_temp_dir uses scratch path when available."""
test_scratch_path = "/test/scratch/path" test_scratch_path = "/test/scratch/path"
with patch("buttercup.fuzzing_infra.temp_dir.shell.create_directory") as mock_create_dir: with patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir:
# Set the context variable # Set the context variable
token = _scratch_path_var.set(test_scratch_path) token = _scratch_path_var.set(test_scratch_path)
@@ -39,8 +39,8 @@ class TestGetTempDir(unittest.TestCase):
test_fuzz_inputs_path = "/test/fuzz/inputs" test_fuzz_inputs_path = "/test/fuzz/inputs"
with ( with (
patch("buttercup.fuzzing_infra.temp_dir.environment.get_value") as mock_get_value, patch("buttercup.fuzzer_runner.temp_dir.environment.get_value") as mock_get_value,
patch("buttercup.fuzzing_infra.temp_dir.shell.create_directory") as mock_create_dir, patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir,
): ):
mock_get_value.return_value = test_fuzz_inputs_path mock_get_value.return_value = test_fuzz_inputs_path
@@ -53,7 +53,7 @@ class TestGetTempDir(unittest.TestCase):
def test_get_temp_dir_fallback_without_fuzz_inputs_disk(self): def test_get_temp_dir_fallback_without_fuzz_inputs_disk(self):
"""Test get_temp_dir falls back to system temp when use_fuzz_inputs_disk=False.""" """Test get_temp_dir falls back to system temp when use_fuzz_inputs_disk=False."""
with patch("buttercup.fuzzing_infra.temp_dir.shell.create_directory") as mock_create_dir: with patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir:
result = get_temp_dir(use_fuzz_inputs_disk=False) result = get_temp_dir(use_fuzz_inputs_disk=False)
expected_path = os.path.join(tempfile.gettempdir(), f"temp-{os.getpid()}") expected_path = os.path.join(tempfile.gettempdir(), f"temp-{os.getpid()}")
@@ -63,8 +63,8 @@ class TestGetTempDir(unittest.TestCase):
def test_get_temp_dir_fallback_to_system_temp(self): def test_get_temp_dir_fallback_to_system_temp(self):
"""Test get_temp_dir falls back to system temp when FUZZ_INPUTS_DISK not set.""" """Test get_temp_dir falls back to system temp when FUZZ_INPUTS_DISK not set."""
with ( with (
patch("buttercup.fuzzing_infra.temp_dir.environment.get_value") as mock_get_value, patch("buttercup.fuzzer_runner.temp_dir.environment.get_value") as mock_get_value,
patch("buttercup.fuzzing_infra.temp_dir.shell.create_directory") as mock_create_dir, patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir,
): ):
# Return system temp dir when FUZZ_INPUTS_DISK is not set # Return system temp dir when FUZZ_INPUTS_DISK is not set
mock_get_value.return_value = tempfile.gettempdir() mock_get_value.return_value = tempfile.gettempdir()
@@ -81,7 +81,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_basic_functionality(self): def test_patched_temp_dir_basic_functionality(self):
"""Test basic functionality of patched_temp_dir context manager.""" """Test basic functionality of patched_temp_dir context manager."""
with patch("buttercup.fuzzing_infra.temp_dir.scratch_dir") as mock_scratch_dir: with patch("buttercup.fuzzer_runner.temp_dir.scratch_dir") as mock_scratch_dir:
# Mock scratch_dir context manager # Mock scratch_dir context manager
mock_scratch = MagicMock() mock_scratch = MagicMock()
mock_scratch.path = "/mock/scratch/path" mock_scratch.path = "/mock/scratch/path"
@@ -97,7 +97,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_patches_clusterfuzz_function(self): def test_patched_temp_dir_patches_clusterfuzz_function(self):
"""Test that patched_temp_dir properly patches the clusterfuzz function.""" """Test that patched_temp_dir properly patches the clusterfuzz function."""
with patch("buttercup.fuzzing_infra.temp_dir.scratch_dir") as mock_scratch_dir: with patch("buttercup.fuzzer_runner.temp_dir.scratch_dir") as mock_scratch_dir:
# Mock scratch_dir context manager # Mock scratch_dir context manager
mock_scratch = MagicMock() mock_scratch = MagicMock()
mock_scratch.path = "/mock/scratch/path" mock_scratch.path = "/mock/scratch/path"
@@ -106,7 +106,7 @@ class TestPatchedTempDir(unittest.TestCase):
# Test that the patching works by calling the function directly # Test that the patching works by calling the function directly
with patched_temp_dir(): with patched_temp_dir():
with patch("buttercup.fuzzing_infra.temp_dir.shell.create_directory") as mock_create_dir: with patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir:
# Import and call the clusterfuzz function - it should be patched to use our implementation # Import and call the clusterfuzz function - it should be patched to use our implementation
from clusterfuzz._internal.bot.fuzzers import utils from clusterfuzz._internal.bot.fuzzers import utils
@@ -119,7 +119,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_context_variable_cleanup(self): def test_patched_temp_dir_context_variable_cleanup(self):
"""Test that context variable is properly cleaned up after context exit.""" """Test that context variable is properly cleaned up after context exit."""
with patch("buttercup.fuzzing_infra.temp_dir.scratch_dir") as mock_scratch_dir: with patch("buttercup.fuzzer_runner.temp_dir.scratch_dir") as mock_scratch_dir:
# Mock scratch_dir context manager # Mock scratch_dir context manager
mock_scratch = MagicMock() mock_scratch = MagicMock()
mock_scratch.path = "/mock/scratch/path" mock_scratch.path = "/mock/scratch/path"
@@ -138,7 +138,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_nested_contexts(self): def test_patched_temp_dir_nested_contexts(self):
"""Test that nested patched_temp_dir contexts work correctly.""" """Test that nested patched_temp_dir contexts work correctly."""
with patch("buttercup.fuzzing_infra.temp_dir.scratch_dir") as mock_scratch_dir: with patch("buttercup.fuzzer_runner.temp_dir.scratch_dir") as mock_scratch_dir:
# Mock different scratch directories for nested contexts # Mock different scratch directories for nested contexts
mock_scratch1 = MagicMock() mock_scratch1 = MagicMock()
mock_scratch1.path = "/mock/scratch/path1" mock_scratch1.path = "/mock/scratch/path1"
@@ -162,7 +162,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_exception_handling(self): def test_patched_temp_dir_exception_handling(self):
"""Test that context variable is cleaned up even when exceptions occur.""" """Test that context variable is cleaned up even when exceptions occur."""
with patch("buttercup.fuzzing_infra.temp_dir.scratch_dir") as mock_scratch_dir: with patch("buttercup.fuzzer_runner.temp_dir.scratch_dir") as mock_scratch_dir:
mock_scratch = MagicMock() mock_scratch = MagicMock()
mock_scratch.path = "/mock/scratch/path" mock_scratch.path = "/mock/scratch/path"
mock_scratch_dir.return_value.__enter__.return_value = mock_scratch mock_scratch_dir.return_value.__enter__.return_value = mock_scratch
@@ -189,7 +189,7 @@ class TestThreadSafety(unittest.TestCase):
def thread_worker(thread_id: int): def thread_worker(thread_id: int):
try: try:
with patch("buttercup.fuzzing_infra.temp_dir.scratch_dir") as mock_scratch_dir: with patch("buttercup.fuzzer_runner.temp_dir.scratch_dir") as mock_scratch_dir:
mock_scratch = MagicMock() mock_scratch = MagicMock()
mock_scratch.path = f"/mock/scratch/thread{thread_id}" mock_scratch.path = f"/mock/scratch/thread{thread_id}"
mock_scratch_dir.return_value.__enter__.return_value = mock_scratch mock_scratch_dir.return_value.__enter__.return_value = mock_scratch
+1488
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ license = "AGPL-3.0-only"
requires-python = ">=3.12,<3.13" requires-python = ">=3.12,<3.13"
dependencies = [ dependencies = [
"argon2-cffi ~=21.3.0", "argon2-cffi ~=21.3.0",
"common", "common[full]",
"fastapi ~=0.115.6", "fastapi ~=0.115.6",
"pydantic ~=2.10.5", "pydantic ~=2.10.5",
"pydantic-settings ~=2.7.1", "pydantic-settings ~=2.7.1",
+464 -407
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -7,12 +7,11 @@ authors = [{ name = "Trail of Bits", email = "opensource@trailofbits.com" }]
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
requires-python = ">=3.12,<3.13" requires-python = ">=3.12,<3.13"
dependencies = [ dependencies = [
"common", "common[full]",
"langchain ~=0.3.20", "langchain-community ~=0.3.27",
"langchain-community ~=0.3.16", "langgraph ~=0.6.6",
"langgraph ~=0.5.2", "langgraph-checkpoint ~=2.1.1",
"langgraph-checkpoint ~=2.1.0", "openai ~=1.100.2",
"openai ~=1.60.2",
"program-model", "program-model",
"pydantic-settings ~=2.7.1", "pydantic-settings ~=2.7.1",
"python-dotenv ~=1.0.1", "python-dotenv ~=1.0.1",
@@ -36,7 +36,7 @@ class PatcherLeaderAgent(PatcherAgentBase):
tasks_storage: Path tasks_storage: Path
model_name: str | None = None model_name: str | None = None
def _init_patch_team(self) -> StateGraph[PatcherAgentState, PatcherAgentState, PatcherAgentState]: def _init_patch_team(self) -> StateGraph[PatcherAgentState, PatcherConfig, PatcherAgentState, PatcherAgentState]:
rootcause_agent = RootCauseAgent(self.challenge, self.input, chain_call=self.chain_call) rootcause_agent = RootCauseAgent(self.challenge, self.input, chain_call=self.chain_call)
swe_agent = SWEAgent(self.challenge, self.input, chain_call=self.chain_call) swe_agent = SWEAgent(self.challenge, self.input, chain_call=self.chain_call)
qe_agent = QEAgent(self.challenge, self.input, chain_call=self.chain_call) qe_agent = QEAgent(self.challenge, self.input, chain_call=self.chain_call)
@@ -69,7 +69,7 @@ class PatcherLeaderAgent(PatcherAgentBase):
workflow.add_node(PatcherAgentName.RUN_POV.value, qe_agent.run_pov_node) workflow.add_node(PatcherAgentName.RUN_POV.value, qe_agent.run_pov_node)
workflow.add_node(PatcherAgentName.RUN_TESTS.value, qe_agent.run_tests_node) workflow.add_node(PatcherAgentName.RUN_TESTS.value, qe_agent.run_tests_node)
workflow.add_node(PatcherAgentName.REFLECTION.value, reflection_agent.reflect_on_patch) workflow.add_node(PatcherAgentName.REFLECTION.value, reflection_agent.reflect_on_patch)
workflow.add_node(PatcherAgentName.CONTEXT_RETRIEVER.value, context_retriever_agent.retrieve_context) # type: ignore[call-overload] workflow.add_node(PatcherAgentName.CONTEXT_RETRIEVER.value, context_retriever_agent.retrieve_context)
workflow.add_node(PatcherAgentName.PATCH_VALIDATION.value, qe_agent.validate_patch_node) workflow.add_node(PatcherAgentName.PATCH_VALIDATION.value, qe_agent.validate_patch_node)
workflow.set_entry_point(PatcherAgentName.INPUT_PROCESSING.value) workflow.set_entry_point(PatcherAgentName.INPUT_PROCESSING.value)
+548 -191
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -7,8 +7,7 @@ authors = [{ name = "Trail of Bits", email = "opensource@trailofbits.com" }]
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
requires-python = ">=3.12,<3.13" requires-python = ">=3.12,<3.13"
dependencies = [ dependencies = [
"common", "common[full]",
"protobuf ==3.20.3", # Pin exactly for stability
"tree-sitter ~=0.24.0", "tree-sitter ~=0.24.0",
"tree-sitter-language-pack ~=0.9.0", "tree-sitter-language-pack ~=0.9.0",
"rapidfuzz ~=3.12.2", "rapidfuzz ~=3.12.2",
+523 -409
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -8,14 +8,13 @@ authors = [
] ]
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
dependencies = [ dependencies = [
"common[full]",
"redis ~=5.2.1", "redis ~=5.2.1",
"common",
"wasmtime ~=29.0.0", "wasmtime ~=29.0.0",
"program-model", "program-model",
"numpy ~=2.2.3", "numpy ~=2.2.3",
"rapidfuzz ~=3.12.2", "rapidfuzz ~=3.12.2",
"langgraph ~=0.5.2", "langgraph ~=0.6.6",
"langchain ~=0.3.20",
"python-dotenv ~=1.0.1", "python-dotenv ~=1.0.1",
] ]
requires-python = ">=3.12,<3.13" requires-python = ">=3.12,<3.13"
@@ -256,13 +256,12 @@ class VulnBaseTask(Task):
ctoken, ctoken,
) )
crash = Crash( crash = Crash()
target=build, crash.target.CopyFrom(build)
harness_name=self.harness_name, crash.harness_name = self.harness_name
crash_input_path=dst, crash.crash_input_path = dst
stacktrace=stacktrace, crash.stacktrace = stacktrace
crash_token=ctoken, crash.crash_token = ctoken
)
self.crash_submit.crash_queue.push(crash) self.crash_submit.crash_queue.push(crash)
logger.debug("PoV stdout: %s", result.command_result.output) logger.debug("PoV stdout: %s", result.command_result.output)
@@ -277,8 +276,8 @@ class VulnBaseTask(Task):
workflow.add_node("tools", tool_node) workflow.add_node("tools", tool_node)
workflow.add_node("analyze_bug", self._analyze_bug) workflow.add_node("analyze_bug", self._analyze_bug)
workflow.add_node("write_pov", self._write_pov) workflow.add_node("write_pov", self._write_pov)
workflow.add_node("execute_python_funcs", self._exec_python_funcs_current) # type: ignore[call-overload] workflow.add_node("execute_python_funcs", self._exec_python_funcs_current)
workflow.add_node("test_povs", self._test_povs) # type: ignore[call-overload] workflow.add_node("test_povs", self._test_povs)
workflow.set_entry_point("gather_context") workflow.set_entry_point("gather_context")
workflow.add_edge("gather_context", "tools") workflow.add_edge("gather_context", "tools")
+1 -1
View File
@@ -64,7 +64,7 @@ def test_do_task_valid_pov(
# Mock reproduce_multiple to return crashes on 2nd iteration # Mock reproduce_multiple to return crashes on 2nd iteration
def mock_get_crashes(pov_path, harness_name): def mock_get_crashes(pov_path, harness_name):
if "iter1_" in pov_path.name: if "iter1_" in pov_path.name:
build = MagicMock(spec=BuildOutput) build = BuildOutput()
build.sanitizer = "asan" build.sanitizer = "asan"
result = MagicMock(spec=ReproduceResult) result = MagicMock(spec=ReproduceResult)
+535 -404
View File
File diff suppressed because it is too large Load Diff