Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 99 additions & 3 deletions infra/experimental/agent-skills/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@

import argparse
import concurrent.futures
import glob
import os
import shutil
import subprocess
import sys
import textwrap
Expand Down Expand Up @@ -99,11 +101,20 @@
per-file hit/total line counts — use this to find files that are still
poorly covered.

**Also re-fetch the latest public OSS-Fuzz coverage report** (same URL
pattern as step 1 of the main workflow) before selecting any new target.
Files and functions already well-covered in the public report (≥ 50%
lines) must NOT be targeted — they are already exercised by the
production fuzzer corpus. Build an explicit blocklist of well-covered
files from the public `summary.json` and cross-check every candidate
against it before writing any harness.

3. **Candidate functions not yet targeted** — read the "Remaining coverage
gaps" section. The previous round identified under-covered functions
but may not have targeted all of them. Pick from that list first before
doing fresh analysis, prioritising functions with the lowest hit ratio
that sit on a security-relevant code path.
that sit on a security-relevant code path. Cross-check each candidate
against the public coverage report before committing to it.

4. **Threat model notes** — read the "Threat model" section. The previous
round reasoned about which input paths reach security-sensitive code.
Expand All @@ -124,6 +135,11 @@
has the harnesses, Dockerfile changes, and build.sh edits from that
round. Do not undo or re-apply them.
- Target the coverage gaps and candidate functions surfaced above.
- **Do not target functions in well-covered files.** Read the
"Already-covered files (blocklist)" section from the previous report
and cross-check every candidate against the public `summary.json`
before writing a harness. Targeting already-covered code is the most
common way expansion rounds fail to produce real gains.
- If coverage stalled completely in the previous round despite multiple
attempts, change subsystem — pick the next-highest-risk area from the
threat model notes rather than hammering the same code paths.
Expand Down Expand Up @@ -159,9 +175,16 @@
reports are at:
`https://storage.googleapis.com/oss-fuzz-coverage/{project}/reports/YYYYMMDD/linux/summary.json`
Try recent dates (last few days) until you find one.
- **Parse the per-file coverage data** from `summary.json`. Build an
explicit list of source files with low line coverage (< 30% lines hit)
— these are your candidate targets. Also note which files already have
high coverage (≥ 50%) so you can avoid wasting effort on them.
- Identify under-covered source files and functions that sit on the
attack surface (parsers, network handlers, file I/O, APIs exposed to
untrusted input).
untrusted input). **Only consider files/functions that appear in the
low-coverage list derived from `summary.json`** — do not rely solely
on reading the source structure to judge what is "important", because
important functions are often already covered by existing harnesses.

2. **Clone the target source locally**
- Study the Dockerfile to find the upstream repository URL.
Expand All @@ -170,7 +193,13 @@
of `git clone` so you can iterate quickly.

3. **Design and write new harness(es)**
- Pick the most impactful under-covered area and write a new
- **Before selecting any target function**, verify it has genuinely low
coverage in the public `summary.json` (< 30% lines hit for its source
file). Do NOT write a harness targeting a function whose source file is
already well-covered — this is the most common cause of wasted effort.
If you cannot confirm a function is poorly covered in the public report,
skip it and pick another candidate from the low-coverage file list.
- Pick the most impactful genuinely-uncovered area and write a new
libFuzzer-style harness targeting it.
- Follow the best practices from the *fuzzing-memory-unsafe-expert*
skill: simplicity, determinism, enough entropy, matching the threat
Expand Down Expand Up @@ -241,6 +270,12 @@
identified but NOT targeted this round, ordered by security
relevance. The next round picks from this list first.

- **Already-covered files (blocklist)** *(hand-off)*: the list of
source files that had ≥ 50% line coverage in the public OSS-Fuzz
report at the time of this round (file path and coverage %). Future
rounds must not write harnesses whose primary target belongs to one
of these files.

