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
147 changes: 147 additions & 0 deletions .github/workflows/daily_fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
name: Fuzzing

'on':
pull_request:
schedule:
- cron: '41 3 * * *'
workflow_dispatch:

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
pr_smoke:
name: fuzz smoke
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout repository
# actions/checkout v5
uses: >-
actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
persist-credentials: false

- name: Install fuzzing build dependencies
run: |
sudo apt-get update
sudo apt-get install --yes autoconf automake clang g++ libtool

- name: Build and run fuzzers
env:
FUZZ_SECONDS: 30
run: |
autoreconf -fvi
./configure
make fuzz-smoke

daily:
name: daily parser and serializer fuzzing
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
issues: write # Create or update the upstream issue after fuzzing fails.
steps:
- name: Check whether this commit was already fuzzed
id: daily_fuzz_marker
# actions/cache v6.1.0
uses: >-
actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: /tmp/libfastjson-daily-fuzz-complete
key: daily-fuzz-v1-${{ github.sha }}
lookup-only: true

- name: Report unchanged commit
if: steps.daily_fuzz_marker.outputs.cache-hit == 'true'
run: |
echo '::notice title=Fuzzing skipped::Exact commit already fuzzed.'

- name: Checkout repository
if: steps.daily_fuzz_marker.outputs.cache-hit != 'true'
# actions/checkout v5
uses: >-
actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
persist-credentials: false

- name: Install fuzzing build dependencies
if: steps.daily_fuzz_marker.outputs.cache-hit != 'true'
run: |
sudo apt-get update
sudo apt-get install --yes autoconf automake clang g++ libtool

- name: Build and run fuzzers
if: steps.daily_fuzz_marker.outputs.cache-hit != 'true'
env:
FUZZ_SECONDS: 300
run: |
autoreconf -fvi
./configure
make fuzz-smoke

- name: Create or update daily fuzzing failure issue
if: >-
failure() && github.event_name == 'schedule' &&
github.repository == 'rsyslog/libfastjson'
# actions/github-script v9.0.0
uses: >-
actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const title = 'Daily fuzzing failures';
const runUrl = `${context.serverUrl}/${owner}/${repo}` +
`/actions/runs/${context.runId}`;
const body = [
'The daily parser/serializer fuzzing campaign failed.',
'',
`Commit: ${context.sha}`,
`Run: ${runUrl}`,
'',
'The failed commit has no completion marker, so the next',
'scheduled campaign will retry it if no newer commit arrives.'
].join('\n');
const query = `repo:${owner}/${repo} ` +
`is:issue is:open in:title ${title}`;
const { data: issues } =
await github.rest.search.issuesAndPullRequests({
q: query,
per_page: 1,
});
if (issues.items.length > 0) {
await github.rest.issues.update({
owner,
repo,
issue_number: issues.items[0].number,
body,
});
} else {
await github.rest.issues.create({owner, repo, title, body});
}

- name: Mark successful fuzzing campaign
if: steps.daily_fuzz_marker.outputs.cache-hit != 'true'
run: |
marker=/tmp/libfastjson-daily-fuzz-complete
mkdir -p "$marker"
printf '%s\n' "$GITHUB_SHA" > "$marker/commit"

- name: Cache successful fuzzing campaign
if: steps.daily_fuzz_marker.outputs.cache-hit != 'true'
# actions/cache v6.1.0
uses: >-
actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: /tmp/libfastjson-daily-fuzz-complete
key: daily-fuzz-v1-${{ github.sha }}
29 changes: 29 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ EXTRA_DIST += \
benchmarks/runner.py \
benchmarks/selftest.py

FUZZ_CC ?= clang
FUZZ_CFLAGS = -g -O1 -fno-omit-frame-pointer \
-fsanitize=fuzzer,address,undefined
FUZZ_CXX_LIBS ?= -lstdc++
FUZZ_SECONDS ?= 30
FUZZ_RSS_LIMIT_MB ?= 2048

.PHONY: fuzz fuzz-smoke

fuzz: all
$(FUZZ_CC) $(FUZZ_CFLAGS) -nostdlib++ -I$(srcdir) \
-o tests/fuzz_parser tests/test_fuzz_parser.c \
$(top_builddir)/.libs/libfastjson.a \
$(top_builddir)/.libs/libfastjson-internal.a -lm $(FUZZ_CXX_LIBS)
$(FUZZ_CC) $(FUZZ_CFLAGS) -nostdlib++ -I$(srcdir) \
-o tests/fuzz_serializer tests/test_fuzz_serializer.c \
$(top_builddir)/.libs/libfastjson.a \
$(top_builddir)/.libs/libfastjson-internal.a -lm $(FUZZ_CXX_LIBS)

