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-python
**/pdm.lock
fuzzer/dockerfiles/runner_image.Dockerfile
fuzzer/Dockerfile
orchestrator/Dockerfile
patcher/Dockerfile
seed-gen/Dockerfile
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- component: orchestrator
dockerfile: ./orchestrator/Dockerfile
- component: fuzzer
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile
dockerfile: ./fuzzer/Dockerfile
- component: patcher
dockerfile: ./patcher/Dockerfile
- component: seed-gen
+6 -2
View File
@@ -150,7 +150,7 @@ send-libpng-task:
# Development targets
lint:
@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; \
done
@@ -169,7 +169,7 @@ lint-component:
reformat:
@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; \
done
@@ -187,6 +187,10 @@ undeploy:
@echo "Cleaning up deployment..."
cd deployment && make down
undeploy-k8s:
@echo "Cleaning up kubernetes resources..."
cd deployment && make down-k8s
clean-local:
@echo "Cleaning up local environment..."
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"
requires-python = ">=3.12,<3.13"
dependencies = [
"protobuf ==3.20.3", # Pin exactly for stability
"pydantic-settings ~=2.7.1",
"pymongo ~=4.10.1",
"redis ~=5.2.1",
"langchain-core ~=0.3.32",
"langchain-openai ~=0.3.2",
"langchain-core ~=0.3.74",
"langchain-openai ~=0.3.30",
"langchain ~=0.3.27",
"langfuse ~=2.59.2",
"langchain ~=0.3.18",
"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]
Repository = "https://github.com/trailofbits/buttercup"
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 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
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
INCONCLUSIVE: SubmissionResult
NONE: SubmissionResult
PASSED: SubmissionResult
PATCH: BuildType
COVERAGE: 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):
__slots__ = ["apply_diff", "build_type", "engine", "internal_patch_id", "sanitizer", "task_dir", "task_id"]
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
ENGINE_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
SANITIZER_FIELD_NUMBER: _ClassVar[int]
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
class Task(_message.Message):
__slots__ = ("message_id", "message_time", "task_id", "task_type", "sources", "deadline", "cancelled", "project_name", "focus", "metadata")
class TaskType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
TASK_TYPE_FULL: _ClassVar[Task.TaskType]
TASK_TYPE_DELTA: _ClassVar[Task.TaskType]
TASK_TYPE_FULL: Task.TaskType
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]
apply_diff: bool
build_type: BuildType
engine: str
internal_patch_id: str
sanitizer: str
task_dir: str
TASK_TYPE_FIELD_NUMBER: _ClassVar[int]
SOURCES_FIELD_NUMBER: _ClassVar[int]
DEADLINE_FIELD_NUMBER: _ClassVar[int]
CANCELLED_FIELD_NUMBER: _ClassVar[int]
PROJECT_NAME_FIELD_NUMBER: _ClassVar[int]
FOCUS_FIELD_NUMBER: _ClassVar[int]
METADATA_FIELD_NUMBER: _ClassVar[int]
message_id: str
message_time: int
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: ...
class BuildRequest(_message.Message):
__slots__ = ["apply_diff", "build_type", "engine", "internal_patch_id", "patch", "sanitizer", "task_dir", "task_id"]
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
ENGINE_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
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: ...
task_type: Task.TaskType
sources: _containers.RepeatedCompositeFieldContainer[SourceDetail]
deadline: int
cancelled: bool
project_name: str
focus: str
metadata: _containers.ScalarMap[str, str]
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 SourceDetail(_message.Message):
__slots__ = ["sha256", "source_type", "url"]
__slots__ = ("sha256", "source_type", "url")
class SourceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
SHA256_FIELD_NUMBER: _ClassVar[int]
SOURCE_TYPE_DIFF: SourceDetail.SourceType
SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int]
SOURCE_TYPE_FUZZ_TOOLING: SourceDetail.SourceType
__slots__ = ()
SOURCE_TYPE_REPO: _ClassVar[SourceDetail.SourceType]
SOURCE_TYPE_FUZZ_TOOLING: _ClassVar[SourceDetail.SourceType]
SOURCE_TYPE_DIFF: _ClassVar[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]
sha256: str
source_type: SourceDetail.SourceType
url: str
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):
__slots__ = ["task"]
__slots__ = ("task",)
TASK_FIELD_NUMBER: _ClassVar[int]
task: Task
def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ...
class TaskReady(_message.Message):
__slots__ = ["task"]
__slots__ = ("task",)
TASK_FIELD_NUMBER: _ClassVar[int]
task: Task
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):
__slots__ = ["crash", "tracer_stacktrace"]
__slots__ = ("crash", "tracer_stacktrace")
CRASH_FIELD_NUMBER: _ClassVar[int]
TRACER_STACKTRACE_FIELD_NUMBER: _ClassVar[int]
crash: Crash
tracer_stacktrace: str
def __init__(self, crash: _Optional[_Union[Crash, _Mapping]] = ..., tracer_stacktrace: _Optional[str] = ...) -> None: ...
class WeightedHarness(_message.Message):
__slots__ = ["harness_name", "package_name", "task_id", "weight"]
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
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 Patch(_message.Message):
__slots__ = ("task_id", "internal_patch_id", "patch")
TASK_ID_FIELD_NUMBER: _ClassVar[int]
WEIGHT_FIELD_NUMBER: _ClassVar[int]
harness_name: str
package_name: str
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
PATCH_FIELD_NUMBER: _ClassVar[int]
task_id: str
weight: float
def __init__(self, weight: _Optional[float] = ..., package_name: _Optional[str] = ..., harness_name: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
internal_patch_id: str
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):
__slots__ = []
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 SubmissionResult(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
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 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
from opentelemetry._logs import set_logger_provider
try:
from opentelemetry._logs import set_logger_provider
if os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") == "grpc":
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
else:
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter # type: ignore
if os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") == "grpc":
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
else:
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 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
_is_initialized = False
@@ -48,26 +53,27 @@ def setup_package_logger(
root.removeHandler(handler)
# 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
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
set_logger_provider(logger_provider)
exporter = OTLPLogExporter()
if _opentelemetry_enabled:
resource = Resource.create(
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
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
otlp_handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
# 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.
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)
-16
View File
@@ -444,19 +444,3 @@ class QueueFactory:
queue_args.update(kwargs)
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 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__)
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()))
service_instance_id = str(uuid.uuid4())
@@ -47,6 +41,9 @@ class CRSActionCategory(Enum):
def init_telemetry(application_name: str) -> None:
"""Initialize the telemetry for the application."""
if not _opentelemetry_enabled:
return
logger.info("Initializing telemetry for %s", application_name)
if not os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
logger.error("OTEL_EXPORTER_OTLP_ENDPOINT not set, disabling telemetry")
@@ -75,6 +72,9 @@ def set_crs_attributes(
task_metadata: dict,
extra_attributes: dict | None = None,
) -> None:
if not _opentelemetry_enabled:
return
extra_attributes = extra_attributes or {}
span.set_attribute("crs.action.category", crs_action_category.value)
span.set_attribute("crs.action.name", crs_action_name)
@@ -95,6 +95,9 @@ def log_crs_action_ok(
task_metadata: dict,
extra_attributes: dict | None = None,
) -> None:
if not _opentelemetry_enabled:
return
extra_attributes = extra_attributes or {}
with tracer.start_as_current_span(crs_action_name) as span:
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)
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
if exc.errno in (errno.ENOTDIR, errno.EINVAL):
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
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 $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"/..
@@ -21,7 +21,7 @@ spec:
- name: merger-bot
image: "{{ .Values.global.fuzzerImage.repository }}:{{ .Values.global.fuzzerImage.tag }}"
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:
{{- include "buttercup.env.nodeData" . | 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" . }}"
- name: BUTTERCUP_FUZZER_MAX_POV_SIZE
value: {{ int .Values.global.maxPovSize | quote }}
- name: BUTTERCUP_FUZZER_RUNNER_PATH
value: "/app/fuzzer_runner/runner.sh"
{{- end }}
{{- define "buttercup.povReproducerEnv" }}
+8 -8
View File
@@ -40,7 +40,7 @@ services:
stimulate-fuzzer-test:
build:
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
profiles:
- fuzzer-test
@@ -63,7 +63,7 @@ services:
orchestrator-sim:
build:
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
profiles:
- fuzzer-test
@@ -140,7 +140,7 @@ services:
command: ["buttercup-coverage-bot", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379"]
build:
context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile
dockerfile: ./fuzzer/Dockerfile
volumes:
- ../../crs_scratch:/crs_scratch
- ../../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"]
build:
context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile
dockerfile: ./fuzzer/Dockerfile
volumes:
- ../../crs_scratch:/crs_scratch
- ../../tasks_storage:/tasks_storage
@@ -172,7 +172,7 @@ services:
command: ["buttercup-tracer-bot", "--wdir", "/node_data/crs_scratch", "--redis_url", "redis://redis:6379"]
build:
context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile
dockerfile: ./fuzzer/Dockerfile
volumes:
- ../../crs_scratch:/crs_scratch
- ../../tasks_storage:/tasks_storage
@@ -187,8 +187,8 @@ services:
fuzzer-bot:
build:
context: ../../
dockerfile: ./fuzzer/dockerfiles/runner_image.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"]
dockerfile: ./fuzzer/Dockerfile
command: ["buttercup-fuzzer"]
env_file: env.dev.compose
volumes:
- ../../crs_scratch:/crs_scratch
@@ -312,7 +312,7 @@ services:
dockerfile: ./orchestrator/Dockerfile
command: ["buttercup-ui"]
ports:
- "127.0.0.1:1323:1323"
- "127.0.0.1:31323:1323"
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_GRAPHDB_ENABLED=false
BUTTERCUP_FUZZER_REDIS_URL=redis://redis:6379
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_LOG_LEVEL=debug
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_SERVER__REDIS_URL=redis://redis:6379
@@ -86,6 +91,7 @@ OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
OSS_FUZZ_CONTAINER_ORG=aixcc-afc
BUTTERCUP_UI_HOST=buttercup-ui
BUTTERCUP_UI_EXTERNAL_HOST=buttercup-ui
BUTTERCUP_UI_CRS_BASE_URL="http://task-server:8000"
BUTTERCUP_UI_STORAGE_DIR="/tmp/buttercup-ui"
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"
requires-python = ">=3.12,<3.13"
dependencies = [
"protobuf ==3.20.3", # Pin exactly for stability
"common[full]",
"redis ~=5.2.1",
"clusterfuzz ==2.6.0", # Pin exactly for stability
"common",
"pydantic-settings ~=2.7.1",
"beautifulsoup4 ~=4.13.3",
"lxml ~=5.3.1",
@@ -5,6 +5,7 @@ import random
import shutil
from dataclasses import dataclass
from os import PathLike
from pathlib import Path
from opentelemetry import trace
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.telemetry import CRSActionCategory, init_telemetry, set_crs_attributes
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
logger = logging.getLogger(__name__)
@@ -184,10 +185,11 @@ class MergerBot:
timeout_seconds: int,
python: str,
crs_scratch_dir: str,
runner_path: PathLike[str],
max_local_files: int = 500,
):
self.redis = redis
self.runner = Runner(Conf(timeout_seconds))
self.runner = RunnerProxy(Conf(timeout_seconds, Path(runner_path)))
self.python = python
self.crs_scratch_dir = crs_scratch_dir
self.harness_weights = HarnessWeights(redis)
@@ -401,6 +403,7 @@ def main() -> None:
args.timeout,
args.python,
args.crs_scratch_dir,
args.runner_path,
args.max_local_files,
)
merger.run()
@@ -1,8 +1,8 @@
import logging
import random
from pathlib import Path
from typing import TYPE_CHECKING
from clusterfuzz.fuzz import engine
from opentelemetry import trace
from opentelemetry.trace.status import Status, StatusCode
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.stack_parsing import CrashSet
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.fuzzing_infra.runner import Conf, FuzzConfiguration, Runner
from buttercup.fuzzing_infra.runner_proxy import Conf, RunnerProxy
from buttercup.fuzzing_infra.settings import FuzzerBotSettings
if TYPE_CHECKING:
from clusterfuzz.fuzz import engine
logger = logging.getLogger(__name__)
@@ -35,8 +39,9 @@ class FuzzerBot(TaskLoop):
crs_scratch_dir: str,
crash_dir_count_limit: int | None,
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.python = python
self.crs_scratch_dir = crs_scratch_dir
@@ -141,6 +146,7 @@ def main() -> None:
setup_periodic_zombie_reaper()
logger.info(f"Starting fuzzer (crs_scratch_dir: {args.crs_scratch_dir})")
logger.debug("Args: %s", args)
seconds_sleep = args.timer // 1000
fuzzer = FuzzerBot(
@@ -151,6 +157,7 @@ def main() -> None:
args.crs_scratch_dir,
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,
runner_path=args.runner_path,
)
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 pydantic import Field
@@ -44,6 +45,7 @@ class FuzzerBotSettings(WorkerSettings):
crash_dir_count_limit: Annotated[int, Field(default=0)]
max_local_files: Annotated[int, Field(default=500)]
max_pov_size: Annotated[int, Field(default=2 * 1024 * 1024)] # 2 MiB
runner_path: Path
class CoverageBotSettings(WorkerSettings, BuilderSettings):
+5 -2
View File
@@ -1,6 +1,6 @@
import unittest
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from redis import Redis
@@ -14,6 +14,8 @@ class TestMergerBot(unittest.TestCase):
def setUp(self):
self.redis_mock = Mock(spec=Redis)
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.lock_mock = MagicMock()
@@ -25,13 +27,14 @@ class TestMergerBot(unittest.TestCase):
self.max_local_files = 500
# 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
self.merger_bot = MergerBot(
self.redis_mock,
self.timeout_seconds,
self.python,
self.crs_scratch_dir,
"/path/to/runner",
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 json
import logging
import os
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.node_local import scratch_dir
from buttercup.common.queues import FuzzConfiguration
from buttercup.fuzzing_infra.temp_dir import patched_temp_dir, scratch_cwd
from buttercup.common.types import FuzzConfiguration
from buttercup.fuzzer_runner.temp_dir import patched_temp_dir, scratch_cwd
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:
prsr = argparse.ArgumentParser("Fuzzer runner")
prsr.add_argument("--timeout", required=True, type=int)
prsr.add_argument("--corpusdir", required=True)
prsr.add_argument("--engine", required=True)
prsr.add_argument("--sanitizer", required=True)
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)
args = prsr.parse_args()
if not hasattr(args, "func"):
prsr.print_help()
return
conf = Conf(args.timeout)
fuzzconf = FuzzConfiguration(args.corpusdir, args.target, args.engine, args.sanitizer)
runner = Runner(conf)
print(runner.run_fuzzer(fuzzconf))
result_dict = args.func(args, runner, fuzzconf)
print(json.dumps(result_dict))
if __name__ == "__main__":
View File
@@ -5,7 +5,7 @@ import time
import unittest
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):
@@ -20,7 +20,7 @@ class TestGetTempDir(unittest.TestCase):
"""Test get_temp_dir uses scratch path when available."""
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
token = _scratch_path_var.set(test_scratch_path)
@@ -39,8 +39,8 @@ class TestGetTempDir(unittest.TestCase):
test_fuzz_inputs_path = "/test/fuzz/inputs"
with (
patch("buttercup.fuzzing_infra.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.environment.get_value") as mock_get_value,
patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir,
):
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):
"""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)
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):
"""Test get_temp_dir falls back to system temp when FUZZ_INPUTS_DISK not set."""
with (
patch("buttercup.fuzzing_infra.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.environment.get_value") as mock_get_value,
patch("buttercup.fuzzer_runner.temp_dir.shell.create_directory") as mock_create_dir,
):
# Return system temp dir when FUZZ_INPUTS_DISK is not set
mock_get_value.return_value = tempfile.gettempdir()
@@ -81,7 +81,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_basic_functionality(self):
"""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 = MagicMock()
mock_scratch.path = "/mock/scratch/path"
@@ -97,7 +97,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_patches_clusterfuzz_function(self):
"""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 = MagicMock()
mock_scratch.path = "/mock/scratch/path"
@@ -106,7 +106,7 @@ class TestPatchedTempDir(unittest.TestCase):
# Test that the patching works by calling the function directly
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
from clusterfuzz._internal.bot.fuzzers import utils
@@ -119,7 +119,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_context_variable_cleanup(self):
"""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 = MagicMock()
mock_scratch.path = "/mock/scratch/path"
@@ -138,7 +138,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_nested_contexts(self):
"""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_scratch1 = MagicMock()
mock_scratch1.path = "/mock/scratch/path1"
@@ -162,7 +162,7 @@ class TestPatchedTempDir(unittest.TestCase):
def test_patched_temp_dir_exception_handling(self):
"""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.path = "/mock/scratch/path"
mock_scratch_dir.return_value.__enter__.return_value = mock_scratch
@@ -189,7 +189,7 @@ class TestThreadSafety(unittest.TestCase):
def thread_worker(thread_id: int):
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.path = f"/mock/scratch/thread{thread_id}"
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"
dependencies = [
"argon2-cffi ~=21.3.0",
"common",
"common[full]",
"fastapi ~=0.115.6",
"pydantic ~=2.10.5",
"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"
requires-python = ">=3.12,<3.13"
dependencies = [
"common",
"langchain ~=0.3.20",
"langchain-community ~=0.3.16",
"langgraph ~=0.5.2",
"langgraph-checkpoint ~=2.1.0",
"openai ~=1.60.2",
"common[full]",
"langchain-community ~=0.3.27",
"langgraph ~=0.6.6",
"langgraph-checkpoint ~=2.1.1",
"openai ~=1.100.2",
"program-model",
"pydantic-settings ~=2.7.1",
"python-dotenv ~=1.0.1",
@@ -36,7 +36,7 @@ class PatcherLeaderAgent(PatcherAgentBase):
tasks_storage: Path
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)
swe_agent = SWEAgent(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_TESTS.value, qe_agent.run_tests_node)
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.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"
requires-python = ">=3.12,<3.13"
dependencies = [
"common",
"protobuf ==3.20.3", # Pin exactly for stability
"common[full]",
"tree-sitter ~=0.24.0",
"tree-sitter-language-pack ~=0.9.0",
"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"
dependencies = [
"common[full]",
"redis ~=5.2.1",
"common",
"wasmtime ~=29.0.0",
"program-model",
"numpy ~=2.2.3",
"rapidfuzz ~=3.12.2",
"langgraph ~=0.5.2",
"langchain ~=0.3.20",
"langgraph ~=0.6.6",
"python-dotenv ~=1.0.1",
]
requires-python = ">=3.12,<3.13"
@@ -256,13 +256,12 @@ class VulnBaseTask(Task):
ctoken,
)
crash = Crash(
target=build,
harness_name=self.harness_name,
crash_input_path=dst,
stacktrace=stacktrace,
crash_token=ctoken,
)
crash = Crash()
crash.target.CopyFrom(build)
crash.harness_name = self.harness_name
crash.crash_input_path = dst
crash.stacktrace = stacktrace
crash.crash_token = ctoken
self.crash_submit.crash_queue.push(crash)
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("analyze_bug", self._analyze_bug)
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("test_povs", self._test_povs) # type: ignore[call-overload]
workflow.add_node("execute_python_funcs", self._exec_python_funcs_current)
workflow.add_node("test_povs", self._test_povs)
workflow.set_entry_point("gather_context")
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
def mock_get_crashes(pov_path, harness_name):
if "iter1_" in pov_path.name:
build = MagicMock(spec=BuildOutput)
build = BuildOutput()
build.sanitizer = "asan"
result = MagicMock(spec=ReproduceResult)
+535 -404
View File
File diff suppressed because it is too large Load Diff