- **Approaches tried and abandoned** *(hand-off)*: any harness designs
that were attempted but discarded (e.g. zero coverage gain, false
positives, excessive slowness), with a one-line reason for each.
Expand Down Expand Up @@ -773,6 +808,26 @@
""")


def cleanup_project_artifacts(project):
"""Remove Docker image and local build/coverage artifacts for a project."""
image = f'gcr.io/oss-fuzz/{project}'
print(f' [cleanup] Removing Docker image {image}')
subprocess.run(['docker', 'rmi', '--force', image],
capture_output=True,
check=False)

for subdir in ('out', 'work'):
path = os.path.join(OSS_FUZZ_ROOT, 'build', subdir, project)
if os.path.isdir(path):
print(f' [cleanup] Removing {path}')
shutil.rmtree(path, ignore_errors=True)

for path in glob.glob(os.path.join(OSS_FUZZ_ROOT, f'{project}-cov-*')):
if os.path.isdir(path):
print(f' [cleanup] Removing {path}')
shutil.rmtree(path, ignore_errors=True)


def get_recent_date_str(days_ago=1):
"""Return a YYYYMMDD string for a recent date."""
dt = datetime.now() - timedelta(days=days_ago)
Expand Down Expand Up @@ -1355,6 +1410,7 @@ def _run_expand_sessions(args):
sys.exit(1)

max_parallel = args.max_parallel
do_cleanup = not args.no_cleanup

print(f'[*] Using agent CLI: {agent_cli}')
print(f'[*] Task: expand')
Expand All @@ -1363,6 +1419,7 @@ def _run_expand_sessions(args):
print(f'[*] Expansion size: {expansion_size}')
print(f'[*] Consolidation agent: {"yes" if run_consolidate else "no"}')
print(f'[*] Summary agent: {"yes" if run_summary else "no"}')
print(f'[*] Cleanup after completion: {"yes" if do_cleanup else "no"}')
print(f'[*] Max parallel sessions: {max_parallel}')
print()

Expand Down Expand Up @@ -1399,6 +1456,10 @@ def run_project_rounds(project):
completed_rounds)
results.append(summary_result)

if do_cleanup:
print(f'[*] {project}: cleaning up build artifacts ...')
cleanup_project_artifacts(project)

return results

print(f'[*] Launching sessions for {len(projects)} project(s) '
Expand Down Expand Up @@ -1470,6 +1531,15 @@ def cmd_integrate_project(args):
_run_integrate_sessions(args)


def cmd_clean(args):
"""Handle the clean subcommand."""
_validate_projects(args.projects)
for project in args.projects:
print(f'[*] Cleaning artifacts for: {project}')
cleanup_project_artifacts(project)
print('\n[*] Done.')


def cmd_show_prompt(args):
"""Handle the show-prompt subcommand."""
task = args.task
Expand Down Expand Up @@ -1547,6 +1617,12 @@ def main():
python %(prog)s integrate-project --print-only \\
https://github.com/owner/repo

# Skip post-run cleanup (Docker image + build/coverage dirs):
python %(prog)s expand-oss-fuzz-projects --no-cleanup open62541

# Manually clean up artifacts from a previous run:
python %(prog)s clean open62541 json-c

# Use a specific agent CLI:
python %(prog)s expand-oss-fuzz-projects --agent gemini htslib

Expand Down Expand Up @@ -1642,6 +1718,13 @@ def main():
action='store_false',
help='Skip the summary agent even when running multiple rounds.',
)
expand_parser.add_argument(
'--no-cleanup',
action='store_true',
default=False,
help='Skip cleanup of Docker images and build/coverage artifacts after '
'each project completes (default: cleanup is on).',
)
expand_parser.set_defaults(func=cmd_expand)

# fix-builds
Expand Down Expand Up @@ -1710,6 +1793,19 @@ def main():
)
integrate_parser.set_defaults(func=cmd_integrate_project)

# clean
clean_parser = subparsers.add_parser(
'clean',
help='Remove Docker images and build/coverage artifacts for one or more '
'projects.',
)
clean_parser.add_argument(
'projects',
nargs='+',
help='One or more OSS-Fuzz project names to clean up.',
)
clean_parser.set_defaults(func=cmd_clean)

# show-prompt
show_parser = subparsers.add_parser(
'show-prompt',
Expand Down
9 changes: 9 additions & 0 deletions infra/experimental/agent-skills/oss-fuzz-engineer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ The agent can also be used to extend and improve the fuzzing posture of existing

A useful approach for extending a project is to study the latest code coverage report for the project, which is publicly available, to identify areas of the code that are not well covered by existing fuzz targets. The agent can then write new fuzz targets to cover those areas, and test them locally before concluding on the work for the security engineer to review.

**Critical: avoid targeting already-covered code.** The most common mistake when extending an OSS-Fuzz project is writing harnesses that target functions already well-exercised by the existing production fuzzer corpus. Before selecting any target function:

1. Fetch the public `summary.json` (see the code_coverage reference).
2. Parse the per-file line coverage percentages.
3. Build an explicit blocklist of files with ≥ 50% line coverage — do NOT write harnesses whose primary target lives in one of these files.
4. Only select targets from files with genuinely low coverage (< 30% lines hit).

Reading the source code and identifying "important-looking" functions is not sufficient — important functions are frequently already covered. Coverage data from `summary.json` is the authoritative source of truth for what needs work.

Use the local code coverage feature of the `python3 infra/helper.py` tool to generate code coverage reports for fuzz targets locally, for example to validate the code coverage achieved by a new fuzz target. This can be done by running `python3 infra/helper.py introspector --coverage-only PROJECT_NAME` and then studying the generated report in e.g. build/out/PROJECT_NAME/report. Some examples of this include:

```
Expand Down
14 changes: 11 additions & 3 deletions projects/graphicsmagick/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ RUN apt-get update && \
nasm \
pkg-config \
po4a \
python3-pip \
python3-setuptools \
python3-wheel \
ninja-build \
libgflags-dev \
yasm

# Get latest meson
RUN apt purge meson
RUN pip3 install --upgrade pip meson --root-user-action ignore

# Due to libtiff requirements, build requires autoconf 2.71 (or later)
RUN curl -LO https://mirrors.edge.kernel.org/ubuntu/pool/main/a/autoconf/autoconf_2.72-3.1ubuntu1_all.deb && \
apt install ./autoconf_2.72-3.1ubuntu1_all.deb
Expand All @@ -53,9 +60,6 @@ RUN git clone --depth 1 https://github.com/Esri/lerc
# Libdeflate
RUN git clone --depth 1 https://github.com/ebiggers/libdeflate

# libgeotiff
RUN git clone --depth 1 https://github.com/OSGeo/libgeotiff

# h.265 codec implementation needed by libheif
RUN git clone --depth 1 https://github.com/strukturag/libde265 || \
printf "https://github.com/strukturag/libde265 is not available!\n"
Expand All @@ -70,6 +74,10 @@ RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git/src/stable/
RUN git clone --depth 1 https://aomedia.googlesource.com/aom aom || \
printf "https://aomedia.googlesource.com/aom is not available!\n"

# Dav1d Codec Library needed by libheif
RUN git clone --depth 1 https://code.videolan.org/videolan/dav1d || \
printf "https://code.videolan.org/videolan/dav1d is not available!\n"

# AVC (OpenH264) Codec Library needed by libheif
RUN git clone --depth 1 https://github.com/cisco/openh264 openh264

Expand Down
2 changes: 1 addition & 1 deletion projects/swift-protobuf/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@

FROM gcr.io/oss-fuzz-base/base-builder-swift:ubuntu-24-04

RUN git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/apple/swift-protobuf.git
RUN git clone --depth 1 https://github.com/apple/swift-protobuf.git
COPY build.sh $SRC
WORKDIR $SRC/swift-protobuf
23 changes: 23 additions & 0 deletions projects/swift-protobuf/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@
. precompile_swift
cd FuzzTesting


# Normally one would use `$SWIFTFLAGS` (from `precompile_swift``) on this
# invocations, but as we found in
# https://github.com/apple/swift-protobuf/pull/2037, the flags needs depend on
# *both* the Swift Toolchain version *and& the `swift-tools-version` in the
# `Package.swift`.
#
# So, for now, manually recode `precompile_swift` with the flags needed since
# swift-protobuf uses a 6.2+ `swift-tools-version`.
export SWIFTFLAGS="-Xswiftc -static-stdlib --static-swift-stdlib"
if [ "$SANITIZER" = "coverage" ] ; then
export SWIFTFLAGS="$SWIFTFLAGS -Xswiftc -profile-generate -Xswiftc -profile-coverage-mapping --sanitize=fuzzer"
else
export SWIFTFLAGS="$SWIFTFLAGS --sanitize=fuzzer --sanitize=$SANITIZER"
for f in $CFLAGS; do
export SWIFTFLAGS="$SWIFTFLAGS -Xcc=$f"
done

for f in $CXXFLAGS; do
export SWIFTFLAGS="$SWIFTFLAGS -Xcxx=$f"
done
fi

# debug build
swift build -c debug $SWIFTFLAGS
(
Expand Down
Loading