mirror of https://github.com/wolfSSL/wolfTPM.git
138 lines
5.5 KiB
YAML
138 lines
5.5 KiB
YAML
name: Fuzz Testing
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 4 * * *' # Nightly 4am UTC
|
|
workflow_dispatch: # Manual trigger
|
|
pull_request:
|
|
branches: [ '*' ]
|
|
types: [opened, reopened, ready_for_review]
|
|
push:
|
|
branches: [ master ]
|
|
|
|
jobs:
|
|
# The matrix context is not available in a job-level if:, so route by run
|
|
# type here instead: 600s full runs go to the nightly schedule / manual
|
|
# dispatch; 60s smoke runs go to non-draft PR open/reopen/ready and merges
|
|
# to master. Intermediate PR pushes (synchronize) are not a trigger.
|
|
select:
|
|
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.pick.outputs.matrix }}
|
|
steps:
|
|
- name: Pick fuzz matrix for ${{ github.event_name }}
|
|
id: pick
|
|
run: |
|
|
# schedule / dispatch -> full (600s); PR / push -> smoke (60s).
|
|
case "${{ github.event_name }}" in
|
|
schedule|workflow_dispatch) WANT=false ;;
|
|
*) WANT=true ;;
|
|
esac
|
|
PQC="--enable-dilithium --enable-mlkem --enable-experimental --enable-harden"
|
|
MATRIX=$(jq -nc --argjson want "$WANT" --arg pqc "$PQC" '{
|
|
include: [
|
|
{name:"fuzz-full", fuzz_time:600, smoke_only:false, wolfssl_extra_flags:"", wolftpm_extra_flags:"", max_len:4096},
|
|
{name:"fuzz-smoke", fuzz_time:60, smoke_only:true, wolfssl_extra_flags:"", wolftpm_extra_flags:"", max_len:4096},
|
|
{name:"fuzz-full-pqc", fuzz_time:600, smoke_only:false, wolfssl_extra_flags:$pqc, wolftpm_extra_flags:"--enable-v185", max_len:8192},
|
|
{name:"fuzz-smoke-pqc", fuzz_time:60, smoke_only:true, wolfssl_extra_flags:$pqc, wolftpm_extra_flags:"--enable-v185", max_len:8192}
|
|
] | map(select(.smoke_only == $want))
|
|
}')
|
|
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
|
|
|
fuzz:
|
|
needs: select
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.select.outputs.matrix) }}
|
|
|
|
steps:
|
|
- name: Checkout wolfTPM
|
|
uses: actions/checkout@v4
|
|
|
|
- name: ASLR workaround
|
|
run: sudo sysctl vm.mmap_rnd_bits=28
|
|
|
|
- name: Setup wolfSSL with fuzzer support
|
|
uses: ./.github/actions/setup-wolfssl
|
|
with:
|
|
cc: clang
|
|
configure-flags: --enable-wolftpm --enable-pkcallbacks --enable-keygen ${{ matrix.wolfssl_extra_flags }}
|
|
cflags: -fsanitize=fuzzer-no-link,address -fno-omit-frame-pointer -g -O1 -DWC_RSA_NO_PADDING
|
|
ldflags: -fsanitize=address
|
|
|
|
- name: Build fuzz target
|
|
run: |
|
|
./autogen.sh
|
|
CC=clang ./configure --enable-fwtpm --enable-fuzz \
|
|
${{ matrix.wolftpm_extra_flags }} \
|
|
CFLAGS="-fsanitize=fuzzer-no-link,address -fno-omit-frame-pointer -g -O1" \
|
|
LDFLAGS="-fsanitize=address"
|
|
make -j$(nproc)
|
|
|
|
- name: Generate seed corpus
|
|
run: python3 tests/fuzz/gen_corpus.py
|
|
|
|
- name: Verify v1.85 PQC opcode coverage in corpus
|
|
if: contains(matrix.name, 'pqc')
|
|
run: |
|
|
# Without this guard, fuzz-*-pqc could spend 10 minutes fuzzing
|
|
# classical paths with -DWOLFTPM_V185 set and never exercise an
|
|
# Encapsulate or SignSequenceComplete opcode. Fail fast if any of
|
|
# the 8 new v1.85 command codes is absent from the seed corpus.
|
|
# Seeds are binary; opcodes appear as raw 4-byte big-endian
|
|
# sequences. Hex-dump the concatenated corpus and grep the
|
|
# resulting hex stream for each opcode's bytes.
|
|
CORPUS_HEX=$(cat tests/fuzz/corpus/*.bin | xxd -p | tr -d '\n')
|
|
MISSING=()
|
|
for cc in 000001a3 000001a4 000001a5 000001a6 \
|
|
000001a7 000001a8 000001a9 000001aa; do
|
|
if ! echo "$CORPUS_HEX" | grep -q "$cc"; then
|
|
MISSING+=("0x${cc^^}")
|
|
fi
|
|
done
|
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
echo "ERROR: PQC seed corpus missing the following command codes:"
|
|
printf ' %s\n' "${MISSING[@]}"
|
|
echo "Update tests/fuzz/gen_corpus.py to emit a seed for each."
|
|
exit 1
|
|
fi
|
|
echo "All 8 v1.85 PQC opcodes (0x1A3-0x1AA) present in seed corpus."
|
|
|
|
- name: Run fuzzer
|
|
env:
|
|
ASAN_OPTIONS: "detect_leaks=1:abort_on_error=1:symbolize=1"
|
|
run: |
|
|
echo "Fuzzing for ${{ matrix.fuzz_time }} seconds..."
|
|
timeout ${{ matrix.fuzz_time }} \
|
|
./tests/fuzz/fwtpm_fuzz \
|
|
tests/fuzz/corpus/ \
|
|
-dict=tests/fuzz/tpm2.dict \
|
|
-max_len=${{ matrix.max_len }} \
|
|
-timeout=30 \
|
|
-rss_limit_mb=2048 \
|
|
-print_final_stats=1 \
|
|
|| FUZZ_RC=$?
|
|
# timeout returns 124 on normal expiry, fuzzer returns 0 on no crash
|
|
if [ "${FUZZ_RC:-0}" -eq 124 ] || [ "${FUZZ_RC:-0}" -eq 0 ]; then
|
|
echo "Fuzzer completed without crashes"
|
|
else
|
|
echo "Fuzzer found crashes (exit code $FUZZ_RC)"
|
|
ls -la crash-* 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
- name: Upload crash artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: fuzz-crashes-${{ matrix.name }}
|
|
path: |
|
|
crash-*
|
|
oom-*
|
|
timeout-*
|
|
retention-days: 30
|
|
if-no-files-found: ignore
|