Skip to content

Commit 3ecb4ec

Browse files
sqllqqVifleY
authored andcommitted
test/fuzz: fuzzing for httpfast and tpleval
1 parent 9b73cab commit 3ecb4ec

90 files changed

Lines changed: 549 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ CTestTestfile.cmake
2525
.idea/
2626
build/
2727
packpack/
28+
./corpus
29+
.vscode
30+
*.dSYM
31+
*.profdata
32+
*.profraw
33+
*.log

test/fuzz/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## Fuzzing
2+
3+
Clone to home dir
4+
```bash
5+
git clone https://github.com/llvm-mirror/compiler-rt.git
6+
```
7+
8+
Build fuzzers
9+
```bash
10+
clang -fsanitize=fuzzer,fuzzer-no-link -g -I../../http tplevah_fuzzer.c -o tplevah_fuzzer
11+
clang -fsanitize=fuzzer,fuzzer-no-link -g -I../../http httpfast_fuzzer.c -o httpfast_fuzzer
12+
```
13+
14+
Run fuzzers
15+
```bash
16+
python3 test/fuzz/fuzz_until.py tpleval --libfuzzer-log tpleval.log --corpus-dir ./corpus/tpleval &
17+
python3 test/fuzz/fuzz_until.py httpfast --libfuzzer-log httpfast.log --corpus-dir ./corpus/httpfast &
18+
```
19+
20+
Build for reports
21+
```bash
22+
clang -fprofile-instr-generate -fcoverage-mapping -o httpfast_fuzzer -g -I../../http httpfast_fuzzer.c ~/compiler-rt/lib/fuzzer/standalone/StandaloneFuzzTargetMain.c
23+
clang -fprofile-instr-generate -fcoverage-mapping -o tplevah_fuzzer -g -I../../http tplevah_fuzzer.c ~/compiler-rt/lib/fuzzer/standalone/StandaloneFuzzTargetMain.c
24+
```
25+
26+
Generate reports
27+
```bash
28+
llvm-profdata merge -sparse httpfast.profraw -o httpfast.profdata
29+
llvm-profdata merge -sparse tpleval.profraw -o tpleval.profdata
30+
31+
llvm-cov show ./test/fuzz/tpleval_fuzzer -instr-profile=tpleval.profdata -format=html -output-dir=coverage_tpleval
32+
llvm-cov show ./test/fuzz/httpfast_fuzzer -instr-profile=httpfast.profdata -format=html -output-dir=coverage_tpleval
33+
```

test/fuzz/fuzz_until.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import time
2+
import subprocess
3+
from argparse import ArgumentParser
4+
from enum import Enum, auto
5+
from dataclasses import dataclass
6+
from pathlib import Path
7+
8+
9+
class MsgType(Enum):
10+
"""Type of libfuzzer log message"""
11+
12+
READ = auto()
13+
INITED = auto()
14+
NEW = auto()
15+
REDUCE = auto()
16+
pulse = auto()
17+
DONE = auto()
18+
RELOAD = auto()
19+
20+
21+
@dataclass(repr=True)
22+
class Msg:
23+
"""Libfuzzer log message contents"""
24+
25+
ty: MsgType
26+
n_inputs: int
27+
cov: int | None
28+
ft: int | None
29+
30+
31+
# See libfuzzer output format: https://llvm.org/docs/LibFuzzer.html#output
32+
def parse_line(line: str) -> Msg | None:
33+
"""Parse libfuzzer log message"""
34+
35+
line = line.split()
36+
37+
msg_ty = None
38+
for ty in MsgType:
39+
if ty.name in line:
40+
msg_ty = ty
41+
break
42+
if not msg_ty:
43+
return None
44+
45+
n_inputs = 0
46+
if line[0][0] == "#":
47+
n_inputs = int(line[0][1:])
48+
49+
cov = None
50+
if "cov:" in line:
51+
cov_i = line.index("cov:")
52+
cov = int(line[cov_i + 1])
53+
54+
ft = None
55+
if "ft:" in line:
56+
ft_i = line.index("ft:")
57+
ft = int(line[ft_i + 1])
58+
59+
return Msg(msg_ty, n_inputs, cov, ft)
60+
61+
62+
class Supervisor:
63+
"""A wrapper to run the fuzzer until stopping criteria are satisfied
64+
or an error is encountered"""
65+
66+
def __init__(
67+
self, fuzzer_name: str, libfuzzer_log: Path, corpus_dir: Path, wait_2x_cov: bool
68+
):
69+
self.fuzzer_name = fuzzer_name
70+
self.libfuzzer_log = libfuzzer_log
71+
self.corpus_dir = corpus_dir
72+
self.wait_2x_cov = wait_2x_cov
73+
self.start_time = time.monotonic()
74+
self.init_cov = None
75+
self.latest_cov = None
76+
self.init_ft = None
77+
self.latest_ft = None
78+
self.latest_n_inputs = 0
79+
self.latest_cov_inc = self.start_time
80+
81+
def criteria_satisfied(self) -> bool:
82+
"""Indicates whether stopping criteria of the corresponding fuzzer
83+
are satisfied. E.g. it was running long enough and covered enough paths."""
84+
85+
# Coverage increased at least twice in comparison with corpus
86+
cov = False
87+
if self.init_cov:
88+
cov = self.latest_cov >= self.init_cov * 2
89+
90+
# Number of unique paths (features) increased at least twice
91+
# in comparison with corpus
92+
ft = False
93+
if self.init_ft:
94+
ft = self.latest_ft >= self.init_ft * 2
95+
96+
# At least 100_000 fuzzer test runs
97+
n_inputs = self.latest_n_inputs > 100_000
98+
99+
# Last coverage increase was detected 2 hours ago
100+
cov_inc = (time.monotonic() - self.latest_cov_inc) > 2 * 60 * 60
101+
102+
return (cov or ft or not self.wait_2x_cov) and n_inputs and cov_inc
103+
104+
def update_stats(self, msg: Msg):
105+
self.latest_n_inputs = msg.n_inputs
106+
if msg.cov:
107+
if self.latest_cov and msg.cov > self.latest_cov:
108+
self.latest_cov_inc = time.monotonic()
109+
self.latest_cov = msg.cov
110+
if msg.ft:
111+
self.latest_ft = msg.ft
112+
113+
if msg.ty == MsgType.INITED:
114+
self.init_cov = msg.cov
115+
self.init_ft = msg.ft
116+
117+
def run(self):
118+
"""Run the corresponding fuzzer until either stopping criteria are
119+
satisfied or fuzzing fails with an error indicating that some problem
120+
was detected."""
121+
122+
print(f"Running {self.fuzzer_name}")
123+
proc = subprocess.Popen(
124+
[
125+
f"./test/fuzz/{self.fuzzer_name}_fuzzer",
126+
# Read from both corpus folders, write only to the first
127+
self.corpus_dir.resolve(),
128+
f"test/static/corpus/{self.fuzzer_name}",
129+
],
130+
stdout=subprocess.PIPE,
131+
stderr=subprocess.PIPE,
132+
text=True,
133+
)
134+
135+
with self.libfuzzer_log.open("w") as libfuzzer_log:
136+
for line in proc.stderr:
137+
msg = parse_line(line)
138+
# Add timestamp in seconds relative to the starting time of a fuzzer
139+
line = f"{time.monotonic() - self.start_time}s {line}"
140+
libfuzzer_log.write(line)
141+
# if it is not a conventional log message print and continue
142+
if not msg:
143+
print(line.strip("\n"), end="\r\n")
144+
continue
145+
146+
# On pulse print the current state
147+
if msg.ty == MsgType.pulse:
148+
print(vars(self), end="\r\n")
149+
150+
self.update_stats(msg)
151+
if self.criteria_satisfied():
152+
print(f"Fuzzer {self.fuzzer_name} satisfied criteria")
153+
proc.terminate()
154+
try:
155+
proc.wait(timeout=15)
156+
except subprocess.TimeoutExpired:
157+
proc.kill()
158+
return
159+
160+
outs, errs = proc.communicate(timeout=15)
161+
print(outs)
162+
print(errs)
163+
code = proc.poll()
164+
if code != 0 and (not self.criteria_satisfied()):
165+
raise Exception(
166+
f"Fuzzer {self.fuzzer_name} stopped without satisfying criteria. Exit code: {code}"
167+
)
168+
169+
170+
# The script takes the fuzzing target name as the first argument.
171+
# Then it runs the fuzzing target until either stopping criteria are satisfied
172+
# or fuzzer detects a bug and fails.
173+
# Fuzzers should be built first. See README.md for instructions.
174+
if __name__ == "__main__":
175+
parser = ArgumentParser(
176+
prog="Fuzzer Supervisor",
177+
description="Runs a supplied fuzzer target until either stopping criteria\
178+
are satisfied or fuzzer detects a bug and fails",
179+
)
180+
parser.add_argument("fuzzer_target_name")
181+
parser.add_argument(
182+
"--libfuzzer-log",
183+
required=True,
184+
type=Path,
185+
help="File where libfuzzer log will be written",
186+
)
187+
parser.add_argument(
188+
"--corpus-dir",
189+
required=True,
190+
type=Path,
191+
help="Directory where libuzzer generated corpus will be stored",
192+
)
193+
parser.add_argument(
194+
"--wait-2x-cov",
195+
action="store_true",
196+
help="Enables 'coverage increased at least twice' criteria",
197+
)
198+
args = parser.parse_args()
199+
Supervisor(
200+
args.fuzzer_target_name, args.libfuzzer_log, args.corpus_dir, args.wait_2x_cov
201+
).run()

