mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
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:
committed by
GitHub
parent
7739f7907d
commit
143a59c236
+1
-1
@@ -16,7 +16,7 @@
|
||||
**/.pdm-build/
|
||||
**/.pdm-python
|
||||
**/pdm.lock
|
||||
fuzzer/dockerfiles/runner_image.Dockerfile
|
||||
fuzzer/Dockerfile
|
||||
orchestrator/Dockerfile
|
||||
patcher/Dockerfile
|
||||
seed-gen/Dockerfile
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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: ...
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class FuzzConfiguration:
|
||||
corpus_dir: str
|
||||
target_path: str
|
||||
engine: str
|
||||
sanitizer: str
|
||||
@@ -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)
|
||||
|
||||
Generated
+509
-405
File diff suppressed because it is too large
Load Diff
@@ -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 }}
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
Generated
+503
-850
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
fuzzing_infra/datastructures/fuzzer_msg*
|
||||
*.pyc
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
Executable
+12
@@ -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
|
||||
+60
-4
@@ -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__":
|
||||
@@ -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
|
||||
Generated
+1488
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Generated
+464
-407
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
Generated
+548
-191
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Generated
+523
-409
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+535
-404
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user