Skip to content

Commit 06f3ff3

Browse files
tschmclaude
andauthored
feat: add ClusterFuzzLite fuzzing scaffold for cvxcla (#782)
Atheris harness fuzzing the DenseCovariance quadratic-form operator (matvec + solve_free) with arbitrary symmetric matrices. build.sh passes --collect-all numpy --collect-all scipy; native deps pre-imported outside instrument_imports(). Verified locally against the OSS-Fuzz base-builder-python image (2500 runs, no crashes). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cebe6ff commit 06f3ff3

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

.clusterfuzzlite/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# ClusterFuzzLite build image for cvxcla.
2+
# Uses the OSS-Fuzz Python base image, which provides Atheris and the
3+
# compile_python_fuzzer helper.
4+
# Pinned by SHA256 so the build image only changes through reviewed updates.
5+
FROM gcr.io/oss-fuzz-base/base-builder-python@sha256:99c714c91ec30778a3ce3ae5b37491a81fc043808bc8f2955159c2f608f80116
6+
7+
COPY . $SRC
8+
WORKDIR $SRC
9+
COPY .clusterfuzzlite/build.sh $SRC/build.sh

.clusterfuzzlite/build.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash -eu
2+
# ClusterFuzzLite build script — installs cvxcla and compiles each Python
3+
# harness in tests/fuzz/ via OSS-Fuzz's compile_python_fuzzer helper.
4+
5+
cd "$SRC"
6+
7+
# Pin pip so the build environment is reproducible and only changes through a
8+
# reviewed bump (the same rationale as the SHA-pinned base image).
9+
pip3 install --upgrade "pip==24.3.1"
10+
11+
# Install the package and its runtime dependencies so PyInstaller can discover
12+
# and bundle cvxcla into each frozen fuzzer binary.
13+
pip3 install .
14+
15+
# PyInstaller does not discover numpy's C-extension submodules on its own, so
16+
# the frozen fuzzer crashes at runtime with
17+
# "No module named 'numpy._core._exceptions'". --collect-all pulls in every
18+
# numpy submodule, data file and shared library.
19+
for fuzzer in tests/fuzz/fuzz_*.py; do
20+
compile_python_fuzzer "$fuzzer" --collect-all numpy --collect-all scipy
21+
done

tests/fuzz/fuzz_operators.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Fuzz the cvxcla DenseCovariance quadratic-form operator.
2+
3+
``DenseCovariance`` wraps a symmetric covariance matrix and exposes ``matvec``
4+
and ``solve_free`` (the free-block linear solve at the heart of the critical-line
5+
algorithm). Neither should crash with an unexpected exception on adversarial
6+
input — non-square/asymmetric matrices are rejected in ``__post_init__``, and
7+
singular free blocks should raise a documented error (or numpy's
8+
``LinAlgError``). This harness exercises that contract with coverage-guided
9+
input.
10+
11+
Run locally:
12+
pip install atheris numpy scipy
13+
python tests/fuzz/fuzz_operators.py -atheris_runs=20000
14+
15+
Run in ClusterFuzzLite: this file is built by .clusterfuzzlite/build.sh.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import contextlib
21+
import sys
22+
23+
import atheris
24+
25+
# Pre-import the native dependencies OUTSIDE the instrumentation block so they
26+
# load uninstrumented; only the first-party package under test is instrumented.
27+
import numpy as np
28+
import scipy.linalg # noqa: F401 # pre-imported uninstrumented
29+
30+
with atheris.instrument_imports():
31+
from cvxcla.operators import DenseCovariance
32+
33+
_ALLOWED = (ValueError, TypeError, np.linalg.LinAlgError)
34+
35+
36+
def test_one_input(data: bytes) -> None:
37+
"""Build a DenseCovariance and exercise matvec/solve_free with fuzzed data."""
38+
fdp = atheris.FuzzedDataProvider(data)
39+
n = fdp.ConsumeIntInRange(1, 6)
40+
raw = np.array([fdp.ConsumeFloat() for _ in range(n * n)], dtype=np.float64).reshape(n, n)
41+
# Symmetrise so construction passes its symmetry check and the numeric paths
42+
# (matvec/solve_free) are actually exercised rather than only the guard.
43+
matrix = (raw + raw.T) / 2.0
44+
45+
with contextlib.suppress(_ALLOWED):
46+
cov = DenseCovariance(matrix=matrix)
47+
cov.matvec(np.array([fdp.ConsumeFloat() for _ in range(n)], dtype=np.float64))
48+
free = np.array([fdp.ConsumeBool() for _ in range(n)], dtype=np.bool_)
49+
rhs = np.array([fdp.ConsumeFloat() for _ in range(int(free.sum()))], dtype=np.float64)
50+
cov.solve_free(free, rhs)
51+
52+
53+
def main() -> None:
54+
"""Run the Atheris fuzz loop."""
55+
atheris.Setup(sys.argv, test_one_input)
56+
atheris.Fuzz()
57+
58+
59+
if __name__ == "__main__":
60+
main()

0 commit comments

Comments
 (0)