mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
47f38b8863
Run `ruff check --fix` across all components to fix I001 import sorting violations in src/ and test/ directories. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
271 lines
7.8 KiB
Python
271 lines
7.8 KiB
Python
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 (exit code 1): 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")
|