Merge pull request 'Add actions' (#3) from action into main
Reviewed-on: https://git.ts.mattnite.net/mattnite/cairn/pulls/3
This commit is contained in:
commit
56c83fa562
|
|
@ -0,0 +1,242 @@
|
||||||
|
name: 'Cairn Zig Fuzz (AFL++)'
|
||||||
|
description: 'Build and run AFL++ fuzz targets, report crashes to Cairn. Each line in zig_build_args is a separate target.'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
cairn_server:
|
||||||
|
description: 'Cairn server URL'
|
||||||
|
required: true
|
||||||
|
repo:
|
||||||
|
description: 'Repository name'
|
||||||
|
required: true
|
||||||
|
owner:
|
||||||
|
description: 'Repository owner'
|
||||||
|
required: true
|
||||||
|
commit:
|
||||||
|
description: 'Commit SHA'
|
||||||
|
required: false
|
||||||
|
default: '${{ github.sha }}'
|
||||||
|
zig_build_args:
|
||||||
|
description: |
|
||||||
|
Line-delimited zig build argument sets. Each line builds and fuzzes one target.
|
||||||
|
Example:
|
||||||
|
fuzz -Dfuzz-target=lexer
|
||||||
|
fuzz -Dfuzz-target=parser
|
||||||
|
fuzz -Dfuzz-target=varint_decode
|
||||||
|
required: true
|
||||||
|
fuzz_binary:
|
||||||
|
description: 'Binary name in zig-out/bin/ (auto-detected if only one)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
corpus_dir:
|
||||||
|
description: 'Seed corpus directory (minimal seed created if empty)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
duration:
|
||||||
|
description: 'Fuzz duration per target in seconds'
|
||||||
|
required: false
|
||||||
|
default: '600'
|
||||||
|
afl_args:
|
||||||
|
description: 'Extra afl-fuzz arguments'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
target:
|
||||||
|
description: 'Target platform metadata'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
cairn_version:
|
||||||
|
description: 'Cairn CLI version'
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Check prerequisites
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
command -v zig >/dev/null 2>&1 || { echo "ERROR: zig not found in PATH. Install Zig before using this action (e.g. https://codeberg.org/mlugg/setup-zig@v2)."; exit 1; }
|
||||||
|
command -v afl-cc >/dev/null 2>&1 || { echo "ERROR: afl-cc not found in PATH. Install AFL++ before using this action (e.g. mattnite/cairn/actions/setup-afl@main)."; exit 1; }
|
||||||
|
echo "zig $(zig version), afl-cc found"
|
||||||
|
|
||||||
|
- name: Setup Cairn CLI
|
||||||
|
uses: mattnite/cairn/actions/setup-cairn@main
|
||||||
|
with:
|
||||||
|
version: ${{ inputs.cairn_version }}
|
||||||
|
server_url: ${{ inputs.cairn_server }}
|
||||||
|
|
||||||
|
- name: Build and fuzz with AFL++
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CAIRN_SERVER: ${{ inputs.cairn_server }}
|
||||||
|
REPO: ${{ inputs.repo }}
|
||||||
|
OWNER: ${{ inputs.owner }}
|
||||||
|
COMMIT: ${{ inputs.commit }}
|
||||||
|
ZIG_BUILD_ARGS: ${{ inputs.zig_build_args }}
|
||||||
|
FUZZ_BINARY: ${{ inputs.fuzz_binary }}
|
||||||
|
CORPUS_DIR: ${{ inputs.corpus_dir }}
|
||||||
|
DURATION: ${{ inputs.duration }}
|
||||||
|
AFL_ARGS: ${{ inputs.afl_args }}
|
||||||
|
TARGET: ${{ inputs.target }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# ── Start Cairn campaign ──
|
||||||
|
SHORT_SHA=$(printf '%.8s' "${COMMIT}")
|
||||||
|
CAMPAIGN_OUTPUT=$(cairn campaign start \
|
||||||
|
-server "${CAIRN_SERVER}" \
|
||||||
|
-repo "${REPO}" \
|
||||||
|
-owner "${OWNER}" \
|
||||||
|
-name "fuzz-${SHORT_SHA}" \
|
||||||
|
-type fuzzing)
|
||||||
|
CAMPAIGN_ID="${CAMPAIGN_OUTPUT#Campaign started: }"
|
||||||
|
echo "Campaign ${CAMPAIGN_ID} started"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cairn campaign finish -server "${CAIRN_SERVER}" -id "${CAMPAIGN_ID}" || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
TOTAL_CRASHES=0
|
||||||
|
TARGET_NUM=0
|
||||||
|
|
||||||
|
# ── Iterate over each line of zig_build_args ──
|
||||||
|
while IFS= read -r BUILD_ARGS; do
|
||||||
|
# Skip empty lines and comments
|
||||||
|
BUILD_ARGS=$(echo "${BUILD_ARGS}" | sed 's/#.*//' | xargs)
|
||||||
|
[ -z "${BUILD_ARGS}" ] && continue
|
||||||
|
|
||||||
|
TARGET_NUM=$((TARGET_NUM + 1))
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Target ${TARGET_NUM}: zig build ${BUILD_ARGS}"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# ── Build ──
|
||||||
|
rm -rf zig-out
|
||||||
|
zig build ${BUILD_ARGS}
|
||||||
|
|
||||||
|
# ── Locate fuzz binary ──
|
||||||
|
if [ -n "${FUZZ_BINARY}" ]; then
|
||||||
|
FUZZ_BIN="zig-out/bin/${FUZZ_BINARY}"
|
||||||
|
else
|
||||||
|
BIN_COUNT=$(find zig-out/bin -maxdepth 1 -type f -executable 2>/dev/null | wc -l)
|
||||||
|
if [ "${BIN_COUNT}" -eq 0 ]; then
|
||||||
|
echo "ERROR: No executable found in zig-out/bin/"
|
||||||
|
exit 1
|
||||||
|
elif [ "${BIN_COUNT}" -gt 1 ]; then
|
||||||
|
echo "ERROR: Multiple executables in zig-out/bin/, specify fuzz_binary input"
|
||||||
|
find zig-out/bin -maxdepth 1 -type f -executable
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
FUZZ_BIN=$(find zig-out/bin -maxdepth 1 -type f -executable)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "${FUZZ_BIN}" ]; then
|
||||||
|
echo "ERROR: Fuzz binary not found or not executable: ${FUZZ_BIN}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Fuzz binary: ${FUZZ_BIN}"
|
||||||
|
|
||||||
|
# ── Seed corpus ──
|
||||||
|
if [ -n "${CORPUS_DIR}" ] && [ -d "${CORPUS_DIR}" ]; then
|
||||||
|
SEEDS="${CORPUS_DIR}"
|
||||||
|
else
|
||||||
|
SEEDS="afl-seeds"
|
||||||
|
mkdir -p "${SEEDS}"
|
||||||
|
printf 'A' > "${SEEDS}/seed-0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Run AFL++ ──
|
||||||
|
FINDINGS="findings-${TARGET_NUM}"
|
||||||
|
rm -rf "${FINDINGS}"
|
||||||
|
mkdir -p "${FINDINGS}"
|
||||||
|
|
||||||
|
AFL_EXIT=0
|
||||||
|
AFL_NO_UI=1 \
|
||||||
|
AFL_SKIP_CPUFREQ=1 \
|
||||||
|
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \
|
||||||
|
AFL_NO_CRASH_README=1 \
|
||||||
|
AFL_EXIT_ON_TIME=60 \
|
||||||
|
timeout "${DURATION}s" \
|
||||||
|
afl-fuzz \
|
||||||
|
-i "${SEEDS}" \
|
||||||
|
-o "${FINDINGS}" \
|
||||||
|
${AFL_ARGS} \
|
||||||
|
-- "${FUZZ_BIN}" \
|
||||||
|
|| AFL_EXIT=$?
|
||||||
|
|
||||||
|
if [ "${AFL_EXIT}" -eq 124 ]; then
|
||||||
|
echo "AFL++ ran for full duration (${DURATION}s)"
|
||||||
|
elif [ "${AFL_EXIT}" -eq 0 ]; then
|
||||||
|
echo "AFL++ exited normally (coverage stagnated)"
|
||||||
|
else
|
||||||
|
echo "AFL++ exited with code ${AFL_EXIT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Upload crashes ──
|
||||||
|
CRASH_DIR="${FINDINGS}/default/crashes"
|
||||||
|
if [ -d "${CRASH_DIR}" ]; then
|
||||||
|
for crash_file in "${CRASH_DIR}"/id:*; do
|
||||||
|
[ -f "${crash_file}" ] || continue
|
||||||
|
|
||||||
|
CRASH_NAME=$(basename "${crash_file}")
|
||||||
|
SIG=""
|
||||||
|
case "${CRASH_NAME}" in
|
||||||
|
*,sig:*)
|
||||||
|
SIG=$(echo "${CRASH_NAME}" | sed 's/.*,sig:\([0-9]*\).*/\1/')
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Uploading crash: ${CRASH_NAME}"
|
||||||
|
set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \
|
||||||
|
-commit "${COMMIT}" -type fuzz -file "${crash_file}" \
|
||||||
|
-crash-message "AFL++ crash (${BUILD_ARGS}): ${CRASH_NAME}"
|
||||||
|
|
||||||
|
if [ -n "${TARGET}" ]; then
|
||||||
|
set -- "$@" -target "${TARGET}"
|
||||||
|
fi
|
||||||
|
if [ -n "${SIG}" ]; then
|
||||||
|
set -- "$@" -signal "${SIG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cairn upload "$@"
|
||||||
|
TOTAL_CRASHES=$((TOTAL_CRASHES + 1))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Upload corpus ──
|
||||||
|
QUEUE_DIR="${FINDINGS}/default/queue"
|
||||||
|
if [ -d "${QUEUE_DIR}" ]; then
|
||||||
|
QUEUE_COUNT=$(find "${QUEUE_DIR}" -maxdepth 1 -type f -name 'id:*' | wc -l)
|
||||||
|
if [ "${QUEUE_COUNT}" -gt 0 ]; then
|
||||||
|
echo "Uploading corpus (${QUEUE_COUNT} entries)..."
|
||||||
|
tar czf "corpus-${TARGET_NUM}.tar.gz" -C "${QUEUE_DIR}" .
|
||||||
|
|
||||||
|
set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \
|
||||||
|
-commit "${COMMIT}" -type fuzz -file "corpus-${TARGET_NUM}.tar.gz"
|
||||||
|
if [ -n "${TARGET}" ]; then
|
||||||
|
set -- "$@" -target "${TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cairn upload "$@"
|
||||||
|
rm -f "corpus-${TARGET_NUM}.tar.gz"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
done <<EOF
|
||||||
|
${ZIG_BUILD_ARGS}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Final report ──
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Fuzzed ${TARGET_NUM} target(s), ${TOTAL_CRASHES} total crash(es)"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
if [ "${TOTAL_CRASHES}" -gt 0 ]; then
|
||||||
|
echo "FAIL: ${TOTAL_CRASHES} crash(es) found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: No crashes found"
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
name: 'Setup AFL++ with LLVM 20'
|
||||||
|
description: 'Installs LLVM 20 from apt.llvm.org and builds AFL++ from source, with caching'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Restore AFL++ cache
|
||||||
|
id: afl-cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /opt/afl
|
||||||
|
key: afl-llvm20-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
|
||||||
|
- name: Restore LLVM 20 deb cache
|
||||||
|
id: llvm-cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /opt/llvm-20-debs
|
||||||
|
key: llvm20-debs-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
|
||||||
|
- name: Install LLVM 20
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ -x /usr/bin/clang-20 ] && [ -x /usr/bin/llvm-config-20 ]; then
|
||||||
|
echo "LLVM 20 already installed"
|
||||||
|
elif [ -d /opt/llvm-20-debs ] && [ "$(ls /opt/llvm-20-debs/*.deb 2>/dev/null | wc -l)" -gt 0 ]; then
|
||||||
|
echo "Installing LLVM 20 from cached debs..."
|
||||||
|
dpkg -i /opt/llvm-20-debs/*.deb 2>/dev/null || apt-get install -f -y -qq >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
echo "Installing LLVM 20 from apt.llvm.org..."
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq wget gnupg lsb-release >/dev/null 2>&1
|
||||||
|
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
|
||||||
|
| tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc >/dev/null
|
||||||
|
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-20 main" \
|
||||||
|
> /etc/apt/sources.list.d/llvm-20.list
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq clang-20 llvm-20-dev zlib1g-dev make git >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Cache the downloaded debs for next run
|
||||||
|
mkdir -p /opt/llvm-20-debs
|
||||||
|
cp /var/cache/apt/archives/*llvm*20* /var/cache/apt/archives/*clang*20* \
|
||||||
|
/var/cache/apt/archives/*lib*20* /opt/llvm-20-debs/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure unversioned symlinks exist
|
||||||
|
ln -sf /usr/bin/llvm-config-20 /usr/bin/llvm-config
|
||||||
|
ln -sf /usr/bin/clang-20 /usr/bin/clang
|
||||||
|
ln -sf /usr/bin/clang++-20 /usr/bin/clang++
|
||||||
|
echo "LLVM $(llvm-config --version) ready"
|
||||||
|
|
||||||
|
- name: Install AFL++
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "${{ steps.afl-cache.outputs.cache-hit }}" = "true" ]; then
|
||||||
|
echo "Restoring AFL++ from cache..."
|
||||||
|
cp -a /opt/afl/bin/* /usr/local/bin/
|
||||||
|
cp -a /opt/afl/lib/* /usr/local/lib/ 2>/dev/null || true
|
||||||
|
cp -a /opt/afl/share/* /usr/local/share/ 2>/dev/null || true
|
||||||
|
echo "AFL++ restored from cache"
|
||||||
|
else
|
||||||
|
echo "Building AFL++ from source..."
|
||||||
|
apt-get install -y -qq make git zlib1g-dev >/dev/null 2>&1
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
git clone --depth 1 https://github.com/AFLplusplus/AFLplusplus.git
|
||||||
|
cd AFLplusplus
|
||||||
|
LLVM_CONFIG=llvm-config-20 CC=clang-20 CXX=clang++-20 \
|
||||||
|
make -j"$(nproc)" source-only >/dev/null 2>&1
|
||||||
|
make install PREFIX=/usr/local >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Cache the install for next run
|
||||||
|
mkdir -p /opt/afl
|
||||||
|
make install PREFIX=/opt/afl >/dev/null 2>&1
|
||||||
|
|
||||||
|
rm -rf /tmp/AFLplusplus
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "AFL++ $(afl-fuzz --version 2>&1 | head -1) ready"
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
name: 'Setup Cairn CLI'
|
||||||
|
description: 'Downloads and caches the Cairn CLI tool'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Cairn CLI version'
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
server_url:
|
||||||
|
description: 'Cairn server URL (exported as CAIRN_SERVER_URL)'
|
||||||
|
required: false
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup Cairn CLI
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
CAIRN_VERSION: ${{ inputs.version }}
|
||||||
|
CAIRN_SERVER_URL: ${{ inputs.server_url }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CACHE_DIR="${RUNNER_TOOL_CACHE:-/tmp/cairn-tool-cache}"
|
||||||
|
TOOL_DIR="${CACHE_DIR}/cairn/${CAIRN_VERSION}/x64"
|
||||||
|
|
||||||
|
if [ -f "${TOOL_DIR}/cairn" ] && [ -f "${TOOL_DIR}/.complete" ]; then
|
||||||
|
echo "Cairn CLI ${CAIRN_VERSION} found in tool cache"
|
||||||
|
else
|
||||||
|
echo "Downloading Cairn CLI ${CAIRN_VERSION}..."
|
||||||
|
mkdir -p "${TOOL_DIR}"
|
||||||
|
curl -sfL \
|
||||||
|
"${{ github.server_url }}/api/packages/${{ github.repository_owner }}/generic/cairn/${CAIRN_VERSION}/cairn-x86_64-linux" \
|
||||||
|
-o "${TOOL_DIR}/cairn"
|
||||||
|
chmod +x "${TOOL_DIR}/cairn"
|
||||||
|
touch "${TOOL_DIR}/.complete"
|
||||||
|
echo "Cairn CLI ${CAIRN_VERSION} installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${TOOL_DIR}" >> "${GITHUB_PATH}"
|
||||||
|
|
||||||
|
if [ -n "${CAIRN_SERVER_URL}" ]; then
|
||||||
|
echo "CAIRN_SERVER_URL=${CAIRN_SERVER_URL}" >> "${GITHUB_ENV}"
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue