samples: Add fuzz sample
This is a simple example of fuzz test integration with Zephyr apps that displays LLVM libfuzzer's most important feature: it's ability to detect and explore deep and complicated call trees by exploiting coverage information gleaned from instrumented binaries. See README.rst and the inline comments for details Signed-off-by: Andy Ross <andyross@google.com>
This commit is contained in:
parent
65d657685e
commit
7f5e1904cc
8
samples/subsys/debug/fuzz/CMakeLists.txt
Normal file
8
samples/subsys/debug/fuzz/CMakeLists.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(fuzz)
|
||||
|
||||
target_sources(app PRIVATE src/main.c)
|
73
samples/subsys/debug/fuzz/README.rst
Normal file
73
samples/subsys/debug/fuzz/README.rst
Normal file
|
@ -0,0 +1,73 @@
|
|||
Fuzzing Example
|
||||
###############
|
||||
|
||||
Overview
|
||||
********
|
||||
|
||||
This is a simple example of fuzz test integration with Zephyr apps
|
||||
that displays LLVM libfuzzer's most important feature: it's ability to
|
||||
detect and explore deep and complicated call trees by exploiting
|
||||
coverage information gleaned from instrumented binaries.
|
||||
|
||||
Building and Running
|
||||
********************
|
||||
|
||||
Right now, the only toolchain that works with libfuzzer is a recent 64
|
||||
bit clang (clang 14 was used at development time). Make sure such a
|
||||
toolchain is installed in your host environment, and build with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ clang --version
|
||||
clang version 14.0.6
|
||||
Target: x86_64-pc-linux-gnu
|
||||
Thread model: posix
|
||||
InstalledDir: /usr/bin
|
||||
$ export ZEPHYR_TOOLCHAIN_VARIANT=llvm
|
||||
$ west build -t run -b native_posix_64 samples/subsys/debug/fuzz
|
||||
|
||||
Over 10-20 seconds or so (runtimes can be quite variable) you will see
|
||||
it discover and recurse deeper into the test's deliberately
|
||||
constructed call tree, eventually crashing when it reaches the final
|
||||
state and reporting the failure.
|
||||
|
||||
Example output:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
-- west build: running target run
|
||||
[0/1] cd /home/andy/z/zephyr/build && .../andy/z/zephyr/build/zephyr/zephyr.exe
|
||||
INFO: Running with entropic power schedule (0xFF, 100).
|
||||
INFO: Seed: 108038547
|
||||
INFO: Loaded 1 modules (2112 inline 8-bit counters): 2112 [0x55cbe336ec55, 0x55cbe336f495),
|
||||
INFO: Loaded 1 PC tables (2112 PCs): 2112 [0x55cbe336f498,0x55cbe3377898),
|
||||
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
|
||||
*** Booting Zephyr OS build zephyr-v3.1.0-3976-g806034e02865 ***
|
||||
Hello World! native_posix_64
|
||||
INFO: A corpus is not provided, starting from an empty corpus
|
||||
#2 INITED cov: 101 ft: 102 corp: 1/1b exec/s: 0 rss: 30Mb
|
||||
#
|
||||
# Found key 0
|
||||
#
|
||||
NEW_FUNC[1/6]: 0x55cbe3339c45 in check1 /home/andy/z/zephyr/samples/subsys/debug/fuzz/src/main.c:43
|
||||
NEW_FUNC[2/6]: 0x55cbe333c8d8 in char_out /home/andy/z/zephyr/lib/os/printk.c:108
|
||||
...
|
||||
...
|
||||
...
|
||||
#418965 REDUCE cov: 165 ft: 166 corp: 15/400b lim: 4052 exec/s: 38087 rss: 31Mb L: 5/256 MS: 1 EraseBytes-
|
||||
#524288 pulse cov: 165 ft: 166 corp: 15/400b lim: 4096 exec/s: 40329 rss: 31Mb
|
||||
#
|
||||
# Found key 5
|
||||
#
|
||||
NEW_FUNC[1/1]: 0x55cbe3339ff7 in check6 /home/andy/z/zephyr/samples/subsys/debug/fuzz/src/main.c:48
|
||||
#579131 NEW cov: 168 ft: 169 corp: 16/406b lim: 4096 exec/s: 38608 rss: 31Mb L: 6/256 MS: 1 InsertByte-
|
||||
#579432 NEW cov: 170 ft: 171 corp: 17/414b lim: 4096 exec/s: 38628 rss: 31Mb L: 8/256 MS: 1 PersAutoDict- DE: "\000\000"-
|
||||
#579948 REDUCE cov: 170 ft: 171 corp: 17/413b lim: 4096 exec/s: 38663 rss: 31Mb L: 7/256 MS: 1 EraseBytes-
|
||||
#
|
||||
# Found key 6
|
||||
#
|
||||
UndefinedBehaviorSanitizer:DEADLYSIGNAL
|
||||
==3243305==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55cbe333a09d bp 0x7f3114afadf0 sp 0x7f3114afade0 T3243308)
|
||||
==3243305==The signal is caused by a WRITE memory access.
|
||||
==3243305==Hint: address points to the zero page.
|
||||
#0 0x55cbe333a09d in check6 /home/andy/z/zephyr/samples/subsys/debug/fuzz/src/main.c:48:1
|
1
samples/subsys/debug/fuzz/prj.conf
Normal file
1
samples/subsys/debug/fuzz/prj.conf
Normal file
|
@ -0,0 +1 @@
|
|||
CONFIG_ARCH_POSIX_LIBFUZZER=y
|
16
samples/subsys/debug/fuzz/sample.yaml
Normal file
16
samples/subsys/debug/fuzz/sample.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
sample:
|
||||
description: native_posix-based libfuzzer example
|
||||
name: fuzz
|
||||
tests:
|
||||
debug.fuzz:
|
||||
toolchain_allow: llvm
|
||||
platform_allow: native_posix_64
|
||||
harness: console
|
||||
harness_config:
|
||||
type: one_line
|
||||
# The sample will run all the way to key 6 (to show off its
|
||||
# ability to search a 1-in-2^56 haystack), but that takes 20-60s
|
||||
# and is quite variable. It's enough to know that it's finding
|
||||
# new code paths by the third new coverage announcement.
|
||||
regex:
|
||||
- "Found key 2"
|
79
samples/subsys/debug/fuzz/src/main.c
Normal file
79
samples/subsys/debug/fuzz/src/main.c
Normal file
|
@ -0,0 +1,79 @@
|
|||
/* Copyright (c) 2022 Google, LLC.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <zephyr/zephyr.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Fuzz testing is coverage-based, so we want to hide a failure case
|
||||
* (a write through a null pointer in this case) down inside a call
|
||||
* tree in such a way that it would be very unlikely to be found by
|
||||
* randomly-selected input. But the fuzzer can still find it in
|
||||
* linear(-ish) time by discovering each new function along the way
|
||||
* and then probing that new space. The 1 in 2^56 case here would
|
||||
* require months-to-years of work for a large datacenter, but the
|
||||
* fuzzer gets it in 20 seconds or so. This requires that the code for
|
||||
* each case be distinguishable/instrumentable though, which is why we
|
||||
* generate the recursive handler functions this way and disable
|
||||
* inlining to prevent optimization.
|
||||
*/
|
||||
int *global_null_ptr;
|
||||
static const uint8_t key[] = { 0x9e, 0x21, 0x0c, 0x18, 0x9d, 0xd1, 0x7d };
|
||||
bool found[ARRAY_SIZE(key)];
|
||||
|
||||
#define LASTKEY (ARRAY_SIZE(key) - 1)
|
||||
|
||||
#define GEN_CHECK(cur, nxt) \
|
||||
void check##nxt(uint8_t *data, size_t sz); \
|
||||
void __attribute__((noinline)) check##cur(uint8_t *data, size_t sz) \
|
||||
{ \
|
||||
if (cur < sz && data[cur] == key[cur]) { \
|
||||
if (!found[cur]) { \
|
||||
printk("#\n# Found key %d\n#\n", cur); \
|
||||
found[cur] = true; \
|
||||
} \
|
||||
if (cur == LASTKEY) { \
|
||||
*global_null_ptr = 0; /* boom! */ \
|
||||
} else { \
|
||||
check##nxt(data, sz); \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
GEN_CHECK(0, 1)
|
||||
GEN_CHECK(1, 2)
|
||||
GEN_CHECK(2, 3)
|
||||
GEN_CHECK(3, 4)
|
||||
GEN_CHECK(4, 5)
|
||||
GEN_CHECK(5, 6)
|
||||
GEN_CHECK(6, 0)
|
||||
|
||||
/* Fuzz input received from LLVM via "interrupt" */
|
||||
extern uint8_t *posix_fuzz_buf, posix_fuzz_sz;
|
||||
|
||||
K_SEM_DEFINE(fuzz_sem, 0, K_SEM_MAX_LIMIT);
|
||||
|
||||
static void fuzz_isr(const void *arg)
|
||||
{
|
||||
/* We could call check0() to execute the fuzz case here, but
|
||||
* pass it through to the main thread instead to get more OS
|
||||
* coverage.
|
||||
*/
|
||||
k_sem_give(&fuzz_sem);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
printk("Hello World! %s\n", CONFIG_BOARD);
|
||||
|
||||
IRQ_CONNECT(CONFIG_ARCH_POSIX_FUZZ_IRQ, 0, fuzz_isr, NULL, 0);
|
||||
irq_enable(CONFIG_ARCH_POSIX_FUZZ_IRQ);
|
||||
|
||||
while (true) {
|
||||
k_sem_take(&fuzz_sem, K_FOREVER);
|
||||
|
||||
/* Execute the fuzz case we got from LLVM and passed
|
||||
* through an interrupt to this thread.
|
||||
*/
|
||||
check0(posix_fuzz_buf, posix_fuzz_sz);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue