From f2813fb53b00d6edde8bc9409712820c45de4c1e Mon Sep 17 00:00:00 2001 From: Florian Westphal Date: Fri, 17 Oct 2025 13:51:41 +0200 Subject: [PATCH] support for afl++ (american fuzzy lop++) fuzzer afl comes with a compiler frontend that can add instrumentation suitable for running nftables via the "afl-fuzz" fuzzer. This change adds a "--with-fuzzer" option to configure script and enables specific handling in nftables and libnftables to speed up the fuzzing process. It also adds the "--fuzzer" command line option. afl-fuzz initialisation gets delayed until after the netlink context is set up and symbol tables such as (e.g. route marks) have been parsed. When afl-fuzz restarts the process with a new input round, it will resume *after* this point (see __AFL_INIT macro in main.c). With --fuzzer , nft will perform multiple fuzzing rounds per invocation: this increases processing rate by an order of magnitude. The argument to '--fuzzer' specifies the last stage to run: 1: 'parser': Only run / exercise the flex/bison parser. 2: 'eval': stop after the evaluation phase. This attempts to build a complete ruleset in memory, does symbol resolution, adds needed shift/masks to payload instructions etc. 3: 'netlink-ro': 'netlink-ro' builds the netlink buffer to send to the kernel, without actually doing so. 4: 'netlink-rw': Pass generated command/ruleset will be passed to the kernel. You can combine it with the '--check' option to send data to the kernel but without actually committing any changes. This could still end up triggering a kernel crash if there are bugs in the valiation / transaction / abort phases. Use 'netlink-ro' if you want to prevent nft from ever submitting any changes to the kernel or if you are only interested in fuzzing nftables and its libraries. In case a kernel splat is detected, the fuzzing process stops and all further fuzzer attemps are blocked until reboot. Signed-off-by: Florian Westphal --- .gitignore | 5 + Makefile.am | 5 + configure.ac | 17 +++ include/afl++.h | 48 +++++++ include/nftables.h | 3 + src/afl++.c | 219 +++++++++++++++++++++++++++++++ src/libnftables.c | 24 +++- src/main.c | 101 ++++++++++++++ src/preprocess.c | 2 + tests/afl++/README | 108 +++++++++++++++ tests/afl++/afl-sysctl.conf | 5 + tests/afl++/check_reproducers.sh | 118 +++++++++++++++++ tests/afl++/run-afl.sh | 47 +++++++ 13 files changed, 701 insertions(+), 1 deletion(-) create mode 100644 include/afl++.h create mode 100644 src/afl++.c create mode 100644 tests/afl++/README create mode 100644 tests/afl++/afl-sysctl.conf create mode 100755 tests/afl++/check_reproducers.sh create mode 100755 tests/afl++/run-afl.sh diff --git a/.gitignore b/.gitignore index db329eaf..719829b6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,11 @@ tests/build/tests.log /tests/shell/run-tests.sh.log /tests/shell/run-tests.sh.trs +# generated by run-afl.sh +/tests/afl++/in/ +/tests/afl++/nft.dict +/tests/afl++/out/ + # Debian package build temporary files build-stamp diff --git a/Makefile.am b/Makefile.am index bf1c3c44..d2cae2a3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -64,6 +64,7 @@ noinst_HEADERS = \ include/linux/netfilter_ipv6.h \ include/linux/netfilter_ipv6/ip6_tables.h \ \ + include/afl++.h \ include/cache.h \ include/cli.h \ include/cmd.h \ @@ -293,6 +294,10 @@ sbin_PROGRAMS += src/nft src_nft_SOURCES = src/main.c +if BUILD_AFL +src_nft_SOURCES += src/afl++.c +endif + if BUILD_CLI src_nft_SOURCES += src/cli.c endif diff --git a/configure.ac b/configure.ac index f3f6ad43..16c819b2 100644 --- a/configure.ac +++ b/configure.ac @@ -91,6 +91,18 @@ AC_MSG_ERROR([unexpected CLI value: $with_cli]) ]) AM_CONDITIONAL([BUILD_CLI], [test "x$with_cli" != xno]) +AC_ARG_ENABLE([fuzzer], + AS_HELP_STRING([--enable-fuzzer], [Enable fuzzer support. NEVER use this unless you work on nftables project]), + [enable_fuzzer=yes], [enable_fuzzer=no]) + +AM_CONDITIONAL([BUILD_AFL], [test "x$enable_fuzzer" = xyes]) + +HAVE_FUZZER_BUILD=0 +AS_IF([test "x$enable_fuzzer" != xno], [ + HAVE_FUZZER_BUILD=1 +]) +AC_DEFINE_UNQUOTED([HAVE_FUZZER_BUILD], [$HAVE_FUZZER_BUILD], [Whether to build with fuzzer support]) + AC_ARG_WITH([xtables], [AS_HELP_STRING([--with-xtables], [Use libxtables for iptables interaction])], [], [with_xtables=no]) @@ -181,3 +193,8 @@ echo " systemd unit: ${unitdir}" else echo " systemd unit: no" fi + +# Do not print "fuzzer support: no", this is development-only. +AS_IF([test "x$enable_fuzzer" = xyes ], [ + echo " fuzzer support: yes" + ], [ ]) diff --git a/include/afl++.h b/include/afl++.h new file mode 100644 index 00000000..a23bcef1 --- /dev/null +++ b/include/afl++.h @@ -0,0 +1,48 @@ +#ifndef _NFT_AFLPLUSPLUS_H_ +#define _NFT_AFLPLUSPLUS_H_ + +#include + +/* + * enum nft_afl_fuzzer_stage - current fuzzer stage + * + * @NFT_AFL_FUZZER_DISABLED: running without --fuzzer + * @NFT_AFL_FUZZER_PARSER: only fuzz the parser, do not run eval step. + * @NFT_AFL_FUZZER_EVALUATION: continue to evaluation step, if possible. + * @NFT_AFL_FUZZER_NETLINK_RO: convert internal representation to netlink buffer but don't send any changes to the kernel. + * @NFT_AFL_FUZZER_NETLINK_RW: send the netlink message to kernel for processing. + */ +enum nft_afl_fuzzer_stage { + NFT_AFL_FUZZER_DISABLED, + NFT_AFL_FUZZER_PARSER, + NFT_AFL_FUZZER_EVALUATION, + NFT_AFL_FUZZER_NETLINK_RO, + NFT_AFL_FUZZER_NETLINK_RW, +}; + +static inline void nft_afl_print_build_info(FILE *fp) +{ +#if HAVE_FUZZER_BUILD && defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + fprintf(fp, "\nWARNING: BUILT WITH FUZZER SUPPORT AND AFL INSTRUMENTATION\n"); +#elif defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + fprintf(fp, "\nWARNING: BUILT WITH AFL INSTRUMENTATION\n"); +#elif HAVE_FUZZER_BUILD + fprintf(fp, "\nWARNING: BUILT WITH FUZZER SUPPORT BUT NO AFL INSTRUMENTATION\n"); +#endif +} + +#if HAVE_FUZZER_BUILD +extern int nft_afl_init(struct nft_ctx *nft, enum nft_afl_fuzzer_stage s); +extern int nft_afl_main(struct nft_ctx *nft); +#else +static inline int nft_afl_main(struct nft_ctx *ctx) +{ + return -1; +} +static inline int nft_afl_init(struct nft_ctx *nft, enum nft_afl_fuzzer_stage s){ return -1; } +#endif + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +#define __AFL_INIT() do { } while (0) +#endif +#endif diff --git a/include/nftables.h b/include/nftables.h index c058667c..53815012 100644 --- a/include/nftables.h +++ b/include/nftables.h @@ -145,6 +145,9 @@ struct nft_ctx { void *json_root; json_t *json_echo; const char *stdin_buf; +#if HAVE_FUZZER_BUILD + int afl_ctx_stage; +#endif }; enum nftables_exit_codes { diff --git a/src/afl++.c b/src/afl++.c new file mode 100644 index 00000000..79925952 --- /dev/null +++ b/src/afl++.c @@ -0,0 +1,219 @@ +/* + * Copyright (c) Red Hat GmbH. Author: Florian Westphal + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 (or any + * later) as published by the Free Software Foundation. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +static const char self_fault_inject_file[] = "/proc/self/make-it-fail"; + +#ifdef __AFL_FUZZ_TESTCASE_LEN +/* the below macro gets passed via afl-cc, declares prototypes + * depending on the afl-cc flavor. + */ +__AFL_FUZZ_INIT(); +#else +/* this lets the source compile without afl-clang-fast/lto */ +static unsigned char fuzz_buf[4096]; +static ssize_t fuzz_len; + +#define __AFL_FUZZ_TESTCASE_LEN fuzz_len +#define __AFL_FUZZ_TESTCASE_BUF fuzz_buf +#define __AFL_FUZZ_INIT() do { } while (0) +#define __AFL_LOOP(x) \ + ((fuzz_len = read(0, fuzz_buf, sizeof(fuzz_buf))) > 0 ? 1 : 0) +#endif + +struct nft_afl_state { + FILE *make_it_fail_fp; +}; + +static struct nft_afl_state state; + +static char *preprocess(unsigned char *input, ssize_t len) +{ + ssize_t real_len = strnlen((char *)input, len); + + if (real_len == 0) + return NULL; + + if (real_len >= len) + input[len - 1] = 0; + + return (char *)input; +} + +static bool kernel_is_tainted(void) +{ + FILE *fp = fopen("/proc/sys/kernel/tainted", "r"); + unsigned int taint; + bool ret = false; + + if (fp) { + if (fscanf(fp, "%u", &taint) == 1 && taint) { + fprintf(stderr, "Kernel is tainted: 0x%x\n", taint); + sleep(3); /* in case we run under fuzzer, don't restart right away */ + ret = true; + } + + fclose(fp); + } + + return ret; +} + +static void fault_inject_write(FILE *fp, unsigned int v) +{ + rewind(fp); + fprintf(fp, "%u\n", v); + fflush(fp); +} + +static void fault_inject_enable(const struct nft_afl_state *state) +{ + if (state->make_it_fail_fp) + fault_inject_write(state->make_it_fail_fp, 1); +} + +static void fault_inject_disable(const struct nft_afl_state *state) +{ + if (state->make_it_fail_fp) + fault_inject_write(state->make_it_fail_fp, 0); +} + +static bool nft_afl_run_cmd(struct nft_ctx *ctx, const char *input_cmd) +{ + if (kernel_is_tainted()) + return false; + + switch (ctx->afl_ctx_stage) { + case NFT_AFL_FUZZER_PARSER: + case NFT_AFL_FUZZER_EVALUATION: + case NFT_AFL_FUZZER_NETLINK_RO: + nft_run_cmd_from_buffer(ctx, input_cmd); + return true; + case NFT_AFL_FUZZER_NETLINK_RW: + break; + } + + fault_inject_enable(&state); + nft_run_cmd_from_buffer(ctx, input_cmd); + fault_inject_disable(&state); + + return kernel_is_tainted(); +} + +static FILE *fault_inject_open(void) +{ + return fopen(self_fault_inject_file, "r+"); +} + +static bool nft_afl_state_init(struct nft_afl_state *state) +{ + state->make_it_fail_fp = fault_inject_open(); + return true; +} + +int nft_afl_init(struct nft_ctx *ctx, enum nft_afl_fuzzer_stage stage) +{ +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + const char instrumented[] = "afl instrumented"; +#else + const char instrumented[] = "no afl instrumentation"; +#endif + nft_afl_print_build_info(stderr); + + if (!nft_afl_state_init(&state)) + return -1; + + ctx->afl_ctx_stage = stage; + + if (state.make_it_fail_fp) { + unsigned int value; + int ret; + + rewind(state.make_it_fail_fp); + ret = fscanf(state.make_it_fail_fp, "%u", &value); + if (ret != 1 || value != 1) { + fclose(state.make_it_fail_fp); + state.make_it_fail_fp = NULL; + } + + /* if its enabled, disable and then re-enable ONLY + * when submitting data to the kernel. + * + * Otherwise even libnftables memory allocations could fail + * which is not what we want. + */ + fault_inject_disable(&state); + } + + fprintf(stderr, "starting (%s, %s fault injection)", instrumented, state.make_it_fail_fp ? "with" : "no"); + return 0; +} + +int nft_afl_main(struct nft_ctx *ctx) +{ + unsigned char *buf; + ssize_t len; + + if (kernel_is_tainted()) + return -1; + + if (state.make_it_fail_fp) { + FILE *fp = fault_inject_open(); + + /* reopen is needed because /proc/self is a symlink, i.e. + * fp refers to parent process, not "us". + */ + if (!fp) { + fprintf(stderr, "Could not reopen %s: %s", self_fault_inject_file, strerror(errno)); + return -1; + } + + fclose(state.make_it_fail_fp); + state.make_it_fail_fp = fp; + } + + buf = __AFL_FUZZ_TESTCASE_BUF; + + while (__AFL_LOOP(UINT_MAX)) { + char *input; + + len = __AFL_FUZZ_TESTCASE_LEN; // do not use the macro directly in a call! + + input = preprocess(buf, len); + if (!input) + continue; + + /* buf is null terminated at this point */ + if (!nft_afl_run_cmd(ctx, input)) + continue; + + /* Kernel is tainted. + * exit() will cause a restart from afl-fuzz. + * Avoid burning cpu cycles in this case. + */ + sleep(1); + } + + /* afl-fuzz will restart us. */ + return 0; +} diff --git a/src/libnftables.c b/src/libnftables.c index 9f6a1bc3..66b03a11 100644 --- a/src/libnftables.c +++ b/src/libnftables.c @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,17 @@ #include #include +static int do_mnl_batch_talk(struct netlink_ctx *ctx, struct list_head *err_list, + uint32_t num_cmds) +{ +#if HAVE_FUZZER_BUILD + if (ctx->nft->afl_ctx_stage && + ctx->nft->afl_ctx_stage < NFT_AFL_FUZZER_NETLINK_RW) + return 0; +#endif + return mnl_batch_talk(ctx, err_list, num_cmds); +} + static int nft_netlink(struct nft_ctx *nft, struct list_head *cmds, struct list_head *msgs) { @@ -37,7 +49,13 @@ static int nft_netlink(struct nft_ctx *nft, if (list_empty(cmds)) goto out; +#if HAVE_FUZZER_BUILD + if (nft->afl_ctx_stage && + nft->afl_ctx_stage <= NFT_AFL_FUZZER_EVALUATION) + goto out; +#endif batch_seqnum = mnl_batch_begin(ctx.batch, mnl_seqnum_inc(&seqnum)); + list_for_each_entry(cmd, cmds, list) { ctx.seqnum = cmd->seqnum_from = mnl_seqnum_inc(&seqnum); ret = do_command(&ctx, cmd); @@ -57,7 +75,7 @@ static int nft_netlink(struct nft_ctx *nft, if (!mnl_batch_ready(ctx.batch)) goto out; - ret = mnl_batch_talk(&ctx, &err_list, num_cmds); + ret = do_mnl_batch_talk(&ctx, &err_list, num_cmds); if (ret < 0) { if (ctx.maybe_emsgsize && errno == EMSGSIZE) { netlink_io_error(&ctx, NULL, @@ -605,6 +623,10 @@ int nft_run_cmd_from_buffer(struct nft_ctx *nft, const char *buf) rc = nft_parse_bison_buffer(nft, nlbuf, &msgs, &cmds, &indesc_cmdline); +#if HAVE_FUZZER_BUILD + if (nft->afl_ctx_stage == NFT_AFL_FUZZER_PARSER) + goto err; +#endif parser_rc = rc; rc = nft_evaluate(nft, &msgs, &cmds); diff --git a/src/main.c b/src/main.c index 72151e62..c2e909d2 100644 --- a/src/main.c +++ b/src/main.c @@ -21,6 +21,7 @@ #include #include #include +#include static struct nft_ctx *nft; @@ -55,6 +56,9 @@ enum opt_indices { IDX_ECHO, #define IDX_CMD_OUTPUT_START IDX_ECHO IDX_JSON, +#if HAVE_FUZZER_BUILD + IDX_FUZZER, +#endif IDX_DEBUG, #define IDX_CMD_OUTPUT_END IDX_DEBUG }; @@ -83,6 +87,11 @@ enum opt_vals { OPT_TERSE = 't', OPT_OPTIMIZE = 'o', OPT_INVALID = '?', + +#if HAVE_FUZZER_BUILD + /* keep last */ + OPT_FUZZER = 254 +#endif }; struct nft_opt { @@ -140,6 +149,10 @@ static const struct nft_opt nft_options[] = { "Specify debugging level (scanner, parser, eval, netlink, mnl, proto-ctx, segtree, all)"), [IDX_OPTIMIZE] = NFT_OPT("optimize", OPT_OPTIMIZE, NULL, "Optimize ruleset"), +#if HAVE_FUZZER_BUILD + [IDX_FUZZER] = NFT_OPT("fuzzer", OPT_FUZZER, "stage", + "fuzzer stage to run (parser, eval, netlink-ro, netlink-rw)"), +#endif }; #define NR_NFT_OPTIONS (sizeof(nft_options) / sizeof(nft_options[0])) @@ -230,6 +243,7 @@ static void show_help(const char *name) print_option(&nft_options[i]); fputs("\n", stdout); + nft_afl_print_build_info(stdout); } static void show_version(void) @@ -271,6 +285,8 @@ static void show_version(void) " libxtables: %s\n", PACKAGE_NAME, PACKAGE_VERSION, RELEASE_NAME, cli, json, minigmp, xt); + + nft_afl_print_build_info(stdout); } static const struct { @@ -311,6 +327,38 @@ static const struct { }, }; +#if HAVE_FUZZER_BUILD +static const struct { + const char *name; + enum nft_afl_fuzzer_stage stage; +} fuzzer_stage_param[] = { + { + .name = "parser", + .stage = NFT_AFL_FUZZER_PARSER, + }, + { + .name = "eval", + .stage = NFT_AFL_FUZZER_EVALUATION, + }, + { + .name = "netlink-ro", + .stage = NFT_AFL_FUZZER_NETLINK_RO, + }, + { + .name = "netlink-rw", + .stage = NFT_AFL_FUZZER_NETLINK_RW, + }, +}; +static void afl_exit(const char *err) +{ + fprintf(stderr, "Error: fuzzer: %s\n", err); + sleep(60); /* assume we're running under afl-fuzz and would be restarted right away */ + exit(EXIT_FAILURE); +} +#else +static inline void afl_exit(const char *err) { } +#endif + static void nft_options_error(int argc, char * const argv[], int pos) { int i; @@ -359,6 +407,7 @@ static bool nft_options_check(int argc, char * const argv[]) int main(int argc, char * const *argv) { const struct option *options = get_options(); + enum nft_afl_fuzzer_stage fuzzer_stage = 0; bool interactive = false, define = false; const char *optstring = get_optstring(); unsigned int output_flags = 0; @@ -500,6 +549,26 @@ int main(int argc, char * const *argv) case OPT_OPTIMIZE: nft_ctx_set_optimize(nft, 0x1); break; +#if HAVE_FUZZER_BUILD + case OPT_FUZZER: + { + unsigned int i; + + for (i = 0; i < array_size(fuzzer_stage_param); i++) { + if (strcmp(fuzzer_stage_param[i].name, optarg)) + continue; + fuzzer_stage = fuzzer_stage_param[i].stage; + break; + } + + if (i == array_size(fuzzer_stage_param)) { + fprintf(stderr, "invalid fuzzer stage `%s'\n", + optarg); + goto out_fail; + } + } + break; +#endif case OPT_INVALID: goto out_fail; } @@ -512,6 +581,38 @@ int main(int argc, char * const *argv) nft_ctx_output_set_flags(nft, output_flags); + if (fuzzer_stage) { + unsigned int input_flags; + + if (filename || define || interactive) + afl_exit("-D/--define, -f/--filename and -i/--interactive are incompatible options"); + + rc = nft_afl_init(nft, fuzzer_stage); + if (rc != 0) + afl_exit("cannot initialize"); + + input_flags = nft_ctx_input_get_flags(nft); + + /* DNS lookups can result in severe fuzzer slowdown */ + input_flags |= NFT_CTX_INPUT_NO_DNS; + nft_ctx_input_set_flags(nft, input_flags); + + if (fuzzer_stage < NFT_AFL_FUZZER_NETLINK_RW) + nft_ctx_set_dry_run(nft, true); + + fprintf(stderr, "Awaiting fuzzer-generated inputs\n"); + } + + __AFL_INIT(); + + if (fuzzer_stage) { + rc = nft_afl_main(nft); + if (rc != 0) + afl_exit("fatal error"); + + return EXIT_SUCCESS; + } + if (optind != argc) { char *buf; diff --git a/src/preprocess.c b/src/preprocess.c index 619f67a1..640ffad3 100644 --- a/src/preprocess.c +++ b/src/preprocess.c @@ -6,6 +6,8 @@ * later) as published by the Free Software Foundation. */ +#include + #include #include #include diff --git a/tests/afl++/README b/tests/afl++/README new file mode 100644 index 00000000..9ff7e648 --- /dev/null +++ b/tests/afl++/README @@ -0,0 +1,108 @@ +First you need to install afl++. If your distro doesn't package it, you can +get it from https://github.com/AFLplusplus/AFLplusplus + +Next build and install afl++, this needs llvm/clang installed. + +Nftables configue + compile steps: + +To get the best results, build nftables with the following options: + +CC=afl-clang-lto LD=afl-clang-lto CFLAGS+=-fsanitize=address ./configure \ + --disable-shared --with-json --without-xtables \ + --with-cli=readline --enable-fuzzer --disable-man-doc + +[ you might want to enable xtables or use a different cli, your choice ]. + +Important options are: +--disable-shared, so that libnftables is instrumented too. +--enable-fuzzer + +--enable-fuzzer is not strictly required, you can run normal nft builds under +afl-fuzz too. But the execution speed will be much slower. + +--enable-fuzzer also provides the nft --fuzzer command line option that allows +more fine-grained control over what code paths should be covered by the fuzzing +process. + +When fuzzing in this mode, then each new input passes through the following +processing stages: + +1: 'parser': + Only run / exercise the flex/bison parser. + +2: 'eval': stop after the evaluation phase. + This attempts to build a complete ruleset in memory, does + symbol resolution, adds needed shift/masks to payload instructions + etc. + +3: 'netlink-ro': + Also build/serialize the ruleset into netlink-commands to send to the + kernel, but omit the final write so the kernel will not see the message. + +4: 'netlink-rw': + Same as 3 but the message will be sent to the kernel. + You can combine this option with the '--check' option to send data to the + kernel but without committing any changes. + Unlike 3), even when combined with '--check', this option can still trigger + a kernel crash if there are bugs in the kernel, e.g. during the + valiation / transaction / abort stages. + When using this without '--check', remember to lauch nft in its own network + namespace to prevent VM connectivity loss due to committed 'drop' rules. + +Use 'netlink-ro' if you want to prevent nft from ever submitting any +changes to the kernel or if you are only interested in fuzzing nftables +and its libraries. + +All --fuzzer modes EXCEPT 'netlink-rw' do imply --check as these modes never +alter state in the kernel. + +In rw mode, before each input, nft checks the kernel "taint" status as provided +by "/proc/sys/kernel/tainted". If this is non-zero, fuzzing stops. + +To run nftables under afl++, run nftables like this: + +unshare -n \ + afl-fuzz -g 16 -G 2000 -t 5000 -i tests/afl++/in -o tests/afl++/out \ + -- src/nft --fuzzer + +arg should be either "netlink-ro" (if you only want to exercise nft userspace) +or "netlink-rw" (if you want to test kernel code paths too). + +Its also a good idea to do this from tmux/screen so you can disconnect/reattach +later. You can also spawn multiple instances. +In that case, add the '-M' option to afl-fuzz for the first instance you start, +and '-S' for subsequent secondary instances. + +This expects a unique directory name as argument, so interesting findings +from the different instances are cleary separated. + +With above default options, outputs will be in 'tests/afl++/out/'. +Please see the afl++ docs for more information about this. + +You can use tests/afl++/run-afl.sh script to autogenerate an initial set of valid +inputs that the fuzzer can start from. + +Use + +sysctl -f tests/afl++/afl-sysctl.conf + +to enable some fuzzer-beneficial sysctl options. + +Kernel config: +When using the 'netlink-rw' option it is best to also use a debug kernel +with at least: + +# CONFIG_NOTIFIER_ERROR_INJECTION is not set +CONFIG_KASAN=y +CONFIG_DEBUG_ATOMIC_SLEEP=y +CONFIG_DEBUG_LOCKDEP=y +CONFIG_FAULT_INJECTION=y +CONFIG_FAILSLAB=y +CONFIG_DEBUG_KMEMLEAK=y + +If you want to sample test coverage, then set +CONFIG_GCOV_KERNEL=y + +echo GCOV_PROFILE := y > net/netfilter/Makefile + +or enable CONFIG_GCOV_PROFILE_ALL=y. diff --git a/tests/afl++/afl-sysctl.conf b/tests/afl++/afl-sysctl.conf new file mode 100644 index 00000000..490e62b0 --- /dev/null +++ b/tests/afl++/afl-sysctl.conf @@ -0,0 +1,5 @@ +kernel.core_pattern=core +fs.suid_dumpable=1 +kernel.core_uses_pid=0 +kernel.randomize_va_space=0 +kernel.sched_autogroup_enabled=1 diff --git a/tests/afl++/check_reproducers.sh b/tests/afl++/check_reproducers.sh new file mode 100755 index 00000000..063ad37d --- /dev/null +++ b/tests/afl++/check_reproducers.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# convenience script to check afl reproducers. Many reproducers are +# variations of the same bug/root cause, so this allows to check if +# we get same assertion. + +pts=$(readlink /proc/self/fd/0) + +files=$(find tests/afl++/out/*/crashes/ -type f -name 'id:0000*') + +TMP="" + +cleanup() +{ + rm -rf "$TMP" +} + +trap cleanup EXIT + +[ -z "$files" ] && exit 0 + +TMP=$(mktemp -d) + +prompt_del() +{ + local f="$1" + + read yn < "$pts" + if [ "$yn" = "y" ];then + echo delete + rm -f "$f" + elif [ "$yn" = "n" ]; then + echo kept. + fi +} + +filter_asan() +{ + # retrain the backtrace only. + # else check_dup_output won't detect duplicates due to PID + # and register dump. + grep '#' "$TMP/output" > "$TMP/asan_bt" + [ -s "$TMP/asan_bt" ] && mv "$TMP/asan_bt" "$TMP/output" +} + +check_dup_output() +{ + local f="$1" + local sha="" + + if [ ! -s "$TMP/output" ]; then + return 0 + fi + + sha=$(sha256sum "$TMP/output" | cut -f 1 -d " ") + + if [ -f "$TMP/$sha.output" ]; then + local dup=$(cat "$TMP/$sha".filename) + echo "Duplicate output, identical splat seen from $dup" + + local s1=$(du -sb "$dup"| cut -f 1) + local s2=$(du -sb "$f"| cut -f 1) + + # keep the smaller file. + if [ "$s2" -lt "$s1" ];then + echo "$f" > "$TMP/$sha".filename + f="$dup" + fi + + echo "Delete $f?" + prompt_del "$f" + return 1 + fi + + # New output. + mv "$TMP/output" "$TMP/$sha.output" + echo "$f" > "$TMP/$sha.filename" + return 0 +} + +check_input() +{ + local NFT="$1" + local f="$2" + + if [ ! -x "$NFT" ] ;then + return 1 + fi + + for arg in "" "-j"; do + "$NFT" --check $arg -f - < "$f" > "$TMP/output" 2>&1 + local rv=$? + + if grep AddressSanitizer "$TMP/output"; then + echo "ASAN: \"$NFT $arg -f $f\" exited with $rv" + filter_asan "$TMP/output" + check_dup_output "$f" + return 0 + fi + + [ $rv -eq 0 ] && continue + [ $rv -eq 1 ] && continue + + echo "\"$NFT $arg -f $f\" exited with $rv" + check_dup_output "$f" + return 0 + done + + return 1 +} + +for f in $files;do + check_input src/nft-asan "$f" && continue + check_input src/nft "$f" && continue + + echo "$f did not trigger a splat. Delete?" + prompt_del "$f" +done diff --git a/tests/afl++/run-afl.sh b/tests/afl++/run-afl.sh new file mode 100755 index 00000000..ee9d98fe --- /dev/null +++ b/tests/afl++/run-afl.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -e + +ME=$(dirname $0) +SRC_NFT="$(dirname $0)/../../src/nft" + +cd $ME/../.. + +echo "# nft tokens and common strings, autogenerated via $0" > tests/afl++/nft.dict +grep %token src/parser_bison.y | while read token comment value; do + echo $value +done | grep -v "[A-Z]" | sort | cut -f 1 -d " " | grep '^"' >> tests/afl++/nft.dict + +cat >> tests/afl++/nft.dict <