Skip to content

Commit 094c698

Browse files
Add files via upload
1 parent 2af713b commit 094c698

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
name: Fuzzing Smoke Test
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
run_minutes:
7+
description: "How many minutes AFL++ should fuzz"
8+
required: false
9+
default: "10"
10+
fail_on_hangs:
11+
description: "Fail workflow when AFL++ reports hangs"
12+
required: false
13+
default: "false"
14+
type: choice
15+
options:
16+
- "false"
17+
- "true"
18+
schedule:
19+
- cron: "0 2 * * 0"
20+
21+
permissions:
22+
contents: read
23+
24+
concurrency:
25+
group: fuzzing-smoke-${{ github.ref }}
26+
cancel-in-progress: false
27+
28+
jobs:
29+
fuzzing-smoke:
30+
name: AFL++ fuzzing smoke test
31+
runs-on: ubuntu-24.04
32+
timeout-minutes: 60
33+
34+
env:
35+
AFL_SKIP_CPUFREQ: "1"
36+
AFL_NO_AFFINITY: "1"
37+
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: "1"
38+
AFL_NO_UI: "1"
39+
AFL_FAST_CAL: "1"
40+
41+
steps:
42+
- name: Checkout repository
43+
uses: actions/checkout@v6
44+
with:
45+
fetch-depth: 0
46+
submodules: recursive
47+
48+
- name: Detect latest Lua packages
49+
id: detect_lua
50+
shell: bash
51+
run: |
52+
set -euo pipefail
53+
54+
sudo apt-get update -y -qq
55+
56+
CANDIDATES="$(apt-cache pkgnames | grep -E '^liblua[0-9]+\.[0-9]+-dev$' || true)"
57+
58+
if [ -z "$CANDIDATES" ]; then
59+
echo "No libluaX.Y-dev package found"
60+
exit 1
61+
fi
62+
63+
BEST_PKG="$(
64+
printf '%s\n' "$CANDIDATES" \
65+
| sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1 &/' \
66+
| sort -V \
67+
| tail -n1 \
68+
| awk '{print $2}'
69+
)"
70+
71+
if [ -z "$BEST_PKG" ]; then
72+
echo "Failed to determine Lua dev package"
73+
printf '%s\n' "$CANDIDATES"
74+
exit 1
75+
fi
76+
77+
BEST_VER="$(printf '%s\n' "$BEST_PKG" | sed -E 's/^liblua([0-9]+\.[0-9]+)-dev$/\1/')"
78+
LUA_PKG="lua$BEST_VER"
79+
80+
echo "lua_dev_pkg=$BEST_PKG" >> "$GITHUB_OUTPUT"
81+
echo "lua_pkg=$LUA_PKG" >> "$GITHUB_OUTPUT"
82+
83+
echo "Using Lua dev package: $BEST_PKG"
84+
echo "Using Lua interpreter: $LUA_PKG"
85+
86+
- name: Install dependencies
87+
shell: bash
88+
run: |
89+
set -euo pipefail
90+
91+
sudo apt-get install -y \
92+
autoconf \
93+
automake \
94+
build-essential \
95+
afl++ \
96+
clang \
97+
libtool \
98+
pkg-config \
99+
libyajl-dev \
100+
libcurl4-openssl-dev \
101+
liblmdb-dev \
102+
${{ steps.detect_lua.outputs.lua_dev_pkg }} \
103+
${{ steps.detect_lua.outputs.lua_pkg }} \
104+
libmaxminddb-dev \
105+
libpcre2-dev \
106+
libxml2-dev \
107+
libfuzzy-dev \
108+
pcre2-utils \
109+
libpcre3-dev \
110+
bison \
111+
flex \
112+
python3 \
113+
python3-venv
114+
115+
- name: Show Lua installation
116+
shell: bash
117+
run: |
118+
which lua || true
119+
lua -v || true
120+
dpkg -l | grep lua || true
121+
122+
- name: Build ModSecurity with AFL++ instrumentation
123+
shell: bash
124+
env:
125+
CC: afl-clang-fast
126+
CXX: afl-clang-fast++
127+
run: |
128+
set -euo pipefail
129+
./build.sh
130+
./configure \
131+
--enable-afl-fuzz \
132+
--enable-parser-generation \
133+
--enable-assertions=yes
134+
make -j"$(nproc)"
135+
136+
- name: Locate AFL fuzzer target
137+
id: target
138+
shell: bash
139+
run: |
140+
set -euo pipefail
141+
142+
CANDIDATES=(
143+
"./test/fuzzer/afl_fuzzer"
144+
"./test/fuzzer/.libs/afl_fuzzer"
145+
)
146+
147+
TARGET=""
148+
for candidate in "${CANDIDATES[@]}"; do
149+
if [ -x "$candidate" ]; then
150+
TARGET="$candidate"
151+
break
152+
fi
153+
done
154+
155+
if [ -z "$TARGET" ]; then
156+
echo "Could not find test/fuzzer/afl_fuzzer"
157+
find . -path "*afl_fuzzer*" -maxdepth 6 -print || true
158+
exit 1
159+
fi
160+
161+
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
162+
echo "Using AFL target: $TARGET"
163+
164+
- name: Create seed corpus
165+
shell: bash
166+
run: |
167+
set -euo pipefail
168+
rm -rf fuzz-in fuzz-out
169+
mkdir -p fuzz-in fuzz-out
170+
171+
# Keep seeds small because the existing ModSecurity AFL harness reads 128 bytes.
172+
printf '' > fuzz-in/empty
173+
printf 'abc' > fuzz-in/plain
174+
printf '../../etc/passwd' > fuzz-in/path-traversal
175+
printf '%s' '<script>alert(1)</script>' > fuzz-in/xss
176+
printf "%s" "' OR '1'='1" > fuzz-in/sqli
177+
printf '%s' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' > fuzz-in/max-128-a
178+
printf '%s' '%u0101%u4141%2f%2e%2e%2f' > fuzz-in/url-encoded
179+
printf '%s' '\x00\xff\xfe\xfd{{{{[[[[' > fuzz-in/binary-ish
180+
printf '%s' 'SecRule ARGS "@rx ^(a+)+$" "id:1,phase:2,deny"' > fuzz-in/rule-ish
181+
printf '%s' '../../../../../../../../../../tmp/a' > fuzz-in/deep-path
182+
printf '%s' '() { :;}; echo vulnerable' > fuzz-in/shellshock-ish
183+
184+
ls -la fuzz-in
185+
186+
- name: Dry-run fuzzer target
187+
shell: bash
188+
run: |
189+
set -euo pipefail
190+
timeout 10s "${{ steps.target.outputs.target }}" < fuzz-in/plain
191+
192+
- name: Run AFL++ smoke fuzzing
193+
shell: bash
194+
run: |
195+
set -euo pipefail
196+
197+
RUN_MINUTES="${{ github.event.inputs.run_minutes }}"
198+
if [ -z "$RUN_MINUTES" ]; then
199+
RUN_MINUTES="10"
200+
fi
201+
202+
if ! [[ "$RUN_MINUTES" =~ ^[0-9]+$ ]]; then
203+
echo "run_minutes must be a positive integer"
204+
exit 1
205+
fi
206+
207+
if [ "$RUN_MINUTES" -lt 1 ]; then
208+
echo "run_minutes must be >= 1"
209+
exit 1
210+
fi
211+
212+
echo "Running AFL++ for ${RUN_MINUTES} minute(s)"
213+
214+
# timeout returns 124 when it stops AFL after the requested smoke window.
215+
# That is expected and should not fail the workflow.
216+
set +e
217+
timeout "${RUN_MINUTES}m" \
218+
afl-fuzz \
219+
-i fuzz-in \
220+
-o fuzz-out \
221+
-m none \
222+
-t 1000+ \
223+
-- "${{ steps.target.outputs.target }}"
224+
AFL_EXIT=$?
225+
set -e
226+
227+
if [ "$AFL_EXIT" -ne 0 ] && [ "$AFL_EXIT" -ne 124 ]; then
228+
echo "afl-fuzz exited with unexpected code: $AFL_EXIT"
229+
exit "$AFL_EXIT"
230+
fi
231+
232+
- name: Summarize AFL++ results
233+
id: summary
234+
shell: bash
235+
run: |
236+
set -euo pipefail
237+
238+
echo "AFL++ output tree:"
239+
find fuzz-out -maxdepth 4 -type f | sort || true
240+
241+
CRASH_COUNT="$(find fuzz-out -path '*/crashes/id:*' -type f | wc -l | tr -d ' ')"
242+
HANG_COUNT="$(find fuzz-out -path '*/hangs/id:*' -type f | wc -l | tr -d ' ')"
243+
244+
echo "crash_count=$CRASH_COUNT" >> "$GITHUB_OUTPUT"
245+
echo "hang_count=$HANG_COUNT" >> "$GITHUB_OUTPUT"
246+
247+
{
248+
echo "## AFL++ fuzzing smoke result"
249+
echo
250+
echo "- Crashes: \`$CRASH_COUNT\`"
251+
echo "- Hangs: \`$HANG_COUNT\`"
252+
echo "- Target: \`${{ steps.target.outputs.target }}\`"
253+
echo "- Lua dev package: \`${{ steps.detect_lua.outputs.lua_dev_pkg }}\`"
254+
echo "- Lua interpreter: \`${{ steps.detect_lua.outputs.lua_pkg }}\`"
255+
} >> "$GITHUB_STEP_SUMMARY"
256+
257+
- name: Upload AFL++ corpus and findings
258+
if: always()
259+
uses: actions/upload-artifact@v4
260+
with:
261+
name: afl-fuzz-results-${{ github.run_id }}
262+
path: |
263+
fuzz-in
264+
fuzz-out
265+
if-no-files-found: warn
266+
retention-days: 14
267+
268+
- name: Fail on AFL++ crashes
269+
if: steps.summary.outputs.crash_count != '0'
270+
shell: bash
271+
run: |
272+
echo "AFL++ found ${{ steps.summary.outputs.crash_count }} crash(es). Download the afl-fuzz-results artifact."
273+
exit 1
274+
275+
- name: Fail on AFL++ hangs when enabled
276+
if: github.event.inputs.fail_on_hangs == 'true' && steps.summary.outputs.hang_count != '0'
277+
shell: bash
278+
run: |
279+
echo "AFL++ found ${{ steps.summary.outputs.hang_count }} hang(s). Download the afl-fuzz-results artifact."
280+
exit 1

0 commit comments

Comments
 (0)