Skip to content

fix: satisfy shellcheck on network cleanup #202

fix: satisfy shellcheck on network cleanup

fix: satisfy shellcheck on network cleanup #202

Workflow file for this run

name: libsixel Fuzzing (push)
on:
push:
branches-ignore:
- 'ci/triage/**'
workflow_dispatch:
inputs:
triage_job_labels:
description: Pipe-delimited fuzz job labels selected by triage.yml
required: false
default: '||'
type: string
triage_source_run_id:
description: Source workflow run id collected by triage.yml
required: false
default: ''
type: string
permissions:
contents: read
concurrency:
group: fuzzing-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
# Pin vcpkg to a stable release to avoid fetching future tool builds that
# depend on unavailable CMake versions during bootstrap.
VCPKG_REF: 2024.12.16
# Enforce Node.js 24 for JavaScript actions before the hosted-runner switch.
# Node.js 24 becomes default on 2026-06-02 and Node.js 20 is removed on
# 2026-09-16.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
fuzz_push_full:
if: >-
${{
github.event_name == 'push'
|| inputs.triage_job_labels != '||'
}}
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
buildtool: [autotools, meson]
target: [png-chunk, png-apng, png-icc-gamma, png-plte-trns, jpeg-icc, pnm-ascii, psd-resource, gif-block, gif-lzw-codes, sixel-stream, hdr-header, pic-structure, bmp-dib, bmp-rle-bitfields, mapfile-spec, mapfile-palette, option-arglist, exif-orientation, loader-order]
env:
FUZZ_STRUCT_CORPUS: ${{ github.workspace }}/.tmp/fuzz-corpus-struct
FUZZ_STRUCT_ARTIFACT_DIR: ${{ github.workspace }}/.tmp/fuzz-artifacts-struct/${{ matrix.target }}
FUZZ_STRUCT_ARGS_BASE: ${{ format('-fork={0} -print_final_stats=1 -timeout=5 -use_value_profile=1 -reduce_inputs=1 -rss_limit_mb=4096 -artifact_prefix={1}/.tmp/fuzz-artifacts-struct/{2}/', github.event_name == 'push' && '10' || '2', github.workspace, matrix.target) }}
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:report_error_type=1
steps:
- name: Select struct fuzz scope
id: struct_scope
shell: bash
run: |
set -euo pipefail
# Run every matrix job on push to avoid effectively skipped fuzz
# configurations. Duration is controlled by per-target fuzz limits.
run_target=1
if test "${{ github.event_name }}" = "workflow_dispatch"; then
label="${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-${{ matrix.target }}"
run_target=0
case "${{ inputs.triage_job_labels }}" in
*"|$label|"*)
run_target=1
;;
esac
fi
echo "run_target=$run_target" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
if: ${{ steps.struct_scope.outputs.run_target == '1' }}
with:
fetch-depth: 1
- name: Install build dependencies
if: ${{ steps.struct_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
bash .github/scripts/disable-hosted-runner-microsoft-apt-sources.sh
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang meson ninja-build autoconf automake libtool pkg-config make
- name: Restore struct fuzz corpus cache
if: ${{ steps.struct_scope.outputs.run_target == '1' }}
uses: actions/cache@v5
with:
path: ${{ env.FUZZ_STRUCT_CORPUS }}/${{ matrix.target }}
key: fuzz-corpus-struct-push-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
fuzz-corpus-struct-push-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-
fuzz-corpus-struct-push-${{ matrix.buildtool }}-${{ matrix.target }}-
fuzz-corpus-struct-${{ matrix.buildtool }}-${{ matrix.target }}-
- name: Seed struct fuzz corpus
if: ${{ steps.struct_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
target_corpus="$FUZZ_STRUCT_CORPUS/${{ matrix.target }}"
mkdir -p "$target_corpus" "$FUZZ_STRUCT_ARTIFACT_DIR"
if test -d tests/data/security/fuzzing/data; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$target_corpus/" || true
done < <(find tests/data/security/fuzzing/data -type f -print0)
fi
while IFS= read -r -d '' seed; do
cp -n "$seed" "$target_corpus/" || true
done < <(find tests/data -type f \
\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \
-o -name '*.gif' -o -name '*.webp' -o -name '*.tiff' \
-o -name '*.pnm' -o -name '*.ppm' -o -name '*.pgm' \
-o -name '*.pbm' -o -name '*.psd' -o -name '*.hdr' \
-o -name '*.six' -o -name '*.sixel' -o -name '*.pic' \
-o -name '*.tga' -o -name '*.bmp' -o -name '*.bin' \) \
-print0)
echo "struct-corpus seed files (${{ matrix.target }}): $(find "$target_corpus" -type f | wc -l)"
- name: Build and run struct fuzz target
if: ${{ steps.struct_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
export PATH="$PWD/build-aux:$PWD/bin:$PATH"
run_log="$FUZZ_STRUCT_ARTIFACT_DIR/run.log"
min_cov=60
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
min_cov=55
fi
dict_common="$PWD/fuzz/loader-builtin.dict"
dict_target="$PWD/fuzz/dicts/struct/${{ matrix.target }}.dict"
dict_merged="$FUZZ_STRUCT_ARTIFACT_DIR/dict.merged"
if [ -f "$dict_target" ]; then
awk '!seen[$0]++' "$dict_common" "$dict_target" > "$dict_merged"
else
cp "$dict_common" "$dict_merged"
fi
struct_max_len=4096
case "${{ matrix.target }}" in
hdr-header|mapfile-spec|mapfile-palette|pnm-ascii|option-arglist|exif-orientation|loader-order)
struct_max_len=2048
;;
gif-lzw-codes)
struct_max_len=3072
;;
png-icc-gamma|png-plte-trns|psd-resource)
struct_max_len=8192
;;
bmp-rle-bitfields)
struct_max_len=16384
;;
esac
struct_max_total_time=600
# Keep expensive structure checks in extended mode.
case "${{ matrix.target }}" in
psd-resource|loader-order)
struct_max_total_time=1800
;;
esac
fuzz_args="$FUZZ_STRUCT_ARGS_BASE -max_total_time=$struct_max_total_time -max_len=$struct_max_len -dict=$dict_merged"
semantic_feature_pattern=''
semantic_min_features=0
case "${{ matrix.target }}" in
gif-block)
semantic_feature_pattern='gif-block-semantic-features:'
semantic_min_features=12
;;
bmp-rle-bitfields)
semantic_feature_pattern='bmp-rle-semantic-features:'
semantic_min_features=12
;;
gif-lzw-codes)
semantic_feature_pattern='gif-lzw-semantic-features:'
semantic_min_features=12
;;
png-apng)
semantic_feature_pattern='png-apng-semantic-features:'
semantic_min_features=12
;;
png-chunk)
semantic_feature_pattern='png-chunk-semantic-features:'
semantic_min_features=12
;;
sixel-stream)
semantic_feature_pattern='sixel-stream-semantic-features:'
semantic_min_features=12
;;
esac
run_semantic_smoke() {
if [ "$semantic_min_features" -eq 0 ]; then
return 0
fi
semantic_log="$FUZZ_STRUCT_ARTIFACT_DIR/semantic-smoke.log"
semantic_rc=0
set +e
"$harness" -runs=64 -max_len="$struct_max_len" \
-seed=1 \
-timeout=5 -rss_limit_mb=4096 -use_value_profile=1 \
-artifact_prefix="$FUZZ_STRUCT_ARTIFACT_DIR/" \
-dict="$dict_merged" >"$semantic_log" 2>&1
semantic_rc=$?
set -e
cat "$semantic_log"
if [ "$semantic_rc" -ne 0 ]; then
echo "::error::struct target '${{ matrix.target }}' semantic smoke failed: exit=$semantic_rc."
exit "$semantic_rc"
fi
semantic_count="$(awk -v pattern="$semantic_feature_pattern" '
index($0, pattern) != 0 {
for (i = 1; i <= NF; ++i) {
if ($i ~ /^count=/) {
sub(/^count=/, "", $i)
count = $i
}
}
}
END { print count }
' "$semantic_log")"
if [ -z "$semantic_count" ] || \
[ "$semantic_count" -lt "$semantic_min_features" ]; then
echo "::error::struct target '${{ matrix.target }}' semantic feature coverage too low: count=${semantic_count:-0} (expected >= $semantic_min_features)."
exit 1
fi
}
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
CC=clang CXX=clang++ ./configure --enable-fuzz
make -j"$(nproc)"
harness="$PWD/fuzz/fuzz-loader-builtin-struct-${{ matrix.target }}-libfuzzer"
target_corpus="$FUZZ_STRUCT_CORPUS/${{ matrix.target }}"
if [ ! -x "$harness" ]; then
echo "::error::missing struct harness: $harness"
exit 1
fi
run_semantic_smoke
merge_seed="$FUZZ_STRUCT_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$target_corpus" -type f -print -quit | grep -q .; then
cp -a "$target_corpus"/. "$merge_seed"/
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_merged" "$target_corpus" "$merge_seed"
else
echo "struct-corpus: no files to merge for ${{ matrix.target }}"
fi
rm -rf "$merge_seed"
make "fuzz-run-struct-${{ matrix.target }}" \
FUZZ_STRUCT_CORPUS="$FUZZ_STRUCT_CORPUS" \
FUZZ_STRUCT_ARGS="$fuzz_args" 2>&1 | tee "$run_log"
else
CC=clang CXX=clang++ meson setup builddir -Dfuzz=true
meson compile -C builddir -v
harness="$PWD/builddir/fuzz/fuzz-loader-builtin-struct-${{ matrix.target }}-libfuzzer"
target_corpus="$FUZZ_STRUCT_CORPUS/${{ matrix.target }}"
if [ ! -x "$harness" ]; then
echo "::error::missing struct harness: $harness"
exit 1
fi
run_semantic_smoke
merge_seed="$FUZZ_STRUCT_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$target_corpus" -type f -print -quit | grep -q .; then
cp -a "$target_corpus"/. "$merge_seed"/
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_merged" "$target_corpus" "$merge_seed"
else
echo "struct-corpus: no files to merge for ${{ matrix.target }}"
fi
rm -rf "$merge_seed"
export FUZZ_STRUCT_ARGS="$fuzz_args"
meson compile -C builddir "fuzz-run-struct-${{ matrix.target }}" 2>&1 | tee "$run_log"
fi
if ! grep -Eq 'DONE cov:|INFO: exiting: 0' "$run_log"; then
echo "::error::libFuzzer did not report completion for struct target '${{ matrix.target }}'."
exit 1
fi
cov="$(grep -Eo 'cov: [0-9]+' "$run_log" | tail -n1 | awk '{print $2}')"
if [ -z "$cov" ] || [ "$cov" -lt "$min_cov" ]; then
echo "::error::struct target '${{ matrix.target }}' coverage too low: cov=$cov (expected >= $min_cov)."
exit 1
fi
- name: Upload struct fuzz artifacts
if: ${{ always() && steps.struct_scope.outputs.run_target == '1' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-${{ matrix.target }}
if-no-files-found: ignore
retention-days: 7
path: |
${{ env.FUZZ_STRUCT_ARTIFACT_DIR }}
${{ env.FUZZ_STRUCT_CORPUS }}
fuzz_push_loader_formats:
if: >-
${{
github.event_name == 'push'
|| inputs.triage_job_labels != '||'
}}
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-loader-${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
buildtool: [autotools, meson]
target: [png, gif, jpeg, pnm, sixel, hdr, psd, pic, bmp, webp]
env:
FUZZ_BUILTIN_CORPUS: ${{ github.workspace }}/.tmp/fuzz-corpus-loader
FUZZ_BUILTIN_ARTIFACT_DIR: ${{ github.workspace }}/.tmp/fuzz-artifacts-loader/${{ matrix.target }}
FUZZ_BUILTIN_ARGS_BASE: ${{ format('-fork={0} -print_final_stats=1 -timeout=5 -use_value_profile=1 -reduce_inputs=1 -rss_limit_mb=4096 -artifact_prefix={1}/.tmp/fuzz-artifacts-loader/{2}/', github.event_name == 'push' && '10' || '2', github.workspace, matrix.target) }}
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:report_error_type=1
steps:
- name: Select loader-format fuzz scope
id: loader_scope
shell: bash
run: |
set -euo pipefail
# Keep all loader-format matrix jobs active on push. Individual fuzz
# runtimes are governed by max_total_time settings in each job.
run_target=1
if test "${{ github.event_name }}" = "workflow_dispatch"; then
label="${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-loader-${{ matrix.target }}"
run_target=0
case "${{ inputs.triage_job_labels }}" in
*"|$label|"*)
run_target=1
;;
esac
fi
echo "run_target=$run_target" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
if: ${{ steps.loader_scope.outputs.run_target == '1' }}
with:
fetch-depth: 1
- name: Install build dependencies
if: ${{ steps.loader_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
bash .github/scripts/disable-hosted-runner-microsoft-apt-sources.sh
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang meson ninja-build autoconf automake libtool pkg-config make
- name: Restore loader-format fuzz corpus cache
if: ${{ steps.loader_scope.outputs.run_target == '1' }}
uses: actions/cache@v5
with:
path: ${{ env.FUZZ_BUILTIN_CORPUS }}/${{ matrix.target }}
key: fuzz-corpus-loader-push-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
fuzz-corpus-loader-push-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-
fuzz-corpus-loader-push-${{ matrix.buildtool }}-${{ matrix.target }}-
fuzz-corpus-loader-${{ matrix.buildtool }}-${{ matrix.target }}-
- name: Seed loader-format fuzz corpus
if: ${{ steps.loader_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
target_corpus="$FUZZ_BUILTIN_CORPUS/${{ matrix.target }}"
mkdir -p "$target_corpus" "$FUZZ_BUILTIN_ARTIFACT_DIR"
if test -d tests/data/security/fuzzing/data; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$target_corpus/" || true
done < <(find tests/data/security/fuzzing/data -type f -print0)
fi
while IFS= read -r -d '' seed; do
cp -n "$seed" "$target_corpus/" || true
done < <(find tests/data -type f \
\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \
-o -name '*.gif' -o -name '*.webp' -o -name '*.tiff' \
-o -name '*.pnm' -o -name '*.ppm' -o -name '*.pgm' \
-o -name '*.pbm' -o -name '*.psd' -o -name '*.hdr' \
-o -name '*.six' -o -name '*.sixel' -o -name '*.pic' \
-o -name '*.tga' -o -name '*.bmp' -o -name '*.bin' \) \
-print0)
echo "loader-corpus seed files (${{ matrix.target }}): $(find "$target_corpus" -type f | wc -l)"
- name: Build and run loader-format fuzz target
if: ${{ steps.loader_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
export PATH="$PWD/build-aux:$PWD/bin:$PATH"
run_log="$FUZZ_BUILTIN_ARTIFACT_DIR/run.log"
min_cov=18
case "${{ matrix.target }}" in
webp)
min_cov=15
;;
esac
dict_common="$PWD/fuzz/loader-builtin.dict"
dict_target="$PWD/fuzz/dicts/loader/${{ matrix.target }}.dict"
dict_merged="$FUZZ_BUILTIN_ARTIFACT_DIR/dict.merged"
if [ -f "$dict_target" ]; then
awk '!seen[$0]++' "$dict_common" "$dict_target" > "$dict_merged"
else
cp "$dict_common" "$dict_merged"
fi
loader_max_total_time=600
# PNG/GIF/PSD are prioritized because they account for most reports.
case "${{ matrix.target }}" in
png|gif|psd)
loader_max_total_time=1800
;;
esac
fuzz_args="$FUZZ_BUILTIN_ARGS_BASE -max_total_time=$loader_max_total_time -dict=$dict_merged"
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
CC=clang CXX=clang++ ./configure --enable-fuzz
make -j"$(nproc)"
harness="$PWD/fuzz/fuzz-loader-builtin-libfuzzer"
target_corpus="$FUZZ_BUILTIN_CORPUS/${{ matrix.target }}"
if [ ! -x "$harness" ]; then
echo "::error::missing loader harness: $harness"
exit 1
fi
merge_seed="$FUZZ_BUILTIN_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$target_corpus" -type f -print -quit | grep -q .; then
cp -a "$target_corpus"/. "$merge_seed"/
FUZZ_BUILTIN_FORCE_FORMAT="${{ matrix.target }}" \
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_merged" "$target_corpus" "$merge_seed"
else
echo "loader-corpus: no files to merge for ${{ matrix.target }}"
fi
rm -rf "$merge_seed"
make "fuzz-run-loader-builtin-${{ matrix.target }}" \
FUZZ_BUILTIN_CORPUS="$FUZZ_BUILTIN_CORPUS" \
FUZZ_BUILTIN_ARGS="$fuzz_args" 2>&1 | tee "$run_log"
else
CC=clang CXX=clang++ meson setup builddir -Dfuzz=true
meson compile -C builddir -v
harness="$PWD/builddir/fuzz/fuzz-loader-builtin-libfuzzer"
target_corpus="$FUZZ_BUILTIN_CORPUS/${{ matrix.target }}"
if [ ! -x "$harness" ]; then
echo "::error::missing loader harness: $harness"
exit 1
fi
merge_seed="$FUZZ_BUILTIN_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$target_corpus" -type f -print -quit | grep -q .; then
cp -a "$target_corpus"/. "$merge_seed"/
FUZZ_BUILTIN_FORCE_FORMAT="${{ matrix.target }}" \
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_merged" "$target_corpus" "$merge_seed"
else
echo "loader-corpus: no files to merge for ${{ matrix.target }}"
fi
rm -rf "$merge_seed"
export FUZZ_BUILTIN_ARGS="$fuzz_args"
meson compile -C builddir "fuzz-run-loader-builtin-${{ matrix.target }}" 2>&1 | tee "$run_log"
fi
if ! grep -Eq 'DONE cov:|INFO: exiting: 0' "$run_log"; then
echo "::error::libFuzzer did not report completion for loader-format target '${{ matrix.target }}'."
exit 1
fi
cov="$(grep -Eo 'cov: [0-9]+' "$run_log" | tail -n1 | awk '{print $2}')"
if [ -z "$cov" ] || [ "$cov" -lt "$min_cov" ]; then
echo "::error::loader-format target '${{ matrix.target }}' coverage too low: cov=$cov (expected >= $min_cov)."
exit 1
fi
- name: Upload loader-format fuzz artifacts
if: ${{ always() && steps.loader_scope.outputs.run_target == '1' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-loader-${{ matrix.target }}
if-no-files-found: ignore
retention-days: 7
path: |
${{ env.FUZZ_BUILTIN_ARTIFACT_DIR }}
${{ env.FUZZ_BUILTIN_CORPUS }}
fuzz_push_loader_autodetect:
if: >-
${{
github.event_name == 'push'
|| inputs.triage_job_labels != '||'
}}
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-loader-autodetect
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
buildtool: [autotools, meson]
env:
FUZZ_BUILTIN_CORPUS: ${{ github.workspace }}/.tmp/fuzz-corpus-loader-autodetect
FUZZ_BUILTIN_ARTIFACT_DIR: ${{ github.workspace }}/.tmp/fuzz-artifacts-loader-autodetect/${{ matrix.buildtool }}
FUZZ_BUILTIN_ARGS_BASE: ${{ format('-fork={0} -print_final_stats=1 -timeout=5 -use_value_profile=1 -reduce_inputs=1 -rss_limit_mb=4096 -artifact_prefix={1}/.tmp/fuzz-artifacts-loader-autodetect/{2}/', github.event_name == 'push' && '10' || '2', github.workspace, matrix.buildtool) }}
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:report_error_type=1
steps:
- name: Select loader-autodetect fuzz scope
id: autodetect_scope
shell: bash
run: |
set -euo pipefail
run_target=1
if test "${{ github.event_name }}" = "workflow_dispatch"; then
label="${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-loader-autodetect"
run_target=0
case "${{ inputs.triage_job_labels }}" in
*"|$label|"*)
run_target=1
;;
esac
fi
echo "run_target=$run_target" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
if: ${{ steps.autodetect_scope.outputs.run_target == '1' }}
with:
fetch-depth: 1
- name: Install build dependencies
if: ${{ steps.autodetect_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
bash .github/scripts/disable-hosted-runner-microsoft-apt-sources.sh
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang meson ninja-build autoconf automake libtool pkg-config make
- name: Restore loader-autodetect fuzz corpus cache
if: ${{ steps.autodetect_scope.outputs.run_target == '1' }}
uses: actions/cache@v5
with:
path: ${{ env.FUZZ_BUILTIN_CORPUS }}
key: fuzz-corpus-loader-autodetect-push-${{ matrix.buildtool }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
fuzz-corpus-loader-autodetect-push-${{ matrix.buildtool }}-${{ github.ref_name }}-
fuzz-corpus-loader-autodetect-push-${{ matrix.buildtool }}-
fuzz-corpus-loader-autodetect-${{ matrix.buildtool }}-
- name: Seed loader-autodetect fuzz corpus
if: ${{ steps.autodetect_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$FUZZ_BUILTIN_CORPUS" "$FUZZ_BUILTIN_ARTIFACT_DIR"
if test -d tests/data/security/fuzzing/data; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_BUILTIN_CORPUS/" || true
done < <(find tests/data/security/fuzzing/data -type f -print0)
fi
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_BUILTIN_CORPUS/" || true
done < <(find tests/data -type f \
\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \
-o -name '*.gif' -o -name '*.webp' -o -name '*.tiff' \
-o -name '*.pnm' -o -name '*.ppm' -o -name '*.pgm' \
-o -name '*.pbm' -o -name '*.psd' -o -name '*.hdr' \
-o -name '*.six' -o -name '*.sixel' -o -name '*.pic' \
-o -name '*.tga' -o -name '*.bmp' -o -name '*.bin' \) \
-print0)
echo "loader-autodetect seed files: $(find "$FUZZ_BUILTIN_CORPUS" -type f | wc -l)"
- name: Build and run loader-autodetect fuzz target
if: ${{ steps.autodetect_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
export PATH="$PWD/build-aux:$PWD/bin:$PATH"
run_log="$FUZZ_BUILTIN_ARTIFACT_DIR/run.log"
min_cov=15
dict_common="$PWD/fuzz/loader-builtin.dict"
fuzz_args="$FUZZ_BUILTIN_ARGS_BASE -max_total_time=600 -dict=$dict_common"
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
CC=clang CXX=clang++ ./configure --enable-fuzz
make -j"$(nproc)"
harness="$PWD/fuzz/fuzz-loader-builtin-libfuzzer"
if [ ! -x "$harness" ]; then
echo "::error::missing loader harness: $harness"
exit 1
fi
merge_seed="$FUZZ_BUILTIN_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$FUZZ_BUILTIN_CORPUS" -type f -print -quit | grep -q .; then
cp -a "$FUZZ_BUILTIN_CORPUS"/. "$merge_seed"/
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_common" "$FUZZ_BUILTIN_CORPUS" "$merge_seed"
else
echo "loader-autodetect corpus: no files to merge"
fi
rm -rf "$merge_seed"
make fuzz-run-loader-builtin \
FUZZ_BUILTIN_CORPUS="$FUZZ_BUILTIN_CORPUS" \
FUZZ_BUILTIN_ARGS="$fuzz_args" 2>&1 | tee "$run_log"
else
CC=clang CXX=clang++ meson setup builddir -Dfuzz=true
meson compile -C builddir -v
harness="$PWD/builddir/fuzz/fuzz-loader-builtin-libfuzzer"
if [ ! -x "$harness" ]; then
echo "::error::missing loader harness: $harness"
exit 1
fi
merge_seed="$FUZZ_BUILTIN_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$FUZZ_BUILTIN_CORPUS" -type f -print -quit | grep -q .; then
cp -a "$FUZZ_BUILTIN_CORPUS"/. "$merge_seed"/
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_common" "$FUZZ_BUILTIN_CORPUS" "$merge_seed"
else
echo "loader-autodetect corpus: no files to merge"
fi
rm -rf "$merge_seed"
export FUZZ_BUILTIN_ARGS="$fuzz_args"
meson compile -C builddir fuzz-run-loader-builtin 2>&1 | tee "$run_log"
fi
if ! grep -Eq 'DONE cov:|INFO: exiting: 0' "$run_log"; then
echo "::error::libFuzzer did not report completion for loader-autodetect target."
exit 1
fi
cov="$(grep -Eo 'cov: [0-9]+' "$run_log" | tail -n1 | awk '{print $2}')"
if [ -z "$cov" ] || [ "$cov" -lt "$min_cov" ]; then
echo "::error::loader-autodetect coverage too low: cov=$cov (expected >= $min_cov)."
exit 1
fi
- name: Upload loader-autodetect fuzz artifacts
if: ${{ always() && steps.autodetect_scope.outputs.run_target == '1' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-loader-autodetect
if-no-files-found: ignore
retention-days: 7
path: |
${{ env.FUZZ_BUILTIN_ARTIFACT_DIR }}
${{ env.FUZZ_BUILTIN_CORPUS }}
fuzz_push_encode_helpers:
if: >-
${{
github.event_name == 'push'
|| inputs.triage_job_labels != '||'
}}
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
buildtool: [autotools, meson]
target: [encoder-encode-bytes, helper-write-image-file]
env:
FUZZ_IMAGE_API_CORPUS: ${{ github.workspace }}/.tmp/fuzz-corpus-image-api/${{ matrix.target }}
FUZZ_IMAGE_API_ARTIFACT_DIR: ${{ github.workspace }}/.tmp/fuzz-artifacts-image-api/${{ matrix.buildtool }}/${{ matrix.target }}
FUZZ_IMAGE_API_ARGS_BASE: ${{ format('-fork={0} -print_final_stats=1 -timeout=5 -use_value_profile=1 -reduce_inputs=1 -rss_limit_mb=4096 -artifact_prefix={1}/.tmp/fuzz-artifacts-image-api/{2}/{3}/', github.event_name == 'push' && '10' || '2', github.workspace, matrix.buildtool, matrix.target) }}
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:report_error_type=1
steps:
- name: Select image API fuzz scope
id: image_api_scope
shell: bash
run: |
set -euo pipefail
run_target=1
if test "${{ github.event_name }}" = "workflow_dispatch"; then
label="${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-${{ matrix.target }}"
run_target=0
case "${{ inputs.triage_job_labels }}" in
*"|$label|"*)
run_target=1
;;
esac
fi
echo "run_target=$run_target" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
if: ${{ steps.image_api_scope.outputs.run_target == '1' }}
with:
fetch-depth: 1
- name: Install build dependencies
if: ${{ steps.image_api_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
bash .github/scripts/disable-hosted-runner-microsoft-apt-sources.sh
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang meson ninja-build autoconf automake libtool pkg-config make
- name: Restore image API fuzz corpus cache
if: ${{ steps.image_api_scope.outputs.run_target == '1' }}
uses: actions/cache@v5
with:
path: ${{ env.FUZZ_IMAGE_API_CORPUS }}
key: fuzz-corpus-image-api-push-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
fuzz-corpus-image-api-push-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-
fuzz-corpus-image-api-push-${{ matrix.buildtool }}-${{ matrix.target }}-
fuzz-corpus-image-api-${{ matrix.buildtool }}-${{ matrix.target }}-
- name: Seed image API fuzz corpus
if: ${{ steps.image_api_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$FUZZ_IMAGE_API_CORPUS" "$FUZZ_IMAGE_API_ARTIFACT_DIR"
case "${{ matrix.target }}" in
encoder-encode-bytes)
printf '\x00\x00\x00\x00\x00\x00\xff\x00\x00' \
> "$FUZZ_IMAGE_API_CORPUS/encoder-rgb888-1x1"
printf '\x03\x00\x00\x00\x00\x00\x7f' \
> "$FUZZ_IMAGE_API_CORPUS/encoder-g8-1x1"
printf '\x02\x00\x00\x01\x00\x00\x00\x00\x00\xff\xff\xff' \
> "$FUZZ_IMAGE_API_CORPUS/encoder-pal8-1x1"
;;
helper-write-image-file)
printf '\x00\x00\x04\x00\xff\x00\x00' \
> "$FUZZ_IMAGE_API_CORPUS/helper-rgb888-png-1x1"
printf '\x01\x01\x03\x01\x00\x55\xaa\xff' \
> "$FUZZ_IMAGE_API_CORPUS/helper-pal8-gif-2x2"
printf '\x00\x00\x12\x07\xff\x00\x00\xff' \
> "$FUZZ_IMAGE_API_CORPUS/helper-rgba8888-sixel-1x1"
;;
esac
if test -d tests/data/security/fuzzing/data; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_IMAGE_API_CORPUS/" || true
done < <(find tests/data/security/fuzzing/data -type f -print0)
fi
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_IMAGE_API_CORPUS/" || true
done < <(find tests/data -type f \
\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \
-o -name '*.gif' -o -name '*.webp' -o -name '*.tiff' \
-o -name '*.pnm' -o -name '*.ppm' -o -name '*.pgm' \
-o -name '*.pbm' -o -name '*.psd' -o -name '*.hdr' \
-o -name '*.six' -o -name '*.sixel' -o -name '*.pic' \
-o -name '*.tga' -o -name '*.bmp' -o -name '*.bin' \) \
-print0)
echo "image-api seed files (${{ matrix.target }}): $(find "$FUZZ_IMAGE_API_CORPUS" -type f | wc -l)"
- name: Build and run image API fuzz target
if: ${{ steps.image_api_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
export PATH="$PWD/build-aux:$PWD/bin:$PATH"
run_log="$FUZZ_IMAGE_API_ARTIFACT_DIR/run.log"
min_cov=10
dict_common="$PWD/fuzz/loader-builtin.dict"
fuzz_args="$FUZZ_IMAGE_API_ARGS_BASE -max_total_time=600 -dict=$dict_common"
run_target="run-${{ matrix.target }}"
meson_run_target="fuzz-run-${{ matrix.target }}"
harness_name="fuzz-${{ matrix.target }}-libfuzzer"
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
CC=clang CXX=clang++ ./configure --enable-fuzz
make -j"$(nproc)"
harness="$PWD/fuzz/$harness_name"
else
CC=clang CXX=clang++ meson setup builddir -Dfuzz=true
meson compile -C builddir -v
harness="$PWD/builddir/fuzz/$harness_name"
fi
if [ ! -x "$harness" ]; then
echo "::error::missing image API harness: $harness"
exit 1
fi
merge_seed="$FUZZ_IMAGE_API_ARTIFACT_DIR/corpus-premerge"
rm -rf "$merge_seed"
mkdir -p "$merge_seed"
if find "$FUZZ_IMAGE_API_CORPUS" -type f -print -quit | grep -q .; then
cp -a "$FUZZ_IMAGE_API_CORPUS"/. "$merge_seed"/
"$harness" -merge=1 -reduce_inputs=1 -timeout=5 -rss_limit_mb=4096 \
-dict="$dict_common" "$FUZZ_IMAGE_API_CORPUS" "$merge_seed"
else
echo "image-api corpus: no files to merge for ${{ matrix.target }}"
fi
rm -rf "$merge_seed"
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
case "${{ matrix.target }}" in
encoder-encode-bytes)
make -C fuzz "$run_target" \
FUZZ_ENCODE_BYTES_CORPUS="$FUZZ_IMAGE_API_CORPUS" \
FUZZ_ENCODE_BYTES_ARGS="$fuzz_args" 2>&1 | tee "$run_log"
;;
helper-write-image-file)
make -C fuzz "$run_target" \
FUZZ_WRITE_IMAGE_CORPUS="$FUZZ_IMAGE_API_CORPUS" \
FUZZ_WRITE_IMAGE_ARGS="$fuzz_args" 2>&1 | tee "$run_log"
;;
esac
else
case "${{ matrix.target }}" in
encoder-encode-bytes)
export FUZZ_ENCODE_BYTES_CORPUS="$FUZZ_IMAGE_API_CORPUS"
export FUZZ_ENCODE_BYTES_ARGS="$fuzz_args"
;;
helper-write-image-file)
export FUZZ_WRITE_IMAGE_CORPUS="$FUZZ_IMAGE_API_CORPUS"
export FUZZ_WRITE_IMAGE_ARGS="$fuzz_args"
;;
esac
meson compile -C builddir "$meson_run_target" 2>&1 | tee "$run_log"
fi
if ! grep -Eq 'DONE cov:|INFO: exiting: 0' "$run_log"; then
echo "::error::libFuzzer did not report completion for image API target '${{ matrix.target }}'."
exit 1
fi
cov="$(grep -Eo 'cov: [0-9]+' "$run_log" | tail -n1 | awk '{print $2}')"
if [ -z "$cov" ] || [ "$cov" -lt "$min_cov" ]; then
echo "::error::image API target '${{ matrix.target }}' coverage too low: cov=$cov (expected >= $min_cov)."
exit 1
fi
- name: Upload image API fuzz artifacts
if: ${{ always() && steps.image_api_scope.outputs.run_target == '1' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-gcc' || 'Meson-ubuntu-x86_64-gcc' }}-fuzz-${{ matrix.target }}
if-no-files-found: ignore
retention-days: 7
path: |
${{ env.FUZZ_IMAGE_API_ARTIFACT_DIR }}
${{ env.FUZZ_IMAGE_API_CORPUS }}
fuzz_push_cli_img2sixel:
if: >-
${{
github.event_name == 'push'
|| inputs.triage_job_labels != '||'
}}
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-clang' || 'Meson-ubuntu-x86_64-clang' }}-fuzz-cli-img2sixel-${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
buildtool: [autotools, meson]
target: [combo, parser, boundary, pathenv, numeric]
env:
FUZZ_INPUT_DIR: ${{ github.workspace }}/.tmp/fuzz-corpus-cli-img2sixel/${{ matrix.target }}
FUZZ_OUTPUT_DIR: ${{ github.workspace }}/.tmp/fuzz-artifacts-cli-img2sixel/${{ matrix.buildtool }}/${{ matrix.target }}
FUZZ_AFL: afl-fuzz
FUZZ_TIMEOUT: 600
FUZZ_AFL_WORKERS: ${{ github.event_name == 'push' && '6' || '2' }}
FUZZ_AFL_MODE: ${{ github.event_name == 'push' && matrix.buildtool == 'autotools' && 'cmplog' || 'normal' }}
FUZZ_AFL_CMPLOG_RUNNER: ""
FUZZ_AFL_EXTRA_OPTS: ""
AFL_USE_ASAN: 1
AFL_USE_UBSAN: 1
AFL_NO_AFFINITY: 1
AFL_SKIP_CPUFREQ: 1
AFL_IMPORT_FIRST: 1
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1:symbolize=0
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:report_error_type=1
steps:
- name: Select CLI fuzz scope
id: cli_scope
shell: bash
run: |
set -euo pipefail
# Execute all CLI fuzz matrix jobs on push; timeout policy is handled
# by FUZZ_TIMEOUT/fuzz_timeout below.
run_target=1
if test "${{ github.event_name }}" = "workflow_dispatch"; then
label="${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-clang' || 'Meson-ubuntu-x86_64-clang' }}-fuzz-cli-img2sixel-${{ matrix.target }}"
run_target=0
case "${{ inputs.triage_job_labels }}" in
*"|$label|"*)
run_target=1
;;
esac
fi
echo "run_target=$run_target" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
if: ${{ steps.cli_scope.outputs.run_target == '1' }}
with:
fetch-depth: 1
- name: Install build dependencies
if: ${{ steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
bash .github/scripts/disable-hosted-runner-microsoft-apt-sources.sh
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
afl++ clang meson ninja-build autoconf automake libtool pkg-config make
- name: Configure AFL core_pattern
if: ${{ steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
before="$(cat /proc/sys/kernel/core_pattern)"
echo "kernel.core_pattern(before)=$before"
if [[ "$before" == \|* ]]; then
echo core | sudo tee /proc/sys/kernel/core_pattern >/dev/null
fi
after="$(cat /proc/sys/kernel/core_pattern)"
echo "kernel.core_pattern(after)=$after"
- name: Restore CLI fuzz corpus cache
if: ${{ steps.cli_scope.outputs.run_target == '1' }}
uses: actions/cache@v5
with:
path: ${{ env.FUZZ_INPUT_DIR }}
key: fuzz-corpus-cli-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
fuzz-corpus-cli-${{ matrix.buildtool }}-${{ matrix.target }}-${{ github.ref_name }}-
fuzz-corpus-cli-${{ matrix.buildtool }}-${{ matrix.target }}-
- name: Seed CLI fuzz corpus
if: ${{ steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$FUZZ_INPUT_DIR" "$FUZZ_OUTPUT_DIR"
if test -d fuzz/corpus/img2sixel-cli; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_INPUT_DIR/" || true
done < <(find fuzz/corpus/img2sixel-cli -type f -print0)
fi
if test -d tests/data/security/fuzzing/data; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_INPUT_DIR/" || true
done < <(find tests/data/security/fuzzing/data -type f -print0)
fi
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_INPUT_DIR/" || true
done < <(find tests/data -type f \
\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \
-o -name '*.gif' -o -name '*.webp' -o -name '*.tiff' \
-o -name '*.pnm' -o -name '*.ppm' -o -name '*.pgm' \
-o -name '*.pbm' -o -name '*.psd' -o -name '*.hdr' \
-o -name '*.six' -o -name '*.sixel' -o -name '*.pic' \
-o -name '*.tga' -o -name '*.bmp' -o -name '*.bin' \) \
-print0)
echo "cli-corpus seed files (${{ matrix.target }}): $(find "$FUZZ_INPUT_DIR" -type f | wc -l)"
- name: Build and run img2sixel CLI fuzz target
if: ${{ steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
export PATH="$PWD/build-aux:$PWD/bin:$PATH"
sanitizer_cflags="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined"
sanitizer_ldflags="-fsanitize=address,undefined"
mkdir -p "$FUZZ_OUTPUT_DIR"
fuzz_target='fuzz-cli-img2sixel'
asan_runner="$PWD/.tmp/fuzz-img2sixel-afl-persistent"
if [ "${{ matrix.target }}" = "parser" ]; then
fuzz_target='fuzz-cli-img2sixel-parser'
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-parser-afl-persistent"
elif [ "${{ matrix.target }}" = "boundary" ]; then
fuzz_target='fuzz-cli-img2sixel-boundary'
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-boundary-afl-persistent"
elif [ "${{ matrix.target }}" = "pathenv" ]; then
fuzz_target='fuzz-cli-img2sixel-pathenv'
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-pathenv-afl-persistent"
elif [ "${{ matrix.target }}" = "numeric" ]; then
fuzz_target='fuzz-cli-img2sixel-numeric'
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-numeric-afl-persistent"
fi
fuzz_timeout="$FUZZ_TIMEOUT"
# Keep parser-focused CLI modes running longer for hang/crash signal.
case "${{ matrix.target }}" in
combo|parser)
fuzz_timeout=1800
;;
esac
export FUZZ_TIMEOUT="$fuzz_timeout"
resolve_runner_path() {
local runner="$1"
local candidate
if [ -f "$runner" ] && head -n 1 "$runner" | grep -q '^#!'; then
candidate="$(dirname "$runner")/.libs/$(basename "$runner")"
if [ -x "$candidate" ]; then
runner="$candidate"
fi
fi
printf '%s\n' "$runner"
}
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
CC=afl-clang-fast CXX=afl-clang-fast++ \
CFLAGS="$sanitizer_cflags" LDFLAGS="$sanitizer_ldflags" \
./configure
make -j"$(nproc)"
if [ "$FUZZ_AFL_MODE" = "cmplog" ]; then
cmplog_runner="$PWD/.tmp/fuzz-img2sixel-${{ matrix.target }}-afl-cmplog-persistent"
case "${{ matrix.target }}" in
combo)
AFL_LLVM_CMPLOG=1 make "$cmplog_runner" \
FUZZ_IMG2SIXEL_PERSISTENT="$cmplog_runner"
;;
parser)
AFL_LLVM_CMPLOG=1 make "$cmplog_runner" \
FUZZ_IMG2SIXEL_CLI_PARSER_PERSISTENT="$cmplog_runner"
;;
boundary)
AFL_LLVM_CMPLOG=1 make "$cmplog_runner" \
FUZZ_IMG2SIXEL_CLI_BOUNDARY_PERSISTENT="$cmplog_runner"
;;
pathenv)
AFL_LLVM_CMPLOG=1 make "$cmplog_runner" \
FUZZ_IMG2SIXEL_CLI_PATHENV_PERSISTENT="$cmplog_runner"
;;
numeric)
AFL_LLVM_CMPLOG=1 make "$cmplog_runner" \
FUZZ_IMG2SIXEL_CLI_NUMERIC_PERSISTENT="$cmplog_runner"
;;
esac
export FUZZ_AFL_CMPLOG_RUNNER="$cmplog_runner"
fi
make "$fuzz_target"
else
CC=afl-clang-fast CXX=afl-clang-fast++ \
CFLAGS="$sanitizer_cflags" LDFLAGS="$sanitizer_ldflags" \
meson setup builddir
meson compile -C builddir -v
if [ "${{ matrix.target }}" = "combo" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-afl-persistent"
elif [ "${{ matrix.target }}" = "parser" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-parser-afl-persistent"
elif [ "${{ matrix.target }}" = "boundary" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-boundary-afl-persistent"
elif [ "${{ matrix.target }}" = "pathenv" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-pathenv-afl-persistent"
else
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-numeric-afl-persistent"
fi
meson compile -C builddir "$fuzz_target"
fi
asan_runner="$(resolve_runner_path "$asan_runner")"
if [ ! -x "$asan_runner" ]; then
echo "::error::missing CLI ASAN runner: $asan_runner"
exit 1
fi
mapfile -d '' -t crash_inputs < <(find "$FUZZ_OUTPUT_DIR" -type f -path '*/crashes/*' \
! -name 'README.txt' -print0)
crash_count="${#crash_inputs[@]}"
queue_count="$(find "$FUZZ_OUTPUT_DIR" -type f -path '*/queue/*' \
! -name 'README.txt' | wc -l | tr -d ' ')"
hang_count="$(find "$FUZZ_OUTPUT_DIR" -type f -path '*/hangs/*' \
! -name 'README.txt' | wc -l | tr -d ' ')"
echo "cli-afl findings: queue=${queue_count} hangs=${hang_count} crashes=${crash_count}"
if [ "${hang_count}" -gt 0 ]; then
echo "::warning::AFL detected ${hang_count} hang input(s)."
find "$FUZZ_OUTPUT_DIR" -type f -path '*/hangs/*' \
! -name 'README.txt' -print
fi
if [ "${crash_count}" -gt 0 ]; then
triage_dir="$FUZZ_OUTPUT_DIR/triage"
mkdir -p "$triage_dir"
for crash_input in "${crash_inputs[@]}"; do
.github/scripts/fuzz/triage_afl_crash.sh \
--crash-file "$crash_input" \
--triage-dir "$triage_dir" \
--asan-runner "$asan_runner" \
--timeout 20 \
--label "${{ matrix.buildtool }}-${{ matrix.target }}"
done
asan_repro_count="$(grep -R --include '*.json' -h '"asan_reproduced": true' "$triage_dir" | wc -l | tr -d ' ' || true)"
echo "::error::AFL detected ${crash_count} crash input(s); ASAN-reproducible=${asan_repro_count}."
find "$FUZZ_OUTPUT_DIR" -type f -path '*/crashes/*' \
! -name 'README.txt' -print
exit 1
fi
- name: Harvest CLI AFL queue into corpus
if: ${{ always() && steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$FUZZ_INPUT_DIR"
if [ -d "$FUZZ_OUTPUT_DIR" ]; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$FUZZ_INPUT_DIR/" || true
done < <(find "$FUZZ_OUTPUT_DIR" -type f -path '*/queue/*' \
! -name 'README.txt' -print0)
fi
echo "cli-corpus files after harvest (${{ matrix.target }}): $(find "$FUZZ_INPUT_DIR" -type f | wc -l)"
- name: Minimize CLI corpus with AFL cmin
if: ${{ always() && steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
before_count="$(find "$FUZZ_INPUT_DIR" -type f | wc -l | tr -d ' ')"
if [ "$before_count" -eq 0 ]; then
echo "cli-corpus minimize: skipped (empty corpus)"
exit 0
fi
if ! command -v afl-cmin >/dev/null 2>&1; then
echo "::warning::afl-cmin was not found; skip corpus minimization."
exit 0
fi
asan_runner="$PWD/.tmp/fuzz-img2sixel-afl-persistent"
if [ "${{ matrix.buildtool }}" = "meson" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-afl-persistent"
fi
case "${{ matrix.target }}" in
parser)
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-parser-afl-persistent"
if [ "${{ matrix.buildtool }}" = "meson" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-parser-afl-persistent"
fi
;;
boundary)
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-boundary-afl-persistent"
if [ "${{ matrix.buildtool }}" = "meson" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-boundary-afl-persistent"
fi
;;
pathenv)
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-pathenv-afl-persistent"
if [ "${{ matrix.buildtool }}" = "meson" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-pathenv-afl-persistent"
fi
;;
numeric)
asan_runner="$PWD/.tmp/fuzz-img2sixel-cli-numeric-afl-persistent"
if [ "${{ matrix.buildtool }}" = "meson" ]; then
asan_runner="$PWD/builddir/converters/fuzz-img2sixel-cli-numeric-afl-persistent"
fi
;;
esac
if [ -f "$asan_runner" ] && head -n 1 "$asan_runner" | grep -q '^#!'; then
candidate="$(dirname "$asan_runner")/.libs/$(basename "$asan_runner")"
if [ -x "$candidate" ]; then
asan_runner="$candidate"
fi
fi
if [ ! -x "$asan_runner" ]; then
echo "::warning::missing CLI runner for corpus minimization: $asan_runner"
exit 0
fi
minimized_dir="${FUZZ_INPUT_DIR}.cmin"
rm -rf "$minimized_dir"
set +e
timeout 180 afl-cmin -i "$FUZZ_INPUT_DIR" -o "$minimized_dir" -- "$asan_runner" >/dev/null 2>&1
cmin_rc=$?
set -e
if [ "$cmin_rc" -ne 0 ]; then
echo "::warning::afl-cmin failed (rc=$cmin_rc); keep original corpus."
rm -rf "$minimized_dir"
exit 0
fi
after_count="$(find "$minimized_dir" -type f | wc -l | tr -d ' ')"
if [ "$after_count" -eq 0 ]; then
echo "::warning::afl-cmin produced an empty corpus; keep original corpus."
rm -rf "$minimized_dir"
exit 0
fi
find "$FUZZ_INPUT_DIR" -mindepth 1 -delete
cp -a "$minimized_dir"/. "$FUZZ_INPUT_DIR"/
rm -rf "$minimized_dir"
echo "cli-corpus minimized (${{ matrix.target }}): before=${before_count} after=${after_count}"
- name: Pack img2sixel CLI fuzz artifacts
if: ${{ always() && steps.cli_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
archive_output="${FUZZ_OUTPUT_DIR}.tar.gz"
archive_corpus="${FUZZ_INPUT_DIR}.tar.gz"
if [ -d "$FUZZ_OUTPUT_DIR" ]; then
tar -C "$FUZZ_OUTPUT_DIR" -czf "$archive_output" .
fi
if [ -d "$FUZZ_INPUT_DIR" ]; then
tar -C "$FUZZ_INPUT_DIR" -czf "$archive_corpus" .
fi
- name: Upload img2sixel CLI fuzz artifacts
if: ${{ always() && steps.cli_scope.outputs.run_target == '1' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-ubuntu-x86_64-clang' || 'Meson-ubuntu-x86_64-clang' }}-fuzz-cli-img2sixel-${{ matrix.target }}
if-no-files-found: ignore
retention-days: 7
path: |
${{ env.FUZZ_OUTPUT_DIR }}.tar.gz
${{ env.FUZZ_INPUT_DIR }}.tar.gz
fuzz_push_loader_coregraphics:
if: >-
${{
github.event_name == 'push'
|| inputs.triage_job_labels != '||'
}}
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-macos-aarch64-clang' || 'Meson-macos-aarch64-clang' }}-fuzz-loader-coregraphics
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
buildtool: [autotools, meson]
env:
FUZZ_COMPONENT_CORPUS: ${{ github.workspace }}/.tmp/fuzz-corpus-loader/component
FUZZ_COMPONENT_ARTIFACT_DIR: ${{ github.workspace }}/.tmp/fuzz-artifacts-loader/coregraphics
FUZZ_COMPONENT_ARGS_BASE: ${{ format('-fork={0} -print_final_stats=1 -timeout=20 -use_value_profile=1 -reduce_inputs=1 -rss_limit_mb=4096 -ignore_ooms=1 -artifact_prefix={1}/.tmp/fuzz-artifacts-loader/coregraphics/', github.event_name == 'push' && '10' || '2', github.workspace) }}
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:report_error_type=1
steps:
- name: Select coregraphics fuzz scope
id: coregraphics_scope
shell: bash
run: |
set -euo pipefail
# Keep both build systems enabled so coregraphics fuzzing does not
# silently skip half of the matrix on push.
run_target=1
if test "${{ github.event_name }}" = "workflow_dispatch"; then
label="${{ matrix.buildtool == 'autotools' && 'Autotools-macos-aarch64-clang' || 'Meson-macos-aarch64-clang' }}-fuzz-loader-coregraphics"
run_target=0
case "${{ inputs.triage_job_labels }}" in
*"|$label|"*)
run_target=1
;;
esac
fi
echo "run_target=$run_target" >> "$GITHUB_OUTPUT"
- name: Disable Spotlight (macOS)
if: ${{ steps.coregraphics_scope.outputs.run_target == '1' }}
shell: bash
run: |
sudo mdutil -i off / || true
sudo mdutil -i off /System/Volumes/Data || true
- uses: actions/checkout@v6
if: ${{ steps.coregraphics_scope.outputs.run_target == '1' }}
with:
fetch-depth: 1
- name: Install build dependencies
if: ${{ steps.coregraphics_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
brew update
brew install llvm meson ninja autoconf automake libtool pkg-config
- name: Restore coregraphics fuzz corpus cache
if: ${{ steps.coregraphics_scope.outputs.run_target == '1' }}
uses: actions/cache@v5
with:
path: ${{ env.FUZZ_COMPONENT_CORPUS }}
key: fuzz-corpus-loader-coregraphics-push-${{ matrix.buildtool }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
fuzz-corpus-loader-coregraphics-push-${{ matrix.buildtool }}-${{ github.ref_name }}-
fuzz-corpus-loader-coregraphics-push-${{ matrix.buildtool }}-
fuzz-corpus-loader-coregraphics-${{ matrix.buildtool }}-
- name: Seed coregraphics fuzz corpus
if: ${{ steps.coregraphics_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euo pipefail
target_corpus="$FUZZ_COMPONENT_CORPUS/coregraphics"
mkdir -p "$target_corpus" "$FUZZ_COMPONENT_ARTIFACT_DIR"
if test -d tests/data/security/fuzzing/data; then
while IFS= read -r -d '' seed; do
cp -n "$seed" "$target_corpus/" || true
done < <(find tests/data/security/fuzzing/data -type f -print0)
fi
while IFS= read -r -d '' seed; do
cp -n "$seed" "$target_corpus/" || true
done < <(find tests/data -type f \
\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \
-o -name '*.gif' -o -name '*.webp' -o -name '*.tiff' \
-o -name '*.pnm' -o -name '*.ppm' -o -name '*.pgm' \
-o -name '*.pbm' -o -name '*.psd' -o -name '*.hdr' \
-o -name '*.six' -o -name '*.sixel' -o -name '*.pic' \
-o -name '*.tga' -o -name '*.bmp' -o -name '*.bin' \) \
-print0)
# This seed repeatedly triggers ImageIO-side timeout traces on macOS
# coregraphics fuzz runs, making the job flaky without actionable
# libsixel signal.
find "$target_corpus" -type f -name 'gif-transparent-anim-dispose2.gif' -print -delete || true
echo "coregraphics-corpus seed files: $(find "$target_corpus" -type f | wc -l)"
- name: Build and run coregraphics loader fuzz harness
if: ${{ steps.coregraphics_scope.outputs.run_target == '1' }}
shell: bash
run: |
set -euxo pipefail
LLVM_PREFIX="$(brew --prefix llvm)"
export PATH="$LLVM_PREFIX/bin:$PWD/build-aux:$PWD/bin:$PATH"
export CC="$LLVM_PREFIX/bin/clang"
export CXX="$LLVM_PREFIX/bin/clang++"
export FUZZ_CFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=fuzzer-no-link,address,undefined"
export FUZZ_LDFLAGS="-fsanitize=fuzzer,address,undefined"
run_log="$FUZZ_COMPONENT_ARTIFACT_DIR/run.log"
min_cov=10
dict_common="$PWD/fuzz/loader-builtin.dict"
dict_target="$PWD/fuzz/dicts/component/coregraphics.dict"
dict_merged="$FUZZ_COMPONENT_ARTIFACT_DIR/dict.merged"
if [ -f "$dict_target" ]; then
awk '!seen[$0]++' "$dict_common" "$dict_target" > "$dict_merged"
else
cp "$dict_common" "$dict_merged"
fi
component_max_total_time=600
# CoreGraphics is a focus target on push runs.
if [ "${{ matrix.buildtool }}" = "meson" ]; then
component_max_total_time=1800
fi
fuzz_args="$FUZZ_COMPONENT_ARGS_BASE -max_total_time=$component_max_total_time -dict=$dict_merged"
if [ "${{ matrix.buildtool }}" = "autotools" ]; then
./configure --enable-fuzz --with-coregraphics
make -C src -j"$(sysctl -n hw.ncpu)" libsixel.la
make -C fuzz -j"$(sysctl -n hw.ncpu)" fuzz-loader-coregraphics-libfuzzer
harness="$PWD/fuzz/fuzz-loader-coregraphics-libfuzzer"
if [ ! -x "$harness" ]; then
echo "::error::missing coregraphics harness: $harness"
exit 1
fi
echo "coregraphics-corpus: skipping premerge to avoid ImageIO timeout backtraces on macOS"
make -C fuzz run-loader-coregraphics \
FUZZ_COMPONENT_CORPUS="$FUZZ_COMPONENT_CORPUS" \
FUZZ_COMPONENT_ARGS="$fuzz_args" 2>&1 \
| sed -E '/^RGBE read error: No such file or directory$/d; /^RGBE bad file format: no FORMAT specifier found$/d; /^Unknown error: 260$/d' \
| tee "$run_log"
else
meson setup builddir -Dfuzz=true -Dcoregraphics=enabled
meson compile -C builddir -v fuzz-loader-coregraphics-libfuzzer
harness="$PWD/builddir/fuzz/fuzz-loader-coregraphics-libfuzzer"
if [ ! -x "$harness" ]; then
echo "::error::missing coregraphics harness: $harness"
exit 1
fi
echo "coregraphics-corpus: skipping premerge to avoid ImageIO timeout backtraces on macOS"
export FUZZ_COMPONENT_ARGS="$fuzz_args"
meson compile -C builddir -v fuzz-run-loader-coregraphics 2>&1 \
| sed -E '/^RGBE read error: No such file or directory$/d; /^RGBE bad file format: no FORMAT specifier found$/d; /^Unknown error: 260$/d' \
| tee "$run_log"
fi
if ! grep -Eq 'DONE cov:|INFO: exiting: 0' "$run_log"; then
echo "::error::libFuzzer did not report completion for coregraphics loader target."
exit 1
fi
cov="$(grep -Eo 'cov: [0-9]+' "$run_log" | tail -n1 | awk '{print $2}')"
if [ -z "$cov" ] || [ "$cov" -lt "$min_cov" ]; then
echo "::error::coregraphics loader coverage too low: cov=$cov (expected >= $min_cov)."
exit 1
fi
- name: Upload coregraphics fuzz artifacts
if: ${{ always() && steps.coregraphics_scope.outputs.run_target == '1' }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.buildtool == 'autotools' && 'Autotools-macos-aarch64-clang' || 'Meson-macos-aarch64-clang' }}-fuzz-loader-coregraphics
if-no-files-found: ignore
retention-days: 7
path: |
${{ env.FUZZ_COMPONENT_ARTIFACT_DIR }}
${{ env.FUZZ_COMPONENT_CORPUS }}
fuzz_push_loader_wic_detect:
if: >-
${{
github.event_name == 'push'
|| contains(inputs.triage_job_labels, '|Meson-cmd-x86_64-msvc-fuzz-loader-wic-detect|')
|| contains(inputs.triage_job_labels, '|Meson-cmd-x86_64-msvc-fuzz-loader-wic-repro-nosan|')
}}
name: Meson-cmd-x86_64-msvc-fuzz-loader-wic-detect
runs-on: windows-latest
outputs:
had_crash: ${{ steps.detect.outputs.had_crash }}
fuzz_exit: ${{ steps.detect.outputs.fuzz_exit }}
crash_count: ${{ steps.detect.outputs.crash_count }}
steps:
- name: Set Windows Defender ExclusionPath (Windows)
shell: pwsh
run: |
try {
Add-MpPreference -ExclusionPath "C:\a" -ErrorAction Stop
} catch {
Write-Warning "Windows Defender exclusion setup failed; continuing: $($_.Exception.Message)"
}
- name: Disable Windows Search Indexer (Windows)
shell: pwsh
run: |
Stop-Service WSearch
- name: Configure git EOL
shell: bash
run: git config --global core.autocrlf false
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup MSVC developer env (vcvars, x86_64 target)
shell: pwsh
run: |
& "${{ github.workspace }}\.github\scripts\setup-msvc-env.ps1" -Arch x64
- name: Build and run wic loader fuzz harness (detect)
id: detect
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
python -m pip install --upgrade pip
python -m pip install meson ninja
$clangCommand = Get-Command clang -ErrorAction SilentlyContinue
if (-not $clangCommand) {
throw "clang was not found in PATH after MSVC environment setup."
}
$clangRuntimeDir = Split-Path -Parent $clangCommand.Source
$env:PATH = "$clangRuntimeDir;$env:PATH"
$workspace = (Get-Location).Path
$fuzzCorpusRoot = Join-Path $workspace '.tmp\fuzz-corpus-loader\component'
$targetCorpus = Join-Path $fuzzCorpusRoot 'wic'
$artifactDir = Join-Path $workspace '.tmp\fuzz-artifacts-loader\wic'
New-Item -ItemType Directory -Force -Path $targetCorpus, $artifactDir | Out-Null
# WIC (windowscodecs/combase) can trigger allocator-size probes on
# foreign pointers under ASan on Windows; disable that noisy check so
# this job can surface actionable libsixel-side faults.
$env:ASAN_OPTIONS = 'detect_leaks=0:abort_on_error=1:check_malloc_usable_size=0'
$env:UBSAN_OPTIONS = 'halt_on_error=1:print_stacktrace=1:report_error_type=1'
$probeSource = Join-Path $env:TEMP 'libfuzzer_probe.c'
@(
'int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size) {',
' (void)data;',
' (void)size;',
' return 0;',
'}'
) | Set-Content -LiteralPath $probeSource -Encoding ASCII
& clang -x c -c $probeSource "-fsanitize=fuzzer-no-link,address,undefined" -o (Join-Path $env:TEMP 'libfuzzer_probe.obj')
if ($LASTEXITCODE -ne 0) {
throw "libFuzzer compile flags are unsupported in this MSVC ABI toolchain."
}
$probeExe = Join-Path $env:TEMP 'libfuzzer_probe.exe'
$probeLog = Join-Path $artifactDir 'libfuzzer-probe.log'
& clang -x c $probeSource "-fsanitize=fuzzer,address,undefined" -o $probeExe
if ($LASTEXITCODE -ne 0) {
throw "libFuzzer link flags are unsupported in this MSVC ABI toolchain."
}
New-Item -ItemType File -Force -Path $probeLog | Out-Null
& $probeExe '-runs=1' 2>&1 | Tee-Object -FilePath $probeLog
if ($LASTEXITCODE -ne 0) {
Write-Warning "libFuzzer runtime probe failed in this MSVC ABI toolchain; skipping WIC ASan fuzz detect."
"fuzz_exit=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"had_crash=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"crash_count=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
$global:LASTEXITCODE = 0
exit 0
}
function Copy-SeedFiles([string]$Root, [string[]]$Patterns) {
if (-not (Test-Path $Root)) {
return
}
$items = if ($Patterns.Count -eq 0) {
Get-ChildItem -Path $Root -Recurse -File
} else {
Get-ChildItem -Path $Root -Recurse -File -Include $Patterns
}
foreach ($item in $items) {
$dst = Join-Path $targetCorpus $item.Name
if (-not (Test-Path $dst)) {
Copy-Item -LiteralPath $item.FullName -Destination $dst
}
}
}
Copy-SeedFiles (Join-Path $workspace 'tests/data/security/fuzzing/data') @()
Copy-SeedFiles (Join-Path $workspace 'tests/data') @(
'*.png', '*.jpg', '*.jpeg',
'*.gif', '*.webp', '*.tiff',
'*.pnm', '*.ppm', '*.pgm',
'*.pbm', '*.psd', '*.hdr',
'*.six', '*.sixel', '*.pic',
'*.tga', '*.bmp', '*.bin'
)
$seedCount = (Get-ChildItem -Path $targetCorpus -Recurse -File -ErrorAction SilentlyContinue | Measure-Object).Count
Write-Host "wic-corpus seed files: $seedCount"
$dictCommon = Join-Path $workspace 'fuzz/loader-builtin.dict'
$dictTarget = Join-Path $workspace 'fuzz/dicts/component/wic.dict'
$dictMerged = Join-Path $artifactDir 'dict.merged'
$seen = @{}
$mergedLines = New-Object 'System.Collections.Generic.List[string]'
foreach ($dict in @($dictCommon, $dictTarget)) {
if (-not (Test-Path $dict)) {
continue
}
foreach ($line in Get-Content -LiteralPath $dict) {
if (-not $seen.ContainsKey($line)) {
$seen[$line] = $true
[void]$mergedLines.Add($line)
}
}
}
$writer = New-Object System.IO.StreamWriter($dictMerged, $false, [System.Text.UTF8Encoding]::new($false))
$writer.NewLine = "`n"
foreach ($line in $mergedLines) {
$writer.WriteLine($line)
}
$writer.Dispose()
$artifactPrefix = ((Join-Path $artifactDir '').Replace('\', '/'))
$dictMergedPosix = $dictMerged.Replace('\', '/')
$targetCorpusPosix = $targetCorpus.Replace('\', '/')
$fuzzArgs = @(
'-max_total_time=1800',
'-fork=4',
'-print_final_stats=1',
'-timeout=5',
'-use_value_profile=1',
'-reduce_inputs=1',
'-rss_limit_mb=4096',
"-artifact_prefix=$artifactPrefix",
"-dict=$dictMergedPosix"
)
$env:CC = 'clang'
$env:CXX = 'clang++'
python -m mesonbuild.mesonmain setup builddir -Dfuzz=true -Dwic=enabled -Dfuzz_sanitizers=full
python -m mesonbuild.mesonmain compile -C builddir -v fuzz-loader-wic-libfuzzer
$harness = Join-Path $workspace 'builddir\fuzz\fuzz-loader-wic-libfuzzer.exe'
if (-not (Test-Path $harness)) {
$harnessNoExt = Join-Path $workspace 'builddir\fuzz\fuzz-loader-wic-libfuzzer'
if (Test-Path $harnessNoExt) {
$harness = $harnessNoExt
} else {
throw "missing wic harness: $harness"
}
}
foreach ($runtimeDir in @(
(Join-Path $workspace 'builddir\src'),
(Join-Path $workspace 'builddir\fuzz')
)) {
if (Test-Path $runtimeDir) {
$env:PATH = "$runtimeDir;$env:PATH"
}
}
$mergeSeed = Join-Path $artifactDir 'corpus-premerge'
$premergeLog = Join-Path $artifactDir 'premerge.log'
if (Test-Path $premergeLog) {
Remove-Item -LiteralPath $premergeLog -Force
}
if (Test-Path $mergeSeed) {
Remove-Item -LiteralPath $mergeSeed -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $mergeSeed | Out-Null
if (Get-ChildItem -Path $targetCorpus -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1) {
Copy-Item -Path (Join-Path $targetCorpus '*') -Destination $mergeSeed -Recurse -Force
New-Item -ItemType File -Force -Path $premergeLog | Out-Null
& $harness '-merge=1' '-reduce_inputs=1' '-timeout=5' '-rss_limit_mb=4096' "-dict=$dictMergedPosix" $targetCorpusPosix ($mergeSeed.Replace('\', '/')) 2>&1 | Tee-Object -FilePath $premergeLog
$premergeExit = $LASTEXITCODE
if ($premergeExit -ne 0) {
Write-Warning "libFuzzer corpus premerge failed for wic target (exit=$premergeExit); continuing without premerge."
}
} else {
Write-Host "wic-corpus: no files to merge"
}
Remove-Item -LiteralPath $mergeSeed -Recurse -Force
$runLog = Join-Path $artifactDir 'run.log'
New-Item -ItemType File -Force -Path $runLog | Out-Null
& $harness @fuzzArgs $targetCorpusPosix 2>&1 | Tee-Object -FilePath $runLog
$fuzzExit = $LASTEXITCODE
$completed = Select-String -Path $runLog -Pattern 'DONE cov:|INFO: exiting: 0' -ErrorAction SilentlyContinue | Select-Object -Last 1
$crashPatterns = @('crash-*', 'timeout-*', 'oom-*', 'leak-*')
$crashInputs = @()
foreach ($pattern in $crashPatterns) {
$crashInputs += Get-ChildItem -Path $artifactDir -Filter $pattern -File -ErrorAction SilentlyContinue
}
$crashInputs = @($crashInputs | Sort-Object -Property FullName -Unique)
$crashCount = $crashInputs.Count
$hadCrash = $crashCount -gt 0
if ($hadCrash) {
Write-Warning "wic detect found $crashCount crash artifact(s). Deferring classification to non-ASan repro job."
}
if (($fuzzExit -ne 0) -and (-not $hadCrash)) {
throw "fuzz-loader-wic failed with exit code $fuzzExit and no crash artifact was produced."
}
if (($fuzzExit -eq 0) -and (-not $completed)) {
throw "libFuzzer did not report completion for wic loader target."
}
if ($fuzzExit -eq 0) {
$covLine = Select-String -Path $runLog -Pattern 'cov: [0-9]+' -ErrorAction SilentlyContinue | Select-Object -Last 1
if (-not $covLine -or $covLine.Line -notmatch 'cov: ([0-9]+)') {
throw "could not parse final coverage from run.log."
}
$cov = [int]$Matches[1]
if ($cov -lt 10) {
throw "wic loader coverage too low: cov=$cov (expected >= 10)."
}
} else {
Write-Host "wic detect run exited with code $fuzzExit; skipping coverage gate."
}
"fuzz_exit=$fuzzExit" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"had_crash=$($hadCrash.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"crash_count=$crashCount" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
# GitHub Actions' pwsh wrapper may propagate LASTEXITCODE from the
# last native command. Keep detect green when crash artifacts were
# produced so the non-ASan repro job can classify them.
$global:LASTEXITCODE = 0
- name: Upload wic detect artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: Meson-cmd-x86_64-msvc-fuzz-loader-wic-detect
if-no-files-found: ignore
retention-days: 7
# Repro only needs logs, merged dict, and any crash artifacts.
# Uploading the restored seed corpus inflates the archive and has
# triggered Windows artifact upload failures.
path: |
${{ github.workspace }}/.tmp/fuzz-artifacts-loader/wic
fuzz_push_loader_wic_repro_nosan:
if: >-
${{
needs.fuzz_push_loader_wic_detect.result == 'success'
&& (
github.event_name == 'push'
|| contains(inputs.triage_job_labels, '|Meson-cmd-x86_64-msvc-fuzz-loader-wic-detect|')
|| contains(inputs.triage_job_labels, '|Meson-cmd-x86_64-msvc-fuzz-loader-wic-repro-nosan|')
)
}}
needs: [fuzz_push_loader_wic_detect]
name: Meson-cmd-x86_64-msvc-fuzz-loader-wic-repro-nosan
runs-on: windows-latest
steps:
- name: Set Windows Defender ExclusionPath (Windows)
shell: pwsh
run: |
try {
Add-MpPreference -ExclusionPath "C:\a" -ErrorAction Stop
} catch {
Write-Warning "Windows Defender exclusion setup failed; continuing: $($_.Exception.Message)"
}
- name: Disable Windows Search Indexer (Windows)
shell: pwsh
run: |
Stop-Service WSearch
- name: Configure git EOL
shell: bash
run: git config --global core.autocrlf false
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup MSVC developer env (vcvars, x86_64 target)
shell: pwsh
run: |
& "${{ github.workspace }}\.github\scripts\setup-msvc-env.ps1" -Arch x64
- name: Download wic detect artifacts
uses: actions/download-artifact@v4
with:
name: Meson-cmd-x86_64-msvc-fuzz-loader-wic-detect
path: ${{ github.workspace }}/.tmp/wic-detect-artifacts
- name: Reproduce wic crash inputs without ASan
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$hadCrash = '${{ needs.fuzz_push_loader_wic_detect.outputs.had_crash }}'
if ($hadCrash -ne 'true') {
Write-Host 'No crash artifacts from detect job; skipping non-ASan repro.'
exit 0
}
python -m pip install --upgrade pip
python -m pip install meson ninja
if (-not (Get-Command clang -ErrorAction SilentlyContinue)) {
throw "clang was not found in PATH after MSVC environment setup."
}
$workspace = (Get-Location).Path
$downloadRoot = Join-Path $workspace '.tmp\wic-detect-artifacts'
$reproDir = Join-Path $workspace '.tmp\fuzz-artifacts-loader\wic-repro'
New-Item -ItemType Directory -Force -Path $reproDir | Out-Null
$crashPatterns = @('crash-*', 'timeout-*', 'oom-*', 'leak-*')
$crashInputs = @()
foreach ($pattern in $crashPatterns) {
$crashInputs += Get-ChildItem -Path $downloadRoot -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue
}
$crashInputs = @($crashInputs | Sort-Object -Property FullName -Unique)
if ($crashInputs.Count -eq 0) {
throw "detect job reported had_crash=true, but no crash artifacts were downloaded."
}
Write-Host "Reproducing $($crashInputs.Count) crash artifact(s) in non-ASan mode."
$env:CC = 'clang'
$env:CXX = 'clang++'
python -m mesonbuild.mesonmain setup builddir -Dfuzz=true -Dwic=enabled -Dfuzz_sanitizers=fuzzer
python -m mesonbuild.mesonmain compile -C builddir -v fuzz-loader-wic-libfuzzer
$harness = Join-Path $workspace 'builddir\fuzz\fuzz-loader-wic-libfuzzer.exe'
if (-not (Test-Path $harness)) {
$harnessNoExt = Join-Path $workspace 'builddir\fuzz\fuzz-loader-wic-libfuzzer'
if (Test-Path $harnessNoExt) {
$harness = $harnessNoExt
} else {
throw "missing wic harness: $harness"
}
}
foreach ($runtimeDir in @(
(Join-Path $workspace 'builddir\src'),
(Join-Path $workspace 'builddir\fuzz')
)) {
if (Test-Path $runtimeDir) {
$env:PATH = "$runtimeDir;$env:PATH"
}
}
$reproduced = New-Object 'System.Collections.Generic.List[string]'
foreach ($input in $crashInputs) {
$inputPosix = $input.FullName.Replace('\', '/')
$logFile = Join-Path $reproDir ("repro-" + $input.Name + ".log")
& $harness '-runs=1' '-timeout=5' $inputPosix 2>&1 | Tee-Object -FilePath $logFile
$exitCode = $LASTEXITCODE
Write-Host "repro: $($input.Name) => exit=$exitCode"
if ($exitCode -ne 0) {
[void]$reproduced.Add($input.FullName)
}
}
if ($reproduced.Count -gt 0) {
Write-Host 'Reproduced in non-ASan mode:'
foreach ($path in $reproduced) {
Write-Host " $path"
}
throw "non-ASan reproduction succeeded for $($reproduced.Count) input(s); classify as real crash."
}
Write-Warning 'No crash artifact reproduced without ASan; classify as ASan-boundary noise.'
- name: Upload wic repro artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: Meson-cmd-x86_64-msvc-fuzz-loader-wic-repro-nosan
if-no-files-found: ignore
retention-days: 7
path: |
${{ github.workspace }}/.tmp/wic-detect-artifacts
${{ github.workspace }}/.tmp/fuzz-artifacts-loader/wic-repro