test/fuzz/httpfast_fuzzer.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <stdio.h>
2+
#include <stdarg.h>
3+
#include <stdint.h>
4+
#include <stddef.h>
5+
#include <string.h>
6+
#include <httpfast.h>
7+
8+
int handle_param(void *uobj, const char *name, size_t name_len, const char *value, size_t value_len)
9+
{
10+
// Simple check to allow branch visiting when calling param callback
11+
if (name_len % 2 == 0)
12+
{
13+
return 0;
14+
}
15+
16+
return 1;
17+
}
18+
19+
void on_error(void *uobj, int code, const char *fmt, va_list ap)
20+
{
21+
}
22+
23+
void on_warn(void *uobj, int code, const char *fmt, va_list ap)
24+
{
25+
}
26+
27+
int on_header(void *uobj,
28+
const char *name, size_t name_len,
29+
const char *value, size_t value_len,
30+
int is_continuation)
31+
{
32+
return 0;
33+
}
34+
35+
int on_body(void *uobj, const char *body, size_t body_len)
36+
{
37+
return 0;
38+
}
39+
40+
int on_request_line(void *uobj,
41+
const char *method, size_t method_len,
42+
const char *path, size_t path_len,
43+
const char *query, size_t query_len,
44+
int http_major, int http_minor)
45+
{
46+
return 0;
47+
}
48+
49+
int on_response_line(void *uobj,
50+
unsigned code,
51+
const char *reason, size_t reason_len,
52+
int http_major, int http_minor)
53+
{
54+
return 0;
55+
}
56+
57+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
58+
{
59+
const char *str = (const char *)data;
60+
61+
struct parse_http_events event_callbacks_one = {
62+
.on_error = on_error,
63+
.on_warn = on_warn,
64+
.on_header = on_header,
65+
.on_body = on_body,
66+
.on_request_line = NULL,
67+
.on_response_line = on_response_line};
68+
struct parse_http_events event_callbacks_two = {
69+
.on_error = on_error,
70+
.on_warn = on_warn,
71+
.on_header = on_header,
72+
.on_body = on_body,
73+
.on_request_line = on_request_line,
74+
.on_response_line = NULL};
75+
httpfast_parse(str, size, &event_callbacks_one, NULL);
76+
httpfast_parse(str, size, &event_callbacks_two, NULL);
77+
httpfast_parse_params(str, size, handle_param, NULL);
78+
return 0;
79+
}

test/fuzz/tpleval_fuzzer.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <stdint.h>
2+
#include <stddef.h>
3+
#include <string.h>
4+
#include <tpleval.h>
5+
6+
void term(int type, const char *str, size_t len, void *data)
7+
{
8+
}
9+
10+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
11+
{
12+
const char *str = (const char *)data;
13+
tpe_parse(str, size, term, NULL);
14+
return 0;
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name29=value29&name30=
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name18==value18
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
HTTP/1.1 200 OK
2+
Content-Type: text/plain
3+
Transfer-Encoding: chunked
4+
5+
4\r\n
6+
Wiki\r\n
7+
5\r\n
8+
pedia\r\n
9+
E\r\n
10+
in\r\n
11+
\r\n
12+
chunks.\r\n
13+
0\r\n
14+
\r\n
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
HTTP/1.1 200 OK
2+
Set-Cookie: SessionToken=abc123; Path=/
3+
Set-Cookie: Expires=Wed, 09 Jun 2021 10:18:14 GMT
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name11=value11&

0 commit comments

Comments
 (0)