fuzz-smoke: fuzz
@set -e; fuzz_tmp=$$(mktemp -d); \
trap 'rm -rf "$$fuzz_tmp"' EXIT; \
mkdir "$$fuzz_tmp/parser" "$$fuzz_tmp/serializer"; \
tests/fuzz_parser -dict=tests/fuzz/json.dict -max_len=65536 \
-max_total_time=$(FUZZ_SECONDS) -rss_limit_mb=$(FUZZ_RSS_LIMIT_MB) \
"$$fuzz_tmp/parser" tests/fuzz/corpus/parser; \
tests/fuzz_serializer -max_len=65536 -max_total_time=$(FUZZ_SECONDS) \
-rss_limit_mb=$(FUZZ_RSS_LIMIT_MB) "$$fuzz_tmp/serializer" tests/fuzz/corpus/serializer

SUBDIRS = . tests

lib_LTLIBRARIES = libfastjson.la
Expand Down
15 changes: 15 additions & 0 deletions tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ TESTS_DEFAULT+= test_float.test
TESTS_DEFAULT+= test_dump_buffered.test
TESTS_DEFAULT+= test_json_util.test
TESTS_DEFAULT+= test_array_api.test
TESTS_DEFAULT+= test_fuzz_parser.test
TESTS_DEFAULT+= test_fuzz_serializer.test
TESTS_DEFAULT+= test1.test
TESTS_DEFAULT+= test2.test
TESTS_DEFAULT+= test4.test
Expand Down Expand Up @@ -42,6 +44,8 @@ cr_obj_multi_SOURCES = cr_obj_multi.c
chk_version_SOURCES = chk_version.c

test_printbuf_SOURCES = test_printbuf.c
test_fuzz_parser_CPPFLAGS = -DFUZZ_REPLAY
test_fuzz_serializer_CPPFLAGS = -DFUZZ_REPLAY

# Note: handled by test1.test
check_PROGRAMS += test1Formatted
Expand Down Expand Up @@ -91,6 +95,17 @@ EXTRA_DIST += test_many_subobj.expected
EXTRA_DIST += test_obj_obj_get_ex-null.expected
EXTRA_DIST += test_json_util.expected
EXTRA_DIST += test_array_api.expected
EXTRA_DIST += fuzz/README.md
EXTRA_DIST += fuzz/json.dict
EXTRA_DIST += fuzz/corpus/parser/array.json
EXTRA_DIST += fuzz/corpus/parser/deep.json
EXTRA_DIST += fuzz/corpus/parser/escaped.json
EXTRA_DIST += fuzz/corpus/parser/invalid.json
EXTRA_DIST += fuzz/corpus/parser/number.json
EXTRA_DIST += fuzz/corpus/parser/object.json
EXTRA_DIST += fuzz/corpus/parser/truncated.json
EXTRA_DIST += fuzz/corpus/serializer/nesting.seed
EXTRA_DIST += fuzz/corpus/serializer/strings.seed

testsubdir=testSubDir
TESTS_ENVIRONMENT = top_builddir=$(top_builddir)
21 changes: 21 additions & 0 deletions tests/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# libfastjson fuzzing

The checked-in corpus is replayed by `make check` through deterministic
standalone executables. Each input must remain safe under normal and sanitizer
builds.

Build libFuzzer targets with Clang after configuring and building the library:

```sh
make fuzz
make fuzz-smoke FUZZ_SECONDS=60
```

The targets need Clang and its libFuzzer runtime plus a C++ standard library.
Set `FUZZ_CXX_LIBS` if the linker cannot find its default `-lstdc++`.
`FUZZ_RSS_LIMIT_MB` sets the libFuzzer memory limit (default: 2048 MiB).

`fuzz-smoke` runs both targets with AddressSanitizer and
UndefinedBehaviorSanitizer. A minimized input that reproduces a crash must be
added to the relevant `corpus/` directory. Do not commit automatically
generated corpus growth without reviewing and minimizing it.
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/array.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[0,true,false,null,"text"]
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/deep.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[[[[[[0]]]]]]]]
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/escaped.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"quote":"a\\\"b","slash":"\\\\","unicode":"\\u20ac"}
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"key":,}
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/number.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-1234567890123456789.25e-10
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"a":1,"a":2,"nested":{"b":[3,4]}}
1 change: 1 addition & 0 deletions tests/fuzz/corpus/parser/truncated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"unfinished":"escape\\
1 change: 1 addition & 0 deletions tests/fuzz/corpus/serializer/nesting.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0123456789abcdef0123456789abcdef
1 change: 1 addition & 0 deletions tests/fuzz/corpus/serializer/strings.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
\\\"/\\\"/\\\"/\\\"/\\\"/
12 changes: 12 additions & 0 deletions tests/fuzz/json.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"null"
"true"
"false"
"{"
"}"
"["
"]"
":"
","
"\\\""
"\\\\"
"\\u0000"
Loading
Loading