summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorFlorian Westphal <fw@strlen.de>2025-10-17 13:51:41 +0200
committerFlorian Westphal <fw@strlen.de>2025-11-11 13:00:29 +0100
commitf2813fb53b00d6edde8bc9409712820c45de4c1e (patch)
tree39eacd46307645a8129fe45f7d83f9a7d1032435 /src
parent454f361434522bbeba32e114a14c336e1ebf20a1 (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++.c219
-rw-r--r--src/libnftables.c24
-rw-r--r--src/main.c101
-rw-r--r--src/preprocess.c2
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);
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 <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>