diff options
| author | Florian Westphal <fw@strlen.de> | 2025-10-17 13:51:41 +0200 |
|---|---|---|
| committer | Florian Westphal <fw@strlen.de> | 2025-11-11 13:00:29 +0100 |
| commit | f2813fb53b00d6edde8bc9409712820c45de4c1e (patch) | |
| tree | 39eacd46307645a8129fe45f7d83f9a7d1032435 /src | |
| parent | 454f361434522bbeba32e114a14c336e1ebf20a1 (diff) | |
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 <stage>, 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 <fw@strlen.de>
Diffstat (limited to 'src')
| -rw-r--r-- | src/afl++.c | 219 | ||||
| -rw-r--r-- | src/libnftables.c | 24 | ||||
| -rw-r--r-- | src/main.c | 101 | ||||
| -rw-r--r-- | src/preprocess.c | 2 |
4 files changed, 345 insertions, 1 deletions
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 <fw@strlen.de> + * + * 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 <nft.h> +#include <stdio.h> + +#include <errno.h> +#include <ctype.h> +#include <limits.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> + +#include <sys/stat.h> +#include <sys/wait.h> + +#include <afl++.h> +#include <nftables.h> + +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 <nft.h> #include <nftables/libnftables.h> +#include <afl++.h> #include <erec.h> #include <mnl.h> #include <parser.h> @@ -19,6 +20,17 @@ #include <sys/stat.h> #include <libgen.h> +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); @@ -21,6 +21,7 @@ #include <nftables/libnftables.h> #include <utils.h> #include <cli.h> +#include <afl++.h> 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 <nft.h> + #include <ctype.h> #include <stdio.h> #include <stdlib.h> |
