mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
Challenge Task API (#59)
This commit is contained in:
committed by
GitHub
parent
9885795582
commit
fdefa0fccb
@@ -15,6 +15,7 @@ dependencies = [
|
||||
|
||||
[project.scripts]
|
||||
buttercup-msg-publisher = "buttercup.common.msg_publisher:main"
|
||||
buttercup-challenge-task = "buttercup.common.challenge_task_cli:main"
|
||||
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Callable
|
||||
from os import PathLike
|
||||
import logging
|
||||
import uuid
|
||||
import subprocess
|
||||
from buttercup.common.utils import create_tmp_dir, copyanything
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChallengeTaskError(Exception):
|
||||
"""Base class for Challenge Task errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandResult:
|
||||
success: bool
|
||||
error: bytes | None = None
|
||||
output: bytes | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChallengeTask:
|
||||
"""Class to manage Challenge Tasks."""
|
||||
|
||||
read_only_task_dir: PathLike
|
||||
project_name: str
|
||||
python_path: PathLike = Path("python")
|
||||
local_task_dir: PathLike | None = None
|
||||
|
||||
SRC_DIR = "src"
|
||||
DIFF_DIR = "diff"
|
||||
OSS_FUZZ_DIR = "fuzz-tooling"
|
||||
|
||||
MAX_COMMIT_RETRIES = 3
|
||||
|
||||
_helper_path: Path = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.read_only_task_dir = Path(self.read_only_task_dir)
|
||||
self.local_task_dir = Path(self.local_task_dir) if self.local_task_dir else None
|
||||
self.python_path = Path(self.python_path)
|
||||
|
||||
self._check_dir_exists(self.read_only_task_dir)
|
||||
if self.local_task_dir:
|
||||
self._check_dir_exists(self.local_task_dir)
|
||||
|
||||
# Verify required directories exist
|
||||
for directory in [self.SRC_DIR, self.OSS_FUZZ_DIR]:
|
||||
if not (self.task_dir / directory).is_dir():
|
||||
raise ChallengeTaskError(f"Missing required directory: {self.task_dir / directory}")
|
||||
|
||||
self._helper_path = Path("infra/helper.py")
|
||||
if not (self.get_oss_fuzz_path() / self._helper_path).exists():
|
||||
raise ChallengeTaskError(f"Missing required file: {self.get_oss_fuzz_path() / self._helper_path}")
|
||||
|
||||
self._check_python_path()
|
||||
|
||||
def _check_dir_exists(self, path: Path) -> None:
|
||||
if not path.exists():
|
||||
raise ChallengeTaskError(f"Missing required directory: {path}")
|
||||
|
||||
if not path.is_dir():
|
||||
raise ChallengeTaskError(f"Required directory is not a directory: {path}")
|
||||
|
||||
def _find_first_dir(self, subpath: Path) -> Path | None:
|
||||
first_elem = next((self.task_dir / subpath).iterdir(), None)
|
||||
if first_elem is None:
|
||||
return None
|
||||
return first_elem.relative_to(self.task_dir)
|
||||
|
||||
def get_source_subpath(self) -> Path | None:
|
||||
# TODO: "Review task structure and Challenge Task operations" Issue #74
|
||||
return self._find_first_dir(self.SRC_DIR)
|
||||
|
||||
def get_diff_subpath(self) -> Path | None:
|
||||
# TODO: "Review task structure and Challenge Task operations" Issue #74
|
||||
return self._find_first_dir(self.DIFF_DIR)
|
||||
|
||||
def get_oss_fuzz_subpath(self) -> Path | None:
|
||||
# TODO: "Review task structure and Challenge Task operations" Issue #74
|
||||
return self._find_first_dir(self.OSS_FUZZ_DIR)
|
||||
|
||||
def _task_dir_compose_path(self, subpath_method: Callable[[], Path | None]) -> Path | None:
|
||||
subpath = subpath_method()
|
||||
if subpath is None:
|
||||
return None
|
||||
return self.task_dir / subpath
|
||||
|
||||
def get_source_path(self) -> Path | None:
|
||||
return self._task_dir_compose_path(self.get_source_subpath)
|
||||
|
||||
def get_diff_path(self) -> Path | None:
|
||||
return self._task_dir_compose_path(self.get_diff_subpath)
|
||||
|
||||
def get_oss_fuzz_path(self) -> Path | None:
|
||||
return self._task_dir_compose_path(self.get_oss_fuzz_subpath)
|
||||
|
||||
def _check_python_path(self) -> None:
|
||||
"""Check if the configured python_path is available in system PATH."""
|
||||
try:
|
||||
subprocess.run([self.python_path, "--version"], check=False, capture_output=True, text=True)
|
||||
except Exception as e:
|
||||
raise ChallengeTaskError(f"Python executable couldn't be run: {self.python_path}") from e
|
||||
|
||||
@property
|
||||
def task_dir(self) -> Path:
|
||||
if self.local_task_dir is None:
|
||||
return Path(self.read_only_task_dir)
|
||||
return Path(self.local_task_dir)
|
||||
|
||||
def read_write_decorator(func: Callable) -> Callable:
|
||||
"""Decorator to check if the task is read-only."""
|
||||
|
||||
def wrapper(self, *args: Any, **kwargs: Any) -> Any:
|
||||
if self.local_task_dir is None:
|
||||
raise ChallengeTaskError("Challenge Task is read-only, cannot perform this operation")
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
def _add_optional_arg(self, cmd: list[str], flag: str, arg: Any | None):
|
||||
if arg is not None:
|
||||
if isinstance(arg, bool):
|
||||
if arg:
|
||||
cmd.append(flag)
|
||||
else:
|
||||
cmd.append(flag)
|
||||
cmd.append(str(arg))
|
||||
|
||||
def _get_helper_cmd(self, helper_cmd: str, *args: Any, **kwargs: Any) -> list[str]:
|
||||
cmd = [str(self.python_path), str(self._helper_path), helper_cmd]
|
||||
for key, value in kwargs.items():
|
||||
if key == "e":
|
||||
for k, v in value.items() if isinstance(value, dict) else {}:
|
||||
cmd.append("-e")
|
||||
cmd.append(f"{k}={v}")
|
||||
else:
|
||||
self._add_optional_arg(cmd, f"--{key}", value)
|
||||
|
||||
for arg in args:
|
||||
if arg is not None:
|
||||
if isinstance(arg, list):
|
||||
cmd.extend(arg)
|
||||
else:
|
||||
cmd.append(arg)
|
||||
|
||||
return cmd
|
||||
|
||||
def _log_output_line(self, current_line: bytes, new_data: bytes) -> bytes:
|
||||
current_line += new_data
|
||||
line_to_print = b""
|
||||
if b"\n" in current_line:
|
||||
line_to_print = current_line[: current_line.index(b"\n")]
|
||||
current_line = current_line[current_line.index(b"\n") + 1 :]
|
||||
logger.debug(line_to_print.decode())
|
||||
|
||||
return current_line
|
||||
|
||||
def _run_helper_cmd(self, cmd: list[str]) -> CommandResult:
|
||||
try:
|
||||
logger.debug(f"Running command (cwd={self.task_dir / self.get_oss_fuzz_subpath()}): {' '.join(cmd)}")
|
||||
process = subprocess.Popen( # noqa: S603
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=self.task_dir / self.get_oss_fuzz_subpath(),
|
||||
)
|
||||
|
||||
# Poll process for new output until finished
|
||||
stdout = b""
|
||||
stderr = b""
|
||||
current_output_line = b""
|
||||
current_error_line = b""
|
||||
while True:
|
||||
stdout_line = process.stdout.readline() if process.stdout else b""
|
||||
stderr_line = process.stderr.readline() if process.stderr else b""
|
||||
if stdout_line:
|
||||
current_output_line = self._log_output_line(current_output_line, stdout_line)
|
||||
stdout += stdout_line
|
||||
|
||||
if stderr_line:
|
||||
current_error_line = self._log_output_line(current_error_line, stderr_line)
|
||||
stderr += stderr_line
|
||||
|
||||
# Break if process has finished and we've read all output
|
||||
if not stdout_line and not stderr_line and process.poll() is not None:
|
||||
break
|
||||
|
||||
returncode = process.wait()
|
||||
|
||||
return CommandResult(
|
||||
success=returncode == 0,
|
||||
error=stderr,
|
||||
output=stdout,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed (cwd={self.task_dir / self.get_oss_fuzz_subpath()}): {' '.join(cmd)}")
|
||||
return CommandResult(
|
||||
success=False, error=e.stderr if e.stderr else None, output=e.stdout if e.stdout else None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Command failed (cwd={self.task_dir / self.get_oss_fuzz_subpath()}): {' '.join(cmd)}")
|
||||
return CommandResult(success=False, error=str(e), output=None)
|
||||
|
||||
@read_write_decorator
|
||||
def build_image(
|
||||
self,
|
||||
*,
|
||||
pull_latest_base_image: bool = False,
|
||||
cache: bool | None = None,
|
||||
architecture: str | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Building image for project %s | pull_latest_base_image=%s | cache=%s | architecture=%s",
|
||||
self.project_name,
|
||||
pull_latest_base_image,
|
||||
cache,
|
||||
architecture,
|
||||
)
|
||||
kwargs = {
|
||||
"pull": pull_latest_base_image,
|
||||
"no-pull": not pull_latest_base_image,
|
||||
"cache": cache,
|
||||
"architecture": architecture,
|
||||
}
|
||||
cmd = self._get_helper_cmd(
|
||||
"build_image",
|
||||
self.project_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return self._run_helper_cmd(cmd)
|
||||
|
||||
@read_write_decorator
|
||||
def build_fuzzers(
|
||||
self,
|
||||
use_source_dir: bool = True,
|
||||
*,
|
||||
architecture: str | None = None,
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Building fuzzers for project %s | architecture=%s | engine=%s | sanitizer=%s | env=%s | use_source_dir=%s",
|
||||
self.project_name,
|
||||
architecture,
|
||||
engine,
|
||||
sanitizer,
|
||||
env,
|
||||
use_source_dir,
|
||||
)
|
||||
cmd = self._get_helper_cmd(
|
||||
"build_fuzzers",
|
||||
self.project_name,
|
||||
str((self.task_dir / self.get_source_subpath()).absolute()) if use_source_dir else None,
|
||||
architecture=architecture,
|
||||
engine=engine,
|
||||
sanitizer=sanitizer,
|
||||
e=env,
|
||||
)
|
||||
|
||||
return self._run_helper_cmd(cmd)
|
||||
|
||||
@read_write_decorator
|
||||
def build_fuzzers_with_cache(
|
||||
self,
|
||||
use_source_dir: bool = True,
|
||||
*,
|
||||
architecture: str | None = None,
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
check_build_res = self.check_build(architecture=architecture, engine=engine, sanitizer=sanitizer, env=env)
|
||||
if check_build_res.success:
|
||||
logger.info("Build is up to date, skipping building fuzzers")
|
||||
return check_build_res
|
||||
|
||||
return self.build_fuzzers(
|
||||
use_source_dir=use_source_dir,
|
||||
architecture=architecture,
|
||||
engine=engine,
|
||||
sanitizer=sanitizer,
|
||||
env=env,
|
||||
)
|
||||
|
||||
@read_write_decorator
|
||||
def check_build(
|
||||
self,
|
||||
*,
|
||||
architecture: str | None = None,
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Checking build for project %s | architecture=%s | engine=%s | sanitizer=%s | env=%s",
|
||||
self.project_name,
|
||||
architecture,
|
||||
engine,
|
||||
sanitizer,
|
||||
env,
|
||||
)
|
||||
cmd = self._get_helper_cmd(
|
||||
"check_build",
|
||||
self.project_name,
|
||||
architecture=architecture,
|
||||
engine=engine,
|
||||
sanitizer=sanitizer,
|
||||
e=env,
|
||||
)
|
||||
|
||||
return self._run_helper_cmd(cmd)
|
||||
|
||||
@read_write_decorator
|
||||
def reproduce_pov(
|
||||
self,
|
||||
fuzzer_name: str,
|
||||
crash_path: Path,
|
||||
fuzzer_args: list[str] | None = None,
|
||||
*,
|
||||
architecture: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Reproducing POV for project %s | fuzzer_name=%s | crash_path=%s | fuzzer_args=%s | architecture=%s | env=%s",
|
||||
self.project_name,
|
||||
fuzzer_name,
|
||||
crash_path,
|
||||
fuzzer_args,
|
||||
architecture,
|
||||
env,
|
||||
)
|
||||
cmd = self._get_helper_cmd(
|
||||
"reproduce",
|
||||
self.project_name,
|
||||
fuzzer_name,
|
||||
str(crash_path.absolute()),
|
||||
fuzzer_args,
|
||||
architecture=architecture,
|
||||
e=env,
|
||||
)
|
||||
|
||||
return self._run_helper_cmd(cmd)
|
||||
|
||||
@contextmanager
|
||||
def get_rw_copy(self, work_dir: PathLike | None = None, delete: bool = True) -> Iterator[ChallengeTask]:
|
||||
"""Create a copy of this task in a new writable directory.
|
||||
Returns a context manager that yields a new ChallengeTask instance pointing to the new copy.
|
||||
|
||||
Example:
|
||||
with task.get_rw_copy() as local_task:
|
||||
local_task.build_fuzzers()
|
||||
"""
|
||||
if self.local_task_dir is not None:
|
||||
yield self
|
||||
return
|
||||
|
||||
work_dir = Path(work_dir) if work_dir else None
|
||||
with create_tmp_dir(work_dir, delete, prefix=self.task_dir.name + "-") as tmp_dir:
|
||||
# Copy the entire task directory to the temporary location
|
||||
logger.info(f"Copying task directory {self.task_dir} to {tmp_dir}")
|
||||
copyanything(self.task_dir, tmp_dir, symlinks=True)
|
||||
|
||||
# Create a new ChallengeTask instance pointing to the copy
|
||||
copied_task = ChallengeTask(
|
||||
read_only_task_dir=self.read_only_task_dir,
|
||||
project_name=self.project_name,
|
||||
python_path=self.python_path,
|
||||
local_task_dir=tmp_dir,
|
||||
)
|
||||
|
||||
try:
|
||||
yield copied_task
|
||||
finally:
|
||||
pass
|
||||
|
||||
def commit(self, suffix: str | None = None) -> None:
|
||||
"""Commit the local task directory to a stable path.
|
||||
|
||||
This is useful to save the task state for later use and together with
|
||||
the `get_rw_copy` context manager.
|
||||
"""
|
||||
if self.local_task_dir is None:
|
||||
raise ChallengeTaskError("Challenge Task is read-only, cannot commit")
|
||||
|
||||
assert isinstance(self.local_task_dir, Path)
|
||||
new_local_task_dir = None
|
||||
max_retries = self.MAX_COMMIT_RETRIES if suffix is None else 1
|
||||
for i in range(max_retries):
|
||||
suffix = suffix if suffix is not None else str(uuid.uuid4())[:16]
|
||||
new_name = f"{self.read_only_task_dir.name}-{suffix}"
|
||||
try:
|
||||
logger.info(f"Committing task {self.local_task_dir} to {new_name}")
|
||||
new_local_task_dir = self.local_task_dir.rename(self.local_task_dir.parent / new_name)
|
||||
logger.info(f"Committed task {self.local_task_dir} to {new_name}")
|
||||
break
|
||||
except OSError as e:
|
||||
if i == max_retries - 1:
|
||||
raise ChallengeTaskError("Failed to commit task") from e
|
||||
|
||||
logger.error(
|
||||
f"Failed to commit task {self.local_task_dir} to {new_name}. Retrying with a random suffix..."
|
||||
)
|
||||
suffix = None
|
||||
|
||||
self.local_task_dir = new_local_task_dir
|
||||
@@ -0,0 +1,126 @@
|
||||
from pydantic_settings import BaseSettings, CliSubCommand, get_subcommand, CliImplicitFlag
|
||||
from pydantic import BaseModel
|
||||
from buttercup.common.challenge_task import ChallengeTask
|
||||
from buttercup.common.logger import setup_logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class BuildImageCommand(BaseModel):
|
||||
pull_latest_base_image: bool = False
|
||||
cache: bool | None = None
|
||||
architecture: str | None = None
|
||||
|
||||
|
||||
class BuildFuzzersCommand(BaseModel):
|
||||
architecture: str | None = None
|
||||
engine: str | None = None
|
||||
sanitizer: str | None = None
|
||||
env: Dict[str, str] | None = None
|
||||
use_cache: bool = True
|
||||
|
||||
|
||||
class CheckBuildCommand(BaseModel):
|
||||
architecture: str | None = None
|
||||
engine: str | None = None
|
||||
sanitizer: str | None = None
|
||||
env: Dict[str, str] | None = None
|
||||
|
||||
|
||||
class ReproducePovCommand(BaseModel):
|
||||
fuzzer_name: str
|
||||
crash_path: Path
|
||||
fuzzer_args: list[str] | None = None
|
||||
architecture: str | None = None
|
||||
env: Dict[str, str] | None = None
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
task_dir: Path
|
||||
project_name: str
|
||||
python_path: Path = Path("python")
|
||||
rw: CliImplicitFlag[bool] = False
|
||||
|
||||
build_image: CliSubCommand[BuildImageCommand]
|
||||
build_fuzzers: CliSubCommand[BuildFuzzersCommand]
|
||||
check_build: CliSubCommand[CheckBuildCommand]
|
||||
reproduce_pov: CliSubCommand[ReproducePovCommand]
|
||||
|
||||
class Config:
|
||||
env_prefix = "BUTTERCUP_CHALLENGE_TASK_"
|
||||
env_file = ".env"
|
||||
cli_parse_args = True
|
||||
nested_model_default_partial_update = True
|
||||
env_nested_delimiter = "__"
|
||||
extra = "allow"
|
||||
|
||||
|
||||
def handle_subcommand(task: ChallengeTask, subcommand: BaseModel):
|
||||
if isinstance(subcommand, BuildImageCommand):
|
||||
return task.build_image(
|
||||
pull_latest_base_image=subcommand.pull_latest_base_image,
|
||||
cache=subcommand.cache,
|
||||
architecture=subcommand.architecture,
|
||||
)
|
||||
elif isinstance(subcommand, BuildFuzzersCommand):
|
||||
if subcommand.use_cache:
|
||||
return task.build_fuzzers_with_cache(
|
||||
architecture=subcommand.architecture,
|
||||
engine=subcommand.engine,
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
else:
|
||||
return task.build_fuzzers(
|
||||
architecture=subcommand.architecture,
|
||||
engine=subcommand.engine,
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
elif isinstance(subcommand, CheckBuildCommand):
|
||||
return task.check_build(
|
||||
architecture=subcommand.architecture,
|
||||
engine=subcommand.engine,
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
elif isinstance(subcommand, ReproducePovCommand):
|
||||
return task.reproduce_pov(
|
||||
fuzzer_name=subcommand.fuzzer_name,
|
||||
crash_path=subcommand.crash_path,
|
||||
fuzzer_args=subcommand.fuzzer_args,
|
||||
architecture=subcommand.architecture,
|
||||
env=subcommand.env,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown subcommand: {subcommand}")
|
||||
|
||||
|
||||
def main():
|
||||
settings = Settings()
|
||||
logger = setup_logging(__name__, "DEBUG")
|
||||
if settings.rw:
|
||||
task = ChallengeTask(
|
||||
read_only_task_dir=settings.task_dir,
|
||||
project_name=settings.project_name,
|
||||
python_path=settings.python_path,
|
||||
local_task_dir=settings.task_dir,
|
||||
logger=logger,
|
||||
)
|
||||
else:
|
||||
task = ChallengeTask(
|
||||
read_only_task_dir=settings.task_dir,
|
||||
project_name=settings.project_name,
|
||||
python_path=settings.python_path,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
subcommand = get_subcommand(settings)
|
||||
with task.get_rw_copy(delete=False) as rw_task:
|
||||
result = handle_subcommand(rw_task, subcommand)
|
||||
|
||||
print("Command result:", result.success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,12 +1,46 @@
|
||||
import shutil
|
||||
import errno
|
||||
import tempfile
|
||||
import contextlib
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator, Any
|
||||
from tempfile import TemporaryDirectory
|
||||
from buttercup.common.logger import setup_logging
|
||||
from pathlib import Path
|
||||
from os import PathLike
|
||||
|
||||
|
||||
def copyanything(src, dst):
|
||||
def copyanything(src: PathLike, dst: PathLike, **kwargs: Any) -> None:
|
||||
"""Copy a file or directory to a destination.
|
||||
This function will:
|
||||
- Copy directories recursively
|
||||
- Copy single files
|
||||
- Handle existing destinations
|
||||
"""
|
||||
src, dst = Path(src), Path(dst)
|
||||
try:
|
||||
shutil.copytree(src, dst)
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True, **kwargs)
|
||||
except OSError as exc: # python >2.5
|
||||
if exc.errno in (errno.ENOTDIR, errno.EINVAL):
|
||||
shutil.copy(src, dst)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_tmp_dir(work_dir: Path | None, delete: bool = True, prefix: str | None = None) -> Iterator[Path]:
|
||||
"""Create a temporary directory inside a working dir and either keep or
|
||||
delete it after use."""
|
||||
logger = setup_logging(__name__)
|
||||
if work_dir:
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if delete:
|
||||
try:
|
||||
with TemporaryDirectory(dir=work_dir, prefix=prefix) as tmp_dir:
|
||||
yield Path(tmp_dir)
|
||||
except PermissionError:
|
||||
logger.warning("Issues while creating/deleting a temporary directory...")
|
||||
else:
|
||||
with contextlib.nullcontext(tempfile.mkdtemp(dir=work_dir, prefix=prefix)) as tmp_dir:
|
||||
yield Path(tmp_dir)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--runintegration", action="store_true", default=False, help="run integration tests")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "integration: mark test as an integration test")
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
if config.getoption("--runintegration"):
|
||||
# --runintegration given in cli: do not skip integration tests
|
||||
return
|
||||
skip_integration = pytest.mark.skip(reason="need --runintegration option to run")
|
||||
for item in items:
|
||||
if "integration" in item.keywords:
|
||||
item.add_marker(skip_integration)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 104 B |
@@ -0,0 +1,477 @@
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
import subprocess
|
||||
from buttercup.common.challenge_task import ChallengeTask, ChallengeTaskError
|
||||
import logging
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def task_dir(tmp_path: Path) -> Path:
|
||||
"""Create a mock challenge task directory structure."""
|
||||
# Create the main directories
|
||||
oss_fuzz = tmp_path / "fuzz-tooling" / "my-oss-fuzz"
|
||||
source = tmp_path / "src" / "my-source"
|
||||
diffs = tmp_path / "diff" / "my-diff"
|
||||
|
||||
oss_fuzz.mkdir(parents=True, exist_ok=True)
|
||||
source.mkdir(parents=True, exist_ok=True)
|
||||
diffs.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create some mock patch files
|
||||
(diffs / "patch1.diff").write_text("mock patch 1")
|
||||
(diffs / "patch2.diff").write_text("mock patch 2")
|
||||
|
||||
# Create a mock helper.py file
|
||||
helper_path = oss_fuzz / "infra/helper.py"
|
||||
helper_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
helper_path.write_text("import sys;\nsys.exit(0)\n")
|
||||
|
||||
# Create a mock test.txt file
|
||||
(source / "test.txt").write_text("mock test content")
|
||||
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def challenge_task_readonly(task_dir: Path) -> ChallengeTask:
|
||||
"""Create a mock challenge task for testing."""
|
||||
return ChallengeTask(
|
||||
read_only_task_dir=task_dir,
|
||||
project_name="example_project",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def challenge_task(task_dir: Path) -> ChallengeTask:
|
||||
"""Create a mock challenge task for testing."""
|
||||
return ChallengeTask(
|
||||
read_only_task_dir=task_dir,
|
||||
local_task_dir=task_dir,
|
||||
project_name="example_project",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def challenge_task_custom_python(task_dir: Path) -> ChallengeTask:
|
||||
"""Create a mock challenge task with custom python path for testing."""
|
||||
return ChallengeTask(
|
||||
read_only_task_dir=task_dir,
|
||||
local_task_dir=task_dir,
|
||||
project_name="example_project",
|
||||
python_path="/usr/bin/python3",
|
||||
)
|
||||
|
||||
|
||||
def get_mock_popen(returncode: int, stdout: list[bytes], stderr: list[bytes]):
|
||||
with patch("subprocess.Popen") as mock_popen:
|
||||
mock_process = Mock()
|
||||
mock_process.returncode = returncode
|
||||
|
||||
# Create mock pipe for stdout that handles multiple readline() calls gracefully
|
||||
class MockStd:
|
||||
def __init__(self, lines: list[bytes]):
|
||||
self.lines = lines
|
||||
self.current_line = 0
|
||||
|
||||
def readline(self):
|
||||
if self.current_line < len(self.lines):
|
||||
line = self.lines[self.current_line]
|
||||
self.current_line += 1
|
||||
return line
|
||||
return b"" # Return empty bytes when no more lines
|
||||
|
||||
mock_process.stdout = MockStd(stdout)
|
||||
|
||||
# Create mock pipe for stderr
|
||||
mock_process.stderr = MockStd(stderr)
|
||||
|
||||
# Setup poll and wait behavior
|
||||
mock_process.poll.side_effect = [None, None, returncode]
|
||||
mock_process.wait.return_value = returncode
|
||||
|
||||
# Make Popen return our mock process
|
||||
mock_popen.return_value = mock_process
|
||||
yield mock_popen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_subprocess():
|
||||
"""Mock subprocess calls for testing."""
|
||||
yield from get_mock_popen(0, [b"output line 1\n", b"output line 2\n"], [b"error line 1\n", b"error line 2\n"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_failed_subprocess():
|
||||
"""Mock subprocess calls for testing."""
|
||||
yield from get_mock_popen(1, [], [b"Build failed"])
|
||||
|
||||
|
||||
def test_directory_structure(challenge_task: ChallengeTask):
|
||||
"""Test that the challenge task correctly identifies its directory structure."""
|
||||
assert challenge_task.get_oss_fuzz_subpath() == Path("fuzz-tooling") / "my-oss-fuzz"
|
||||
assert challenge_task.get_source_subpath() == Path("src") / "my-source"
|
||||
assert challenge_task.get_diff_subpath() == Path("diff") / "my-diff"
|
||||
|
||||
assert (challenge_task.task_dir / challenge_task.get_oss_fuzz_subpath()).is_dir()
|
||||
assert (challenge_task.task_dir / challenge_task.get_source_subpath()).is_dir()
|
||||
assert (challenge_task.task_dir / challenge_task.get_diff_subpath()).is_dir()
|
||||
|
||||
assert challenge_task.get_source_path() == challenge_task.task_dir / challenge_task.get_source_subpath()
|
||||
assert challenge_task.get_diff_path() == challenge_task.task_dir / challenge_task.get_diff_subpath()
|
||||
assert challenge_task.get_oss_fuzz_path() == challenge_task.task_dir / challenge_task.get_oss_fuzz_subpath()
|
||||
|
||||
|
||||
def test_readonly_task(challenge_task_readonly: ChallengeTask):
|
||||
"""Test that a readonly task raises an error when trying to build."""
|
||||
with pytest.raises(ChallengeTaskError, match="Challenge Task is read-only, cannot perform this operation"):
|
||||
challenge_task_readonly.build_image()
|
||||
|
||||
with pytest.raises(ChallengeTaskError, match="Challenge Task is read-only, cannot perform this operation"):
|
||||
challenge_task_readonly.build_fuzzers(engine="libfuzzer", sanitizer="address")
|
||||
|
||||
with pytest.raises(ChallengeTaskError, match="Challenge Task is read-only, cannot perform this operation"):
|
||||
challenge_task_readonly.reproduce_pov(fuzzer_name="fuzz_target", crash_path=Path("crash-sample"))
|
||||
|
||||
|
||||
def test_build_image(challenge_task: ChallengeTask, mock_subprocess):
|
||||
"""Test building the docker image for the project."""
|
||||
result = challenge_task.build_image()
|
||||
|
||||
assert result.success is True
|
||||
assert result.output is not None
|
||||
assert result.output == b"output line 1\noutput line 2\n"
|
||||
mock_subprocess.assert_called_once()
|
||||
|
||||
# Verify the command and working directory
|
||||
args, kwargs = mock_subprocess.call_args
|
||||
assert args[0] == ["python", "infra/helper.py", "build_image", "--no-pull", "example_project"]
|
||||
assert kwargs["cwd"] == challenge_task.task_dir / challenge_task.get_oss_fuzz_subpath()
|
||||
|
||||
# Verify output was read
|
||||
mock_process = mock_subprocess.return_value
|
||||
assert mock_process.wait.called # Verify we waited for process completion
|
||||
|
||||
|
||||
def test_build_fuzzers(challenge_task: ChallengeTask, mock_subprocess):
|
||||
"""Test building fuzzers with different configurations."""
|
||||
result = challenge_task.build_fuzzers(
|
||||
engine="libfuzzer",
|
||||
sanitizer="address",
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
mock_subprocess.assert_called_once()
|
||||
args, kwargs = mock_subprocess.call_args
|
||||
assert args[0][:-1] == [
|
||||
"python",
|
||||
"infra/helper.py",
|
||||
"build_fuzzers",
|
||||
"--engine",
|
||||
"libfuzzer",
|
||||
"--sanitizer",
|
||||
"address",
|
||||
"example_project",
|
||||
]
|
||||
assert args[0][-1].endswith(str(challenge_task.get_source_subpath()))
|
||||
assert kwargs["cwd"] == challenge_task.task_dir / challenge_task.get_oss_fuzz_subpath()
|
||||
|
||||
|
||||
def test_check_build(challenge_task: ChallengeTask, mock_subprocess):
|
||||
"""Test checking the build status."""
|
||||
result = challenge_task.check_build(engine="libfuzzer", sanitizer="address")
|
||||
|
||||
assert result.success is True
|
||||
mock_subprocess.assert_called_once()
|
||||
args, kwargs = mock_subprocess.call_args
|
||||
assert args[0] == [
|
||||
"python",
|
||||
"infra/helper.py",
|
||||
"check_build",
|
||||
"--engine",
|
||||
"libfuzzer",
|
||||
"--sanitizer",
|
||||
"address",
|
||||
"example_project",
|
||||
]
|
||||
assert kwargs["cwd"] == challenge_task.task_dir / challenge_task.get_oss_fuzz_subpath()
|
||||
|
||||
|
||||
def test_reproduce_pov(challenge_task: ChallengeTask, mock_subprocess):
|
||||
"""Test reproducing a proof of vulnerability."""
|
||||
pov_file = Path("crash-sample")
|
||||
result = challenge_task.reproduce_pov(
|
||||
fuzzer_name="fuzz_target",
|
||||
crash_path=pov_file,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
mock_subprocess.assert_called_once()
|
||||
args, kwargs = mock_subprocess.call_args
|
||||
assert args[0][:-1] == ["python", "infra/helper.py", "reproduce", "example_project", "fuzz_target"]
|
||||
assert args[0][-1].endswith("/crash-sample")
|
||||
assert kwargs["cwd"] == challenge_task.task_dir / challenge_task.get_oss_fuzz_subpath()
|
||||
|
||||
|
||||
def test_failed_build_image(challenge_task: ChallengeTask, mock_failed_subprocess):
|
||||
"""Test handling failed build image operation."""
|
||||
result = challenge_task.build_image()
|
||||
|
||||
assert result.success is False
|
||||
assert result.error == b"Build failed"
|
||||
|
||||
|
||||
def test_invalid_task_dir():
|
||||
"""Test handling of invalid task directory."""
|
||||
with pytest.raises(ChallengeTaskError, match="Missing required directory: /nonexistent"):
|
||||
ChallengeTask(
|
||||
read_only_task_dir=Path("/nonexistent"),
|
||||
project_name="example_project",
|
||||
)
|
||||
|
||||
|
||||
def test_missing_required_dirs(tmp_path: Path):
|
||||
"""Test handling of missing required directories."""
|
||||
# Create task dir without required subdirectories
|
||||
task_dir = tmp_path / "task"
|
||||
task_dir.mkdir()
|
||||
|
||||
with pytest.raises(ChallengeTaskError, match=f"Missing required directory: {task_dir / 'src'}"):
|
||||
ChallengeTask(
|
||||
read_only_task_dir=task_dir,
|
||||
project_name="example_project",
|
||||
)
|
||||
|
||||
|
||||
def test_build_image_custom_python(challenge_task_custom_python: ChallengeTask, mock_subprocess):
|
||||
"""Test building the docker image using custom python path."""
|
||||
result = challenge_task_custom_python.build_image()
|
||||
|
||||
assert result.success is True
|
||||
mock_subprocess.assert_called_once()
|
||||
args, kwargs = mock_subprocess.call_args
|
||||
assert args[0] == ["/usr/bin/python3", "infra/helper.py", "build_image", "--no-pull", "example_project"]
|
||||
assert kwargs["cwd"] == challenge_task_custom_python.task_dir / challenge_task_custom_python.get_oss_fuzz_subpath()
|
||||
|
||||
|
||||
def test_build_fuzzers_custom_python(challenge_task_custom_python: ChallengeTask, mock_subprocess):
|
||||
"""Test building fuzzers with custom python path."""
|
||||
result = challenge_task_custom_python.build_fuzzers(
|
||||
engine="libfuzzer",
|
||||
sanitizer="address",
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
mock_subprocess.assert_called_once()
|
||||
args, kwargs = mock_subprocess.call_args
|
||||
assert args[0][:-1] == [
|
||||
"/usr/bin/python3",
|
||||
"infra/helper.py",
|
||||
"build_fuzzers",
|
||||
"--engine",
|
||||
"libfuzzer",
|
||||
"--sanitizer",
|
||||
"address",
|
||||
"example_project",
|
||||
]
|
||||
assert args[0][-1].endswith(str(challenge_task_custom_python.get_source_subpath()))
|
||||
assert kwargs["cwd"] == challenge_task_custom_python.task_dir / challenge_task_custom_python.get_oss_fuzz_subpath()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def libjpeg_oss_fuzz_task(tmp_path: Path) -> ChallengeTask:
|
||||
"""Create a challenge task using a real OSS-Fuzz repository."""
|
||||
# Clone real oss-fuzz repo into temp dir
|
||||
oss_fuzz_dir = tmp_path / "fuzz-tooling"
|
||||
oss_fuzz_dir.mkdir(parents=True)
|
||||
source_dir = tmp_path / "src"
|
||||
source_dir.mkdir(parents=True)
|
||||
|
||||
subprocess.run(["git", "-C", str(oss_fuzz_dir), "clone", "https://github.com/google/oss-fuzz.git"], check=True)
|
||||
# Restore libjpeg-turbo project directory to specific commit
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(oss_fuzz_dir / "oss-fuzz"),
|
||||
"checkout",
|
||||
"a5d517924f758028f299d7c6cecf3b471503a202",
|
||||
"--",
|
||||
"projects/libjpeg-turbo",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Download libpng source code
|
||||
libjpeg_url = "https://github.com/libjpeg-turbo/libjpeg-turbo"
|
||||
# Checkout specific libjpeg commit for reproducibility
|
||||
subprocess.run(["git", "-C", str(source_dir), "clone", libjpeg_url], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(source_dir / "libjpeg-turbo"), "checkout", "6d91e950c871103a11bac2f10c63bf998796c719"],
|
||||
check=True,
|
||||
)
|
||||
|
||||
return ChallengeTask(
|
||||
read_only_task_dir=tmp_path,
|
||||
local_task_dir=tmp_path,
|
||||
project_name="libjpeg-turbo",
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def libjpeg_crash_testcase() -> Path:
|
||||
"""Get libjpeg-turbo crash testcase"""
|
||||
crash_file = Path(__file__).parent / "data" / "libjpeg-crash"
|
||||
if not crash_file.exists():
|
||||
raise FileNotFoundError(f"Crash file not found at {crash_file}")
|
||||
|
||||
return crash_file
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_real_build_workflow(libjpeg_oss_fuzz_task: ChallengeTask):
|
||||
"""Test the full build workflow using actual OSS-Fuzz repository."""
|
||||
# Build the base image
|
||||
result = libjpeg_oss_fuzz_task.build_image(pull_latest_base_image=False)
|
||||
assert result.success is True, f"Build image failed: {result.error}"
|
||||
|
||||
# Build the fuzzers
|
||||
result = libjpeg_oss_fuzz_task.build_fuzzers(
|
||||
engine="libfuzzer",
|
||||
sanitizer="address",
|
||||
architecture="x86_64",
|
||||
)
|
||||
assert result.success is True, f"Build fuzzers failed: {result.error}"
|
||||
|
||||
# Check the build
|
||||
result = libjpeg_oss_fuzz_task.check_build(
|
||||
engine="libfuzzer",
|
||||
sanitizer="address",
|
||||
architecture="x86_64",
|
||||
)
|
||||
assert result.success is True, f"Check build failed: {result.error}"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_build_fuzzers_with_cache(libjpeg_oss_fuzz_task: ChallengeTask):
|
||||
"""Test building fuzzers with cache."""
|
||||
result = libjpeg_oss_fuzz_task.build_fuzzers_with_cache(
|
||||
engine="libfuzzer",
|
||||
sanitizer="address",
|
||||
)
|
||||
assert result.success is True
|
||||
assert result.output is not None
|
||||
assert "cp /src/libjpeg_turbo_fuzzer_seed_corpus.zip /out/" in result.output.decode()
|
||||
|
||||
result = libjpeg_oss_fuzz_task.build_fuzzers_with_cache(
|
||||
engine="libfuzzer",
|
||||
sanitizer="address",
|
||||
)
|
||||
assert result.success is True
|
||||
assert result.output is not None
|
||||
assert "Check build passed" in result.output.decode()
|
||||
assert "cp /src/libjpeg_turbo_fuzzer_seed_corpus.zip /out/" not in result.output.decode()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_real_reproduce_pov(libjpeg_oss_fuzz_task: ChallengeTask, libjpeg_crash_testcase: Path):
|
||||
"""Test the reproduce POV workflow using actual OSS-Fuzz repository."""
|
||||
# Reproduce the POV
|
||||
libjpeg_oss_fuzz_task.build_image(pull_latest_base_image=False)
|
||||
libjpeg_oss_fuzz_task.build_fuzzers(engine="libfuzzer", sanitizer="address")
|
||||
result = libjpeg_oss_fuzz_task.reproduce_pov(
|
||||
fuzzer_name="libjpeg_turbo_fuzzer",
|
||||
crash_path=libjpeg_crash_testcase,
|
||||
)
|
||||
assert result.success is False, "Reproduce POV failed"
|
||||
|
||||
|
||||
def test_copy_task(challenge_task_readonly: ChallengeTask, mock_subprocess):
|
||||
"""Test copying a challenge task to a temporary directory."""
|
||||
with patch.object(ChallengeTask, "_check_python_path"):
|
||||
with challenge_task_readonly.get_rw_copy() as local_task:
|
||||
# Verify the task directory is different
|
||||
assert local_task.task_dir != challenge_task_readonly.task_dir
|
||||
|
||||
# Verify the file structure was copied
|
||||
assert local_task.get_source_path().is_dir()
|
||||
assert local_task.get_oss_fuzz_path().is_dir()
|
||||
assert local_task.get_diff_path().is_dir()
|
||||
|
||||
# Verify file contents were copied
|
||||
copied_file = local_task.get_source_path() / "test.txt"
|
||||
assert copied_file.exists()
|
||||
assert copied_file.read_text() == "mock test content"
|
||||
|
||||
# Verify we can modify the copy without affecting the original
|
||||
new_content = "modified content"
|
||||
copied_file.write_text(new_content)
|
||||
assert copied_file.read_text() != (challenge_task_readonly.get_source_path() / "test.txt").read_text()
|
||||
|
||||
result = local_task.build_image()
|
||||
assert result.success is True
|
||||
assert result.output is not None
|
||||
assert result.output == b"output line 1\noutput line 2\n"
|
||||
mock_subprocess.assert_called_once()
|
||||
|
||||
# Verify the temporary directory was cleaned up
|
||||
assert not local_task.task_dir.exists()
|
||||
|
||||
|
||||
def test_commit_task(challenge_task_readonly: ChallengeTask, mock_subprocess):
|
||||
"""Test committing a challenge task."""
|
||||
with patch.object(ChallengeTask, "_check_python_path"):
|
||||
with challenge_task_readonly.get_rw_copy() as local_task:
|
||||
local_task.build_image()
|
||||
local_task.build_fuzzers(engine="libfuzzer", sanitizer="address")
|
||||
old_local_dir = local_task.task_dir
|
||||
local_task.commit()
|
||||
commited_local_dir = local_task.task_dir
|
||||
assert commited_local_dir != old_local_dir
|
||||
assert commited_local_dir.exists()
|
||||
assert commited_local_dir.is_dir()
|
||||
assert not old_local_dir.exists()
|
||||
|
||||
commited_task = ChallengeTask(
|
||||
read_only_task_dir=commited_local_dir,
|
||||
local_task_dir=commited_local_dir,
|
||||
project_name="example_project",
|
||||
)
|
||||
assert commited_task.get_source_path().exists()
|
||||
assert commited_task.get_oss_fuzz_path().exists()
|
||||
assert commited_task.get_diff_path().exists()
|
||||
|
||||
with pytest.raises(ChallengeTaskError, match="Missing required directory"):
|
||||
ChallengeTask(
|
||||
read_only_task_dir=old_local_dir,
|
||||
local_task_dir=old_local_dir,
|
||||
project_name="example_project",
|
||||
)
|
||||
|
||||
|
||||
def test_commit_task_with_suffix(challenge_task_readonly: ChallengeTask, mock_subprocess, tmp_path: Path):
|
||||
"""Test committing a challenge task with a suffix."""
|
||||
with patch.object(ChallengeTask, "_check_python_path"):
|
||||
with challenge_task_readonly.get_rw_copy(work_dir=tmp_path) as local_task:
|
||||
local_task.commit(suffix="test")
|
||||
|
||||
with challenge_task_readonly.get_rw_copy(work_dir=tmp_path) as local_task:
|
||||
with pytest.raises(ChallengeTaskError, match="Failed to commit task"):
|
||||
local_task.commit(suffix="test")
|
||||
|
||||
|
||||
def test_no_commit_task(challenge_task_readonly: ChallengeTask, mock_subprocess):
|
||||
"""Test that a not-commited task cannot be accessed after the context manager is closed."""
|
||||
with patch.object(ChallengeTask, "_check_python_path"):
|
||||
with challenge_task_readonly.get_rw_copy() as local_task:
|
||||
local_task.build_image()
|
||||
local_task.build_fuzzers(engine="libfuzzer", sanitizer="address")
|
||||
old_local_dir = local_task.task_dir
|
||||
|
||||
with pytest.raises(ChallengeTaskError, match="Missing required directory"):
|
||||
ChallengeTask(
|
||||
read_only_task_dir=old_local_dir,
|
||||
local_task_dir=old_local_dir,
|
||||
project_name="example_project",
|
||||
)
|
||||
@@ -1,22 +1,19 @@
|
||||
from buttercup.common.queues import BuildConfiguration, QueueNames, GroupNames
|
||||
from buttercup.common.oss_fuzz_tool import OSSFuzzTool, Conf
|
||||
from buttercup.common.queues import QueueNames, GroupNames
|
||||
from redis import Redis
|
||||
import argparse
|
||||
import tempfile
|
||||
from buttercup.common.queues import RQItem, QueueFactory
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildRequest, BuildOutput
|
||||
from buttercup.common.logger import setup_logging
|
||||
import shutil
|
||||
from buttercup.common.challenge_task import ChallengeTask
|
||||
from pathlib import Path
|
||||
import time
|
||||
import uuid
|
||||
import os
|
||||
|
||||
logger = setup_logging(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
prsr = argparse.ArgumentParser("Builder bot")
|
||||
prsr.add_argument("--wdir", default=tempfile.TemporaryDirectory())
|
||||
prsr.add_argument("--wdir", default="/tmp/builder-bot")
|
||||
prsr.add_argument("--python", default="python")
|
||||
prsr.add_argument("--redis_url", default="redis://127.0.0.1:6379")
|
||||
prsr.add_argument("--timer", default=1000, type=int)
|
||||
@@ -34,47 +31,46 @@ def main():
|
||||
|
||||
seconds = float(args.timer) // 1000.0
|
||||
while True:
|
||||
rqit: RQItem = queue.pop()
|
||||
rqit: RQItem[BuildRequest] = queue.pop()
|
||||
if rqit is not None:
|
||||
msg: BuildRequest = rqit.deserialized
|
||||
msg = rqit.deserialized
|
||||
logger.info(f"Received build request for {msg.package_name}")
|
||||
ossfuzz_dir = msg.ossfuzz
|
||||
dirid = str(uuid.uuid4())
|
||||
|
||||
target = ossfuzz_dir
|
||||
source_path_output = msg.source_path
|
||||
if not args.allow_caching:
|
||||
wdirstr = args.wdir
|
||||
if not isinstance(wdirstr, str):
|
||||
wdirstr = args.wdir.name
|
||||
|
||||
target = os.path.join(wdirstr, f"ossfuzz-snapshot-{dirid}")
|
||||
logger.info(f"Copying {ossfuzz_dir} to {target}")
|
||||
shutil.copytree(ossfuzz_dir, target)
|
||||
source_snapshot = os.path.join(wdirstr, f"source-snapshot-{dirid}-{msg.package_name}")
|
||||
shutil.copytree(msg.source_path, source_snapshot)
|
||||
source_path_output = source_snapshot
|
||||
|
||||
conf = BuildConfiguration(msg.package_name, msg.engine, msg.sanitizer, source_path_output)
|
||||
logger.info(f"Building oss-fuzz project {msg.package_name}")
|
||||
build_tool = OSSFuzzTool(Conf(target, args.python, args.allow_pull, args.base_image_url))
|
||||
if not build_tool.build_fuzzer_with_cache(conf):
|
||||
logger.error(f"Could not build fuzzer {msg.package_name}")
|
||||
|
||||
logger.info(f"Pushing build output for {msg.package_name}")
|
||||
output_q.push(
|
||||
BuildOutput(
|
||||
package_name=msg.package_name,
|
||||
engine=msg.engine,
|
||||
sanitizer=msg.sanitizer,
|
||||
output_ossfuzz_path=target,
|
||||
source_path=source_path_output,
|
||||
task_id=msg.task_id,
|
||||
build_type=msg.build_type,
|
||||
task_dir = Path(msg.source_path).parent.parent
|
||||
if args.allow_caching:
|
||||
origin_task = ChallengeTask(
|
||||
task_dir,
|
||||
msg.package_name,
|
||||
python_path=args.python,
|
||||
local_task_dir=task_dir,
|
||||
)
|
||||
)
|
||||
logger.info(f"Acked build request for {msg.package_name}")
|
||||
queue.ack_item(rqit.item_id)
|
||||
else:
|
||||
origin_task = ChallengeTask(
|
||||
task_dir,
|
||||
msg.package_name,
|
||||
python_path=args.python,
|
||||
)
|
||||
|
||||
with origin_task.get_rw_copy(work_dir=args.wdir, delete=False) as task:
|
||||
res = task.build_fuzzers_with_cache(engine=msg.engine, sanitizer=msg.sanitizer)
|
||||
if not res.success:
|
||||
logger.error(f"Could not build fuzzer {msg.package_name}")
|
||||
continue
|
||||
|
||||
task.commit()
|
||||
logger.info(f"Pushing build output for {msg.package_name}")
|
||||
output_q.push(
|
||||
BuildOutput(
|
||||
package_name=msg.package_name,
|
||||
engine=msg.engine,
|
||||
sanitizer=msg.sanitizer,
|
||||
output_ossfuzz_path=str(task.get_oss_fuzz_path()),
|
||||
source_path=str(task.get_source_path()),
|
||||
task_id=msg.task_id,
|
||||
build_type=msg.build_type,
|
||||
)
|
||||
)
|
||||
logger.info(f"Acked build request for {msg.package_name}")
|
||||
queue.ack_item(rqit.item_id)
|
||||
|
||||
logger.info(f"Sleeping {seconds} seconds")
|
||||
time.sleep(seconds)
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
from redis import Redis
|
||||
from buttercup.common.queues import ReliableQueue, QueueFactory, RQItem, QueueNames, GroupNames
|
||||
from buttercup.common.maps import HarnessWeights, BuildMap, BUILD_TYPES
|
||||
from buttercup.common.challenge_task import ChallengeTask
|
||||
from buttercup.common.datastructures.msg_pb2 import (
|
||||
TaskReady,
|
||||
Task,
|
||||
@@ -58,9 +59,8 @@ class Scheduler:
|
||||
(source for source in task.sources if source.source_type == SourceDetail.SourceType.SOURCE_TYPE_REPO), None
|
||||
)
|
||||
if repo_source is not None:
|
||||
example_libpng_path = Path(f"/tasks_storage/{task.task_id}/src/example-libpng")
|
||||
logger.info(f"Checking if {example_libpng_path} exists")
|
||||
if example_libpng_path.is_dir():
|
||||
challenge_task = ChallengeTask(self.tasks_storage_dir / task.task_id, "example-libpng")
|
||||
if challenge_task.get_source_path().is_dir():
|
||||
logger.info(f"Mocking task {task.task_id} / example-libpng")
|
||||
return BuildRequest(
|
||||
package_name="libpng",
|
||||
@@ -71,7 +71,6 @@ class Scheduler:
|
||||
task_id=task.task_id,
|
||||
build_type=BUILD_TYPES.FUZZER,
|
||||
)
|
||||
logger.info(f"{example_libpng_path} does not exist")
|
||||
|
||||
raise RuntimeError(f"Couldn't handle task {task.task_id}")
|
||||
|
||||
@@ -89,6 +88,7 @@ class Scheduler:
|
||||
logger.info(
|
||||
f"Processing build output for {build_output.package_name}|{build_output.engine}|{build_output.sanitizer}|{build_output.output_ossfuzz_path}"
|
||||
)
|
||||
|
||||
build_dir = Path(build_output.output_ossfuzz_path) / "build" / "out" / build_output.package_name
|
||||
targets = get_fuzz_targets(build_dir)
|
||||
logger.debug(f"Found {len(targets)} targets: {targets}")
|
||||
|
||||
Reference in New Issue
Block a user