Re: [PATCH nft] initial support for the afl++ (american fuzzy lop++) fuzzer

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Fri, Dec 01, 2023 at 04:43:04PM +0100, Florian Westphal wrote:
> 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.

Pull "such as" into the braces?

> 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.
> 
> Each new input passes through the following processing stages:
> 
> 1: pre-validation. Checks the input buffer (the fuzzer generated
>    ruleset/commands) to make sure minimal requirements are met:
>    - minimum "ineresting" length
>    - 0-terminated input (command, ruleset)
>    - initial keywords could result in state change (ruleset load,
>      deletion or addition of rules/chains/tables,..
> 
> This stage is always executed.
> 
> 2: 'parser':
>     Only run flex/bison parser and quit instantly if that fails,
>     without evaluation of the generated AST (No need to generate
>     error messages for the user).
> 
> 3: 'eval': pass the input through the evaluation phase as well.
>     This attempts to build a complete ruleset in memory, does
>     symbol resolution, adds needed shift/masks to payload instructions
>     etc.
> 
> 4: 'netlink-ro' or 'netlink-write'.
>     'netlink-ro' builds the netlink buffer to send to the kernel,
>     without actually doing so.
>     With 'write', the generated command/ruleset will be passed to the
>     kernel.
> 
> The argument to '--fuzzer' specifies the last stage to run.
> 
> Use 'netlink-ro' if you want to prevent the fuzzing from ever submitting any
> changes to the kernel.
> 
> You can combine '--fuzzer netlink-write' 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.
> 
> Before sending a command to the kernel, the input is stored in temporary
> files (named [0-255]/[0-255] in the "tests/afl++/nft-afl-work" directory.
> 
> In case a splat is detected, the fuzzing process halts.
> This will also block all further fuzzer attempts until reboot.
> 
> nft-afl-work/log logs when a "talk to kernel" phase begins or when a
> kernel crash/warning splat was detected.
> 
> Check "tests/afl++/hooks" for script hooks, those allow to customize the
> fuzzing process by making regular snapshots of the ruleset, deleting old
> temporary files, flushing rules, checking KASAN, etc.
> 
> The default hook scripts will enable fault injection for the nft process
> being fuzzed, i.e. kernel memory allocations will randonly fail.

s/randonly/randomly/

> 
> Signed-off-by: Florian Westphal <fw@xxxxxxxxx>
> ---
>  I still can get nft to crash or assert() with this but
>  if i try to fix all of these crashes I'll never get this
>  upstreamed.
> 
>  There is also one issue: My local copy sets
>  NFT_CTX_INPUT_NO_DNS with --fuzzer to prevent the fuzzing from stalling
>  when input contains "hostnames".
> 
>  Let me know if i should send a v2 that does this in
>  this version too.
> 
>  .gitignore                     |   6 +
>  Makefile.am                    |   5 +
>  configure.ac                   |  20 +
>  include/afl++.h                |  49 ++
>  include/nftables.h             |   3 +
>  src/afl++.c                    | 825 +++++++++++++++++++++++++++++++++
>  src/libnftables.c              |  26 ++
>  src/main.c                     |  94 ++++
>  src/mnl.c                      |  13 +
>  tests/afl++/README             | 117 +++++
>  tests/afl++/TODO               |   6 +
>  tests/afl++/afl-sysctl.cfg     |   5 +
>  tests/afl++/hooks/init         |  54 +++
>  tests/afl++/hooks/post-commit  |   8 +
>  tests/afl++/hooks/post-restart |   8 +
>  tests/afl++/hooks/pre-commit   |   6 +
>  tests/afl++/hooks/pre-restart  |  12 +
>  tests/afl++/run-afl.sh         |  41 ++
>  18 files changed, 1298 insertions(+)
>  create mode 100644 include/afl++.h
>  create mode 100644 src/afl++.c
>  create mode 100644 tests/afl++/README
>  create mode 100644 tests/afl++/TODO
>  create mode 100644 tests/afl++/afl-sysctl.cfg
>  create mode 100755 tests/afl++/hooks/init
>  create mode 100755 tests/afl++/hooks/post-commit
>  create mode 100755 tests/afl++/hooks/post-restart
>  create mode 100755 tests/afl++/hooks/pre-commit
>  create mode 100755 tests/afl++/hooks/pre-restart
>  create mode 100755 tests/afl++/run-afl.sh
> 
> diff --git a/.gitignore b/.gitignore
> index a62e31f31c6b..1d9eb9c22a70 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -22,6 +22,12 @@ libtool
>  *.payload.got
>  tests/build/tests.log
>  
> +# generated by run-afl.sh
> +tests/afl++/in/
> +tests/afl++/nft.dict
> +tests/afl++/out/
> +tests/afl++/nft-afl-work/
> +
>  # Debian package build temporary files
>  build-stamp
>  
> diff --git a/Makefile.am b/Makefile.am
> index 0ed831a19e95..21c01e712010 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -63,6 +63,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 \
> @@ -284,6 +285,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 724a4ae726c1..408be61080e5 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -91,6 +91,15 @@ 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])
> +AS_IF([test "x$enable_fuzzer" != xno], [
> +	AC_DEFINE([HAVE_FUZZER_BUILD], [1], [Define if you want to build with fuzzer support])
> +	], [])
> +
>  AC_ARG_WITH([xtables], [AS_HELP_STRING([--with-xtables],
>              [Use libxtables for iptables interaction])],
>  	    [], [with_xtables=no])
> @@ -128,3 +137,14 @@ nft configuration:
>    enable man page:              ${enable_man_doc}
>    libxtables support:		${with_xtables}
>    json output support:          ${with_json}"
> +
> +AS_IF([test "$enable_python" != "no"], [
> +	echo "  enable Python:		yes (with $PYTHON_BIN)"
> +	], [
> +	echo "  enable Python:		no"
> +	]
> +	)
> +# 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 000000000000..dd95a759414a
> --- /dev/null
> +++ b/include/afl++.h
> @@ -0,0 +1,49 @@
> +#ifndef _NFT_AFLPLUSPLUS_H_
> +#define _NFT_AFLPLUSPLUS_H_
> +
> +#include <nftables/libnftables.h>
> +#include <config.h>
> +
> +/*
> + * enum nft_afl_fuzzer_stage - current fuzzer stage
> + *
> + * @NFT_AFL_FUZZER_ALL: no limitations
> + * @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_WR: send the netlink message to kernel for processing.
> + */
> +enum nft_afl_fuzzer_stage {
> +	NFT_AFL_FUZZER_ALL,
> +	NFT_AFL_FUZZER_PARSER,
> +	NFT_AFL_FUZZER_EVALUATION,
> +	NFT_AFL_FUZZER_NETLINK_RO,
> +	NFT_AFL_FUZZER_NETLINK_WR,
> +};

I bet the conditional afl_ctx_stage returns would reduce to a simple 'if
(nft->afl_ctx_state <= NFT_AFL_FUZZER_SOMEVAL)' if NFT_AFL_FUZZER_ALL
was put at the end of this enum.

> +
> +static inline void nft_afl_print_build_info(FILE *fp)
> +{
> +#if defined(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, "\nAWRNING: BUILT WITH AFL INSTRUMENTATION\n");

s/AWRNING/WARNING/

> +#elif defined(HAVE_FUZZER_BUILD)
> +	fprintf(fp, "\nWARNING: BUILT WITH FUZZER SUPPORT BUT NO AFL INSTRUMENTATION\n");
> +#endif
> +}
> +
> +#if defined(HAVE_FUZZER_BUILD)
> +extern int nft_afl_init(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(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 4b7c335928da..02d110e609eb 100644
> --- a/include/nftables.h
> +++ b/include/nftables.h
> @@ -144,6 +144,9 @@ struct nft_ctx {
>  	void			*json_root;
>  	json_t			*json_echo;
>  	const char		*stdin_buf;
> +#if defined(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 000000000000..9152faaf7251
> --- /dev/null
> +++ b/src/afl++.c
> @@ -0,0 +1,825 @@
> +/*
> + * Copyright (c) Red Hat GmbH.	Author: Florian Westphal <fw@xxxxxxxxx>
> + *
> + * 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.
> + */
> +
> +#define _GNU_SOURCE
> +#include <stdio.h>
> +#include <stdlib.h>
> +
> +#include <errno.h>
> +#include <limits.h>
> +#include <fcntl.h>
> +#include <unistd.h>
> +#include <string.h>
> +#include <time.h>
> +
> +#include <sys/stat.h>
> +#include <sys/wait.h>
> +
> +#include <afl++.h>
> +#include <nftables.h>
> +
> +static const char nft_afl_hookdirname[] = "tests/afl++/hooks";
> +static const char nft_afl_workdirname[] = "tests/afl++/nft-afl-work";
> +static const char self_fault_inject_file[] = "/proc/self/make-it-fail";
> +
> +/* this lets the source compile without afl-clang-fast/lto */
> +#ifndef __AFL_FUZZ_TESTCASE_LEN
> +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)
> +
> +#else
> +__AFL_FUZZ_INIT();
> +/* this get passed via afl-cc, declares prototypes
> + * depending on the afl-cc flavor.
> + */

This comment seems out of place?

> +#endif
> +
> +#define NFT_AFL_GC_TIME		30
> +#define NFT_AFL_FB_SIZE	1024
> +#define NFT_AFL_FNAME_SIZE	(strlen(nft_afl_workdirname) + sizeof("/255/255"))
> +#define NFT_AFL_HDIR_SIZE	(strlen(nft_afl_hookdirname) + sizeof("/hooks/"))
> +
> +struct nft_afl_input {
> +	unsigned int failcount;
> +	char *buffer;
> +	char *fname;
> +	int retval;
> +	bool use_filename;
> +};
> +
> +/* we store 256 test cases in 256 dirs */
> +struct nft_afl_level {
> +	struct nft_afl_input inputs[256];
> +	uint8_t next;
> +};
> +
> +enum nft_afl_state_enum {
> +	NFT_AFL_STATE_NORMAL,
> +	NFT_AFL_STATE_TAINTED,
> +};
> +
> +struct nft_afl_state {
> +	unsigned int candidates;
> +	struct nft_afl_level level[256];
> +	uint8_t levelcount;
> +	enum nft_afl_fuzzer_stage stage_max;
> +	enum nft_afl_state_enum state;
> +	FILE *log;
> +	FILE *make_it_fail_fp;
> +};
> +
> +/*
> + * This is supposed to pre-select inputs that are more
> + * likely to cause a state change, i.e.
> + * "add chain", "table foo {", and so on.
> + */
> +struct nft_afl_tokens {
> +	const struct nft_afl_tokens *next;
> +	unsigned int next_count;
> +	const char *token;
> +};
> +
> +static struct nft_afl_state state;
> +
> +static bool precheck_token(const char *input, ssize_t len, const struct nft_afl_tokens *t,
> +			   unsigned int token_count)
> +{
> +	unsigned int i;
> +
> +	if (!t)
> +		return true;
> +
> +	if (len < 4)
> +		return false;
> +
> +	for (i = 0; i < token_count; i++, t++) {
> +		unsigned int token_len = strlen(t->token);
> +
> +		 /* discard entire input, rest of input too short */
> +		if (len < token_len)
> +			return false;
> +
> +		if ((strncmp(input, t->token, token_len)) == 0)
> +			return precheck_token(input + token_len, len - token_len, t->next, t->next_count);
> +	}
> +
> +	return false;
> +}
> +
> +static bool precheck_token_start(const char *input, ssize_t len)
> +{
> +	static const struct nft_afl_tokens last[] = {
> +		{ .token = " " },
> +	};
> +#define X(tok, obj) { .token = (tok), .next = (obj), .next_count = (sizeof(obj) / sizeof(obj[0])), }
> +	static const struct nft_afl_tokens table[] = {
> +		X(" arp", last),
> +		X(" bridge", last),
> +		X(" ip6", last),
> +		X(" ip", last),
> +		X(" ", last),
> +	};
> +	static const struct nft_afl_tokens add[] = {
> +		X(" table", last),
> +		X(" chain", last),
> +		X(" rule", last),
> +		X(" set", last),
> +		X(" map", last),
> +		X(" element", last),
> +	};
> +	static const struct nft_afl_tokens start[] = {
> +		X("insert", add),
> +		X("add", add),
> +		X("table", table),
> +		X("delete", table),
> +		X("destroy", table),
> +		X("flush", last),
> +	};
> +#undef X
> +	unsigned int i;
> +
> +	for (i = 0; i < sizeof(start) / sizeof(start[0]); i++) {

Use array_size()? (Also in X() macro above.)

> +		unsigned int token_len = strlen(start[i].token);
> +
> +		if (strncmp(input, start[i].token, token_len) == 0)
> +			return precheck_token(input + token_len, len - token_len,
> +					      start[i].next, start[i].next_count);
> +	}
> +
> +	return false;
> +}
> +
> +static char *preprocess(unsigned char *input, ssize_t len)
> +{
> +	/* shortest state-change command i could think of */
> +	static const ssize_t minlen = (ssize_t)sizeof("add set s");
> +	unsigned int i;
> +	char *real_in;
> +
> +	if (len < minlen)
> +		return NULL;
> +
> +	/* skip whitespace, and check minlen again.
> +	 * This is to speed things up by avoiding
> +	 * more costly libnftables parser for junk
> +	 * inputs.
> +	 */
> +	real_in = (char *)input;
> +	for (; len > 0; real_in++, len--) {
> +		switch (*real_in) {
> +		case ' ':
> +		case '\t':
> +		case '\n':
> +			break;
> +		case 0:
> +			/* pure whitespace? reject. */
> +			return NULL;
> +		default:
> +			/* non-whitespace, truncate at earliest opportunity */
> +			for (i = 0; i < len; i++) {
> +				uint8_t byte = real_in[i];
> +
> +				/* don't bother: parser rejects it. */
> +				if (byte >= 0x7f || byte == 0) {

Maybe using isascii(real_in[i]) allows to avoid the uint8_t "cast"?

> +					len = i;
> +					goto done;
> +				}
> +			}
> +
> +			/* no '\0' found in input. Truncate if possible. */
> +			if (i == len) {
> +				if (len <= minlen)
> +					return false;
> +				--len;
> +				real_in[len] = 0;
> +				goto done;
> +			}
> +		}
> +	}
> +done:
> +	if (len < minlen)
> +		return false;
> +
> +	if (!precheck_token_start(real_in, len))
> +		return NULL;
> +
> +	return real_in;
> +}
> +
> +static struct nft_afl_input *savebuf(struct nft_afl_level *level, const char *buf)
> +{
> +	struct nft_afl_input *input = &level->inputs[level->next];
> +	char *fast_buf = input->buffer;
> +	int r;
> +
> +	r = snprintf(fast_buf, NFT_AFL_FB_SIZE, "%s", buf);
> +	if (r > NFT_AFL_FB_SIZE) {

This should be '>=' according to snprintf(3).

> +		input->use_filename = true;
> +		fast_buf[0] = 0;
> +	} else {
> +		input->use_filename = false;
> +	}
> +
> +	return input;
> +}
> +
> +static int opencandidate(const char *name)
> +{
> +	return open(name, O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644);
> +}
> +
> +static void mark_tainted(struct nft_afl_state *s)
> +{
> +	char taintname[512];
> +	FILE *fp;
> +
> +	snprintf(taintname, sizeof(taintname), "%s/TAINT", nft_afl_workdirname);
> +
> +	/* mark it tainted, no more fuzzer runs until this file gets deleted. */
> +	fp = fopen(taintname, "w");
> +	if (fp)
> +		fclose(fp);
> +
> +	s->state = NFT_AFL_STATE_TAINTED;
> +}
> +
> +static void nft_afl_state_log_str(const struct nft_afl_state *s, const char *message)
> +{
> +	FILE *fp = fopen("/proc/uptime", "r");
> +	char buf[256];
> +	char *nl;
> +
> +	if (!fp)
> +		return;
> +
> +	if (!fgets(buf, sizeof(buf), fp))
> +		snprintf(buf, sizeof(buf), "error reading uptime: %s", strerror(errno));
> +
> +	fclose(fp);
> +
> +	nl = strchr(buf, '\n');
> +	if (nl)
> +		*nl = 0;
> +
> +	fprintf(s->log, "%s: %s\n", buf, message);
> +	fflush(s->log);
> +}
> +
> +__attribute__((format(printf, 2, 3)))
> +static void nft_afl_state_logf(const struct nft_afl_state *s, const char *fmt, ... )
> +{
> +	char buffer[512];
> +	va_list ap;
> +
> +	va_start(ap, fmt);
> +	vsnprintf(buffer, sizeof(buffer), fmt, ap);
> +	va_end(ap);
> +
> +	nft_afl_state_log_str(s, buffer);
> +}
> +
> +static bool kernel_is_tainted(const struct nft_afl_state *s)
> +{
> +	FILE *fp = fopen("/proc/sys/kernel/tainted", "r");
> +	unsigned int taint;
> +	bool ret = false;
> +
> +	if (fp && fscanf(fp, "%u", &taint) == 1 && taint) {
> +		nft_afl_state_logf(s, "Kernel is tainted: 0x%x\n", taint);
> +		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);
> +}
> +
> +/* NB: splats can happen even after return to userspace,
> + * or even when the kernel returned an error from
> + * nft_run_cmd_from_buffer/filename.
> + *
> + * This is due to delayed destruction in kernel
> + * and cleanup/error unwind semantics.
> + */
> +static void log_splat_seen(struct nft_afl_state *state, const struct nft_afl_input *r)
> +{
> +	nft_afl_state_logf(state, "tainted, last run: %s", r->fname);
> +
> +	mark_tainted(state);
> +}
> +
> +static bool nft_afl_run_cmd(struct nft_afl_state *s, struct nft_ctx *ctx, struct nft_afl_input *run)
> +{
> +	if (kernel_is_tainted(s))
> +		goto splat;
> +
> +	switch (s->stage_max) {
> +	case NFT_AFL_FUZZER_ALL:
> +		ctx->afl_ctx_stage = NFT_AFL_FUZZER_NETLINK_WR;
> +		break;
> +	case NFT_AFL_FUZZER_PARSER:
> +	case NFT_AFL_FUZZER_EVALUATION:
> +		return true;
> +	case NFT_AFL_FUZZER_NETLINK_RO:
> +	case NFT_AFL_FUZZER_NETLINK_WR:
> +		ctx->afl_ctx_stage = s->stage_max;
> +		break;
> +	}
> +
> +	fault_inject_enable(s);
> +
> +	if (run->use_filename)
> +		nft_run_cmd_from_filename(ctx, run->fname);
> +	else
> +		nft_run_cmd_from_buffer(ctx, run->buffer);
> +
> +	fault_inject_disable(s);
> +
> +	if (kernel_is_tainted(s))
> +		goto splat;
> +
> +	return true;
> +splat:
> +	log_splat_seen(s, run);
> +	return false;
> +}
> +
> +static struct nft_afl_input *
> +save_candidate(struct nft_afl_state *state, const char *buf)
> +{
> +	unsigned int next, lc = state->levelcount;
> +	char levelname[512], name[NFT_AFL_FNAME_SIZE];
> +	struct nft_afl_level *level;
> +	struct nft_afl_input *input;
> +	int len, err, fd;
> +	ssize_t rv;
> +
> +	/* no need to save in a file, kernel won't see any data */
> +	if (state->stage_max == NFT_AFL_FUZZER_NETLINK_RO) {
> +		level = &state->level[lc];
> +		next = level->next;
> +
> +		input = savebuf(level, buf);
> +
> +		if (input->use_filename)
> +			goto save_now;
> +
> +		return input;
> +	}
> +
> +	for (; lc <= UINT8_MAX; lc++) {

lc < array_size(state->level)

> +		level = &state->level[lc];
> +
> +		for (next = level->next; next <= UINT8_MAX; next++) {

next < array_size(level->inputs)

> +			input = &level->inputs[lc];

Uhm. Should this really use lc as index, not next?

> +
> +			if (!input->use_filename) {
> +				level->next = next;
> +				state->levelcount = lc;
> +				goto save_now;
> +			}
> +		}
> +	}
> +
> +save_now:
> +	snprintf(name, sizeof(name), "%s/%03d/%03d", nft_afl_workdirname, lc, next);
> +
> +	fd = opencandidate(name);
> +	if (fd >= 0)
> +		goto doit;
> +
> +	if (errno != ENOENT)
> +		return NULL;
> +
> +	snprintf(levelname, sizeof(levelname), "%s/%03d", nft_afl_workdirname, lc);
> +
> +	err = mkdir(levelname, 0755);
> +	if (err == 0) {
> +		fd = opencandidate(name);
> +		if (fd >= 0)
> +			goto doit;
> +		return NULL;
> +	}
> +
> +	err = mkdir(levelname, 0755);
> +	if (err == 0) {
> +		fd = opencandidate(name);
> +		if (fd < 0)
> +			return NULL;
> +	}

Why the double try to create the directory? Maybe reorder the code to call
mkdir() first and bail if errno && errno != EEXIST?

> +doit:
> +	input = savebuf(level, buf);
> +
> +	snprintf(input->fname, NFT_AFL_FNAME_SIZE, "%s", name);
> +
> +	len = strlen(buf);
> +
> +	rv = write(fd, buf, len);

So this sets input->fname to name and writes into the opened fd, but
what if savebuf() noticed buf fits into input->buffer and thus set
input->use_filename = false?

> +	if (rv != len) {
> +		close(fd);
> +		mark_tainted(state);
> +		nft_afl_state_logf(state, "%s: write %u bytes, got %ld: %s",
> +				   name, len, (long)rv, strerror(errno));
> +		return NULL;
> +	}
> +
> +	++next;
> +	level->next = next;
> +	if (next == UINT8_MAX) {
> +		lc++;
> +
> +		if (lc <= UINT8_MAX)
> +			state->levelcount = lc;
> +
> +	}
> +
> +	close(fd);
> +
> +	return input;
> +}
> +
> +static bool nft_afl_level_init(struct nft_afl_level *level)
> +{
> +	struct nft_afl_input *input;
> +	unsigned int i;
> +
> +	for (i = 0; i <= UINT8_MAX; i++) {

i < array_size(level->inputs)

> +		input = &level->inputs[i];
> +
> +		input->buffer = malloc(NFT_AFL_FB_SIZE);
> +		input->fname = malloc(NFT_AFL_FNAME_SIZE);
> +
> +		if (!input->buffer || !input->fname)
> +			return false;
> +	}
> +
> +	return true;
> +}
> +
> +static FILE *fault_inject_open(void)
> +{
> +	return fopen(self_fault_inject_file, "r+");
> +}
> +
> +static bool nft_afl_state_init(struct nft_afl_state *state, enum nft_afl_fuzzer_stage max)
> +{
> +	char logname[512];
> +	unsigned int i;
> +
> +	memset(state, 0, sizeof(*state));
> +
> +	for (i = 0; i <= UINT8_MAX; i++) {

i < array_size(state->level)

> +		if (!nft_afl_level_init(&state->level[i]))
> +			return false;
> +	}
> +
> +	snprintf(logname, sizeof(logname), "%s/log", nft_afl_workdirname);
> +
> +	state->log = fopen(logname, "a");
> +	if (!state->log)
> +		mkdir(nft_afl_workdirname, 0755);
> +
> +	state->log = fopen(logname, "a");
> +	if (!state->log) {
> +		fprintf(stderr, "open %s: %s\n", logname, strerror(errno));
> +		return false;
> +	}

Same here: Call mkdir() unconditionally, bail on errno && errno !=
EEXIST and then call fopen()?

> +
> +	state->stage_max = max;
> +	state->make_it_fail_fp = fault_inject_open();
> +	return true;
> +}
> +
> +static bool is_tainted(void)
> +{
> +	char taintname[512];
> +	FILE *fp;
> +
> +	snprintf(taintname, sizeof(taintname), "%s/TAINT", nft_afl_workdirname);
> +
> +	fp = fopen(taintname, "r");
> +	if (fp) {
> +		fclose(fp);
> +		sleep(60);	/* don't burn cycles, no more fuzzing */
> +		return true;
> +	}
> +
> +	return false;
> +}
> +
> +static void forkandexec(struct nft_afl_state *state, char *name)
> +{
> +	char * const argv[] = { name, NULL };
> +	int ret, wstatus;
> +	pid_t p = fork();
> +
> +	if (p < 0) {
> +		nft_afl_state_logf(state, "Cannot fork %s: %s", name, strerror(errno));
> +		return;
> +	}
> +
> +	if (p == 0) {
> +		execve(name, argv, environ);
> +		nft_afl_state_logf(state, "Cannot execve %s: %s", name, strerror(errno));
> +		exit(EXIT_FAILURE);
> +	}
> +
> +	ret = waitpid(p, &wstatus, 0);
> +	if (ret < 0) {
> +		nft_afl_state_logf(state, "waitpid %ld %s: %s", (long)p, name, strerror(errno));
> +		return;
> +	}
> +
> +	if (WIFEXITED(wstatus)) {
> +		if (WEXITSTATUS(wstatus) == 0)
> +			return;
> +
> +		nft_afl_state_logf(state, "waitpid %s: exited with %d", name, WEXITSTATUS(wstatus));
> +		return;
> +	}
> +
> +	if (WIFSIGNALED(wstatus)) {
> +		nft_afl_state_logf(state, "waitpid %s: exited with signal %d", name, WTERMSIG(wstatus));
> +		return;
> +	}
> +
> +	nft_afl_state_logf(state, "waitpid %s: exited with unexpected result", name);
> +}
> +
> +static void nft_exec_hookscript(struct nft_afl_state *state, const char *sname)
> +{
> +	char name[NFT_AFL_HDIR_SIZE + sizeof("post-commit")]; /* largest possible script name */
> +	struct stat sb;
> +	int ret;
> +
> +	snprintf(name, sizeof(name), "%s/%s", nft_afl_hookdirname, sname);
> +
> +	ret = lstat(name, &sb);
> +	if (ret != 0) {
> +		if (errno == ENOENT)
> +			return;
> +
> +		nft_afl_state_logf(state, "Cannot stat %s: %s", name, strerror(errno));
> +		return;
> +	}
> +
> +	if (sb.st_uid != 0 && geteuid() == 0) {
> +		nft_afl_state_logf(state, "ignore %s: owner != uid 0", name);
> +		return;
> +	}
> +
> +	if (sb.st_mode & 0002) {
> +		nft_afl_state_logf(state, "ignore %s: writeable by anyone", name);
> +		return;
> +	}
> +
> +	if ((sb.st_mode & 0700) == 0)
> +		return;
> +
> +	forkandexec(state, name);
> +}
> +
> +static bool nft_afl_read_existing_candidates(struct nft_afl_state *state,
> +					     struct nft_ctx *ctx)
> +{
> +	char name[NFT_AFL_FNAME_SIZE];
> +	time_t now = time(NULL);
> +	unsigned int found = 0;
> +	unsigned int gc = 0;
> +	int i, j;
> +
> +	nft_exec_hookscript(state, "pre-restart");
> +
> +	for (i = 0; i <= UINT8_MAX; i++) {

i < array_size(state->level)

> +		struct nft_afl_level *level = &state->level[i];
> +		struct stat sb;
> +		int ret;
> +
> +		snprintf(name, sizeof(name), "%s/%03d", nft_afl_workdirname, i);
> +		ret = lstat(name, &sb);
> +		if (ret != 0) {
> +			if (errno == ENOENT)
> +				continue;
> +
> +			nft_afl_state_logf(state, "stat dir %s: %s", name, strerror(errno));
> +			return false;
> +		}
> +
> +		if ((sb.st_mode & S_IFMT) != S_IFDIR) {
> +			nft_afl_state_logf(state, "stat dir %s: not a directory", name);
> +			return false;
> +		}
> +
> +		for (j = 0; j <= UINT8_MAX; j++) {

j < array_size(level->inputs)

> +			struct nft_afl_input *in = &level->inputs[i];
> +
> +			snprintf(name, sizeof(name), "%s/%03d/%03d", nft_afl_workdirname, i, j);
> +
> +			ret = lstat(name, &sb);
> +			if (ret == 0) {
> +				unsigned int next = j + 1;
> +
> +				if (next > 255)
> +					next = 0;

next is not used anymore later in this block?

> +
> +				if (sb.st_mtime + NFT_AFL_GC_TIME < now) {
> +					++gc;
> +					continue;
> +				}
> +
> +				if (sb.st_size > 0) {
> +					state->candidates++;
> +					in->use_filename = true;
> +					snprintf(in->buffer, NFT_AFL_FB_SIZE, "%s", name);
> +					found++;
> +					continue;
> +				}
> +
> +				continue;
> +			} else {
> +				if (errno == ENOENT)
> +					continue;
> +
> +				nft_afl_state_logf(state, "stat file %s: %s", name, strerror(errno));
> +				return false;
> +			}
> +		}
> +
> +		state->levelcount++;
> +	}
> +
> +	if (gc) {
> +		state->levelcount = 0;
> +		state->level[0].next = 0;
> +	}
> +
> +	nft_afl_state_logf(state, "start with %u existing test cases from %s/*/* (%u marked expired)", found, nft_afl_workdirname, gc);
> +
> +	nft_exec_hookscript(state, "post-restart");
> +
> +	return true;
> +}
> +
> +static bool nft_afl_switch_to_next_stage(const struct nft_afl_state *state, struct nft_ctx *ctx, enum nft_afl_fuzzer_stage next_stage)
> +{
> +	if (state->stage_max == 0 || next_stage <= state->stage_max) {
> +		ctx->afl_ctx_stage = next_stage;
> +		return true;
> +	}
> +
> +	return false;
> +}
> +
> +int nft_afl_init(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
> +	char *workdir = NULL;
> +	int ret;
> +
> +	nft_afl_print_build_info(stderr);
> +
> +	if (!nft_afl_state_init(&state, stage))
> +		return -1;
> +
> +	ret = asprintf(&workdir, "NFT_AFL_WORKDIR=%s", nft_afl_workdirname);
> +	if (ret < 0 || !workdir || putenv(workdir) != 0) {
> +		nft_afl_state_logf(&state, "Out of memory");
> +		return -1;
> +	}
> +
> +	nft_exec_hookscript(&state, "init");
> +
> +	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 hook scripts will cause fault injections,
> +		 * which is not what we want.
> +		 */
> +		fault_inject_disable(&state);
> +	}
> +
> +	nft_afl_state_logf(&state, "starting (%s, %s fault injection)", instrumented,
> +			   state.make_it_fail_fp ? "with" : "no");
> +	return 0;
> +}
> +
> +int nft_afl_main(struct nft_ctx *ctx)
> +{
> +	unsigned int new_additions = 0;
> +	bool last_run = false;
> +	unsigned char *buf;
> +	ssize_t len;
> +	int rc;
> +
> +	if (is_tainted()) {
> +		nft_afl_state_log_str(&state, "No more fuzzer runs: TAINT file exists");

Maybe give instructions here: "Reboot and remove /path/to/TAINT."

> +		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) {
> +			nft_afl_state_logf(&state, "Could not repoen %s: %s", self_fault_inject_file, strerror(errno));

s/repoen/reopen/

> +			return -1;
> +		}
> +		fclose(state.make_it_fail_fp);
> +		state.make_it_fail_fp = fp;
> +	}
> +
> +	rc = nft_afl_read_existing_candidates(&state, ctx);
> +	if (rc <= 0)
> +		return rc;
> +
> +	buf = __AFL_FUZZ_TESTCASE_BUF;
> +
> +	while (__AFL_LOOP(UINT_MAX)) {
> +		struct nft_afl_input *cmd_to_run;
> +		char *input;
> +
> +		len = __AFL_FUZZ_TESTCASE_LEN;  // do not use the macro directly in a call!
> +
> +		input = preprocess(buf, len);
> +		if (!input)
> +			continue;
> +
> +		/* buf has minimum length and is null terminated at this point */
> +		ctx->afl_ctx_stage = NFT_AFL_FUZZER_PARSER;
> +		rc = nft_run_cmd_from_buffer(ctx, input);
> +		if (rc < 0) /* rejected at parsing stage */
> +			continue;
> +
> +		if (!nft_afl_switch_to_next_stage(&state, ctx, NFT_AFL_FUZZER_EVALUATION))
> +			continue;
> +
> +		rc = nft_run_cmd_from_buffer(ctx, input);
> +		if (rc < 0) /* rejected at eval stage */
> +			continue;
> +
> +		cmd_to_run = save_candidate(&state, input);
> +		if (!cmd_to_run)
> +			break;
> +
> +		nft_exec_hookscript(&state, "pre-commit");
> +
> +		if (!nft_afl_run_cmd(&state, ctx, cmd_to_run))
> +			continue;
> +
> +		nft_exec_hookscript(&state, "post-commit");
> +
> +		if (++new_additions > 1024 || last_run)
> +			break;
> +
> +		last_run = state.levelcount == UINT8_MAX && state.level[UINT8_MAX].next;
> +	}
> +
> +	/* afl-fuzz will restart us. */
> +	return 0;
> +}
> diff --git a/src/libnftables.c b/src/libnftables.c
> index 0dee1bacb0db..b3b29135b3e8 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>
> @@ -36,6 +37,17 @@ static int nft_netlink(struct nft_ctx *nft,
>  	if (list_empty(cmds))
>  		goto out;
>  
> +#if defined(HAVE_FUZZER_BUILD)
> +	switch (nft->afl_ctx_stage) {
> +	case NFT_AFL_FUZZER_ALL:
> +	case NFT_AFL_FUZZER_NETLINK_RO:
> +	case NFT_AFL_FUZZER_NETLINK_WR:
> +		break;
> +	case NFT_AFL_FUZZER_PARSER:
> +	case NFT_AFL_FUZZER_EVALUATION:
> +		goto out;
> +	}
> +#endif
>  	batch_seqnum = mnl_batch_begin(ctx.batch, mnl_seqnum_alloc(&seqnum));
>  	list_for_each_entry(cmd, cmds, list) {
>  		ctx.seqnum = cmd->seqnum = mnl_seqnum_alloc(&seqnum);
> @@ -579,6 +591,20 @@ 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 defined(HAVE_FUZZER_BUILD)
> +	if (rc && nft->afl_ctx_stage == NFT_AFL_FUZZER_PARSER) {
> +		list_for_each_entry_safe(cmd, next, &cmds, list) {
> +			list_del(&cmd->list);
> +			cmd_free(cmd);
> +		}
> +		if (nft->scanner) {
> +			scanner_destroy(nft);
> +			nft->scanner = NULL;
> +		}
> +		free(nlbuf);
> +		return rc;
> +	}
> +#endif

You want to keep nft->cache and thus don't make this just 'goto err',
right?

>  	parser_rc = rc;
>  
>  	rc = nft_evaluate(nft, &msgs, &cmds);
> diff --git a/src/main.c b/src/main.c
> index 9485b193cd34..51bdf6deb86e 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,
> +#ifdef 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		= '?',
> +
> +#ifdef HAVE_FUZZER_BUILD
> +	/* keep last */
> +        OPT_FUZZER		= 0x254
> +#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"),
> +#ifdef HAVE_FUZZER_BUILD
> +	[IDX_FUZZER]	    = NFT_OPT("fuzzer",			OPT_FUZZER,		"stage",
> +				      "fuzzer stage to run (parser, eval, netlink-ro, netlink-write)"),
> +#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 defined(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-write",
> +		.stage		= NFT_AFL_FUZZER_NETLINK_WR,
> +	},
> +};
> +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;
> @@ -344,6 +392,10 @@ static bool nft_options_check(int argc, char * const argv[])
>  				   !strcmp(argv[i], "--debug") ||
>  				   !strcmp(argv[i], "--includepath") ||
>  				   !strcmp(argv[i], "--define") ||
> +				   !strcmp(argv[i], "--define") ||
> +#if defined(HAVE_FUZZER_BUILD)
> +				   !strcmp(argv[i], "--fuzzer") ||
> +#endif
>  				   !strcmp(argv[i], "--file")) {
>  				skip = true;
>  				continue;

If my series ([nft PATCH 0/2] Review nft_options_check()) is applied
before this one, the above change is not needed anymore.

> @@ -359,6 +411,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;
> @@ -501,6 +554,26 @@ int main(int argc, char * const *argv)
>  		case OPT_OPTIMIZE:
>  			nft_ctx_set_optimize(nft, 0x1);
>  			break;
> +#if defined(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;
>  		}
> @@ -513,6 +586,27 @@ int main(int argc, char * const *argv)
>  
>  	nft_ctx_output_set_flags(nft, output_flags);
>  
> +	if (fuzzer_stage) {
> +		if (filename || define || interactive)
> +			afl_exit("-D/--define, -f/--filename and -i/--interactive are incompatible options");
> +
> +		rc = nft_afl_init(fuzzer_stage);
> +		if (rc != 0)
> +			afl_exit("cannot initialize");
> +
> +		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/mnl.c b/src/mnl.c
> index 9e4bfcd9a030..57e14f376716 100644
> --- a/src/mnl.c
> +++ b/src/mnl.c
> @@ -27,6 +27,7 @@
>  #include <linux/netfilter/nfnetlink_hook.h>
>  #include <linux/netfilter/nf_tables.h>
>  
> +#include <afl++.h>
>  #include <mnl.h>
>  #include <cmd.h>
>  #include <net/if.h>
> @@ -428,6 +429,18 @@ int mnl_batch_talk(struct netlink_ctx *ctx, struct list_head *err_list,
>  
>  	mnl_set_rcvbuffer(ctx->nft->nf_sock, rcvbufsiz);
>  
> +#if defined(HAVE_FUZZER_BUILD)
> +	switch (ctx->nft->afl_ctx_stage) {
> +	case NFT_AFL_FUZZER_ALL:
> +	case NFT_AFL_FUZZER_NETLINK_WR:
> +		break;
> +	case NFT_AFL_FUZZER_NETLINK_RO:
> +	case NFT_AFL_FUZZER_PARSER:
> +	case NFT_AFL_FUZZER_EVALUATION:
> +		return 0;
> +	}
> +#endif

Wrapping mnl_batch_talk() in libnftables.c would restrict this beautiful
integration to that source (plus main.c) at least.

> +
>  	ret = mnl_nft_socket_sendmsg(ctx, &msg);
>  	if (ret == -1)
>  		return -1;
> diff --git a/tests/afl++/README b/tests/afl++/README
> new file mode 100644
> index 000000000000..d9e4f2df0453
> --- /dev/null
> +++ b/tests/afl++/README
> @@ -0,0 +1,117 @@
> +Running nft with afl++ fuzzer howto.
> +
> +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 ./configure --disable-shared --with-json --without-xtables --with-cli=readline --enable-fuzzer
> +
> +[ you might want to enable xtables or use a different cli, your choice ].
> +
> +Important options are:
> +--disable-shared, so that libnftables is instrumented and included in src/nft binary.
> +--enable-fuzzer
> +
> +--enable-fuzzer is not 100% needed but greatly improves execution speed.
> +This also adds a --fuzzer option that allows more fine-grained control
> +over what code paths should be excluded from the fuzzing process.
> +Most importantly, it collects the pseudo-generated rules/commands it sends
> +to the kernel and saves them in the 'nft-afl-work' directory for later
> +debugging.
> +
> +When fuzzing in this mode, then each new input passes through the following
> +processing stages:
> +
> +1: pre-validation. Checks the input buffer (the fuzzer generated
> +   ruleset/commands) to:
> +     - ensure minimum "ineresting" length
> +     - ensure a 0-terminated ruleset
> +     - ensure initial keywords would result in state
> +       change (ruleset load, deletion or addition of
> +       rules/chains/tables, ..
> +
> +  This stage is always executed.
> +
> +2: 'parser':
> +    Only run flex/bison parser and quit instantly if that fails,
> +    without evaluation of the generated (parial) AST: No need to

s/parial/partial/

> +    generate error messages for the user.
> +
> +3: 'eval': pass the input through the evaliation phase as well.

s/evaliation/evaluation/

> +    This attempts to build a complete ruleset in memory.
> +
> +4: 'netlink-ro' or 'netlink-write' (default).
> +    'netlink-ro' builds the netlink buffer to send to the kernel,
> +    without actually doing so.
> +    With 'write' variant, the buffer will be sent to the kernel.
> +
> +The argument to '--fuzzer' specifies the last stage to run.
> +Use 'netlink-ro' if you want to prevent the fuzzing from ever
> +submitting any changes to the kernel.
> +
> +The default is to attempt to submit all changes.
> +
> +You can use "--fuzzer netlink-write --check" to send the ruleset to the kernel,
> +but have the transaction be aborted before committing any changes.
> +
> +Note that "--fuzzer netlink-write --check" can trigger a kernel crash if
> +there are bugs in the kernels valiation / transaction / abort phases.

s/kernels/kernel's/

> +
> +Before sending a command to the kernel, the input is stored
> +in temporary files (named [0-255]/[0-255] in the "nft-afl-work"
> +directory.
> +
> +After each input, fuzzer mode will check the kernel ringbuffer for any BUG
> +or WARNING splat messages.
> +
> +If any is detected, it creates the file nft-afl-work/TAINT and will refuse

s/is/are/? Or maybe just "If found, ..."

> +further fuzzing until reboot and a manual removal of the TAINT file.
> +
> +To execute nftables with afl++, run nftables like this:
> +
> +unshare -n \
> +  afl-fuzz -g 16 -G 8192 -t 10000 -i tests/afl++/in -o tests/afl++/out -- src/nft --fuzzer <arg>
> +
> +arg should be either "netlink-ro" (if you only want to exercise nft userspace) or
> +"netlink-write" (if you want to test kernel code paths too).
> +
> +See nft --help for a list of valid stages to fuzz.
> +
> +You can use tests/afl++/run-afl.sh script to autogenerate some valid inputs based on the shell
> +test dump files.
> +
> +Use
> +
> +sysctl -f tests/afl++/afl-sysctl.cfg
> +
> +to enable some fuzzer-beneficial sysctl options.
> +
> +--fuzzer supports hook scripts, see the examples in tests/afl++/hooks/
> +Remove or copy the scripts and remove the '-sample' suffix.  Scripts need to be executable.

s/\.$/, root-owned and not writable by others./

Also, I don't see any detection of the '-sample' suffix and this patch
doesn't create such files.

> +
> +The environment variable NFT_AFL_WORKDIR will be set before running a hook script, it contains
> +the relative pathname to the nft-afl work directory.

General advise to limit lines to max 80 columns, even though it's "just
a README."

> +
> +Kernel config:
> +Best to use a debug kernel with at least
> +
> +# CONFIG_NOTIFIER_ERROR_INJECTION is not set
> +CONFIG_KASAN=y
> +CONFIG_KASAN_INLINE=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++/TODO b/tests/afl++/TODO
> new file mode 100644
> index 000000000000..d31ba71d2364
> --- /dev/null
> +++ b/tests/afl++/TODO
> @@ -0,0 +1,6 @@
> +Fuzzing is sometimes VERY slow, this happens when current inputs contain
> +"host names".  Either --fuzzer could set NFT_CTX_INPUT_NO_DNS to avoid this,
> +or nft needs a new command line option so both (dns and no dns) can be combined.

Overriding nsswitch.conf's hosts-entry would be neat, but seems not
supported by glibc.

> +
> +Investigate custom mutator for afl to further improve the fuzzing rate.
> +This would likely allow to get rid of the pre-validation in src/afl++.c.
> diff --git a/tests/afl++/afl-sysctl.cfg b/tests/afl++/afl-sysctl.cfg
> new file mode 100644
> index 000000000000..490e62b00094
> --- /dev/null
> +++ b/tests/afl++/afl-sysctl.cfg
> @@ -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++/hooks/init b/tests/afl++/hooks/init
> new file mode 100755
> index 000000000000..503257afa2f1
> --- /dev/null
> +++ b/tests/afl++/hooks/init

Is this supposed to run by default? I guess unless one clones the tree
as root, nft_exec_hookscript() will reject it because sb.st_uid != 0,
right? Not sure if the check is sensible given the circumstances.
Otherwise just document the need to chown in the README?

> @@ -0,0 +1,54 @@
> +#!/bin/bash
> +
> +# This script is run only once, when "nft --fuzzer" is started.
> +# It doesn't run again when afl-fuzz auto-restarts with a new
> +# input round, you need to CTRL-C and re-invoke afl.
> +
> +rm -f $NFT_AFL_WORKDIR/[0-255][0-255][0-255]/[0-255][0-255][0-255]
> +
> +# clean state
> +nft flush ruleset
> +
> +set_fails() {
> +	FAILTYPE=$1
> +
> +	test -d /sys/kernel/debug/$FAILTYPE/ || return
> +
> +	# We want failures when the fuzzed nft talks to kernel, this
> +	# pairs with the post-restart script which has to do
> +	#  echo 1 > /proc/$PPID/make-it-fail
> +	echo Y > /sys/kernel/debug/$FAILTYPE/task-filter
> +
> +	# 10% failure rate, this is a lot as nft transactions
> +	# allocate a lot of temporary transaction objects.
> +	echo 1 > /sys/kernel/debug/$FAILTYPE/probability
> +
> +	# interval where fault inject is suppressed
> +	echo 100 > /sys/kernel/debug/$FAILTYPE/interval
> +
> +	# unlimited failslab errors.
> +	echo -1 > /sys/kernel/debug/$FAILTYPE/times
> +
> +	# no initial budget, i.e. fails are allowed instantly
> +	echo 0 > /sys/kernel/debug/$FAILTYPE/space
> +
> +	# 0: no messages, 1: one line, 2: backtrace for each injected error
> +	echo 0 > /sys/kernel/debug/$FAILTYPE/verbose
> +
> +	# also fail GFP_KERNEL allocations.
> +	# Most allocations nf_tables control plane performs
> +	# can sleep. We want to maximise error path coverage so
> +	# we want allocation failures for those as well.
> +	echo N > /sys/kernel/debug/$FAILTYPE/ignore-gfp-wait
> +}
> +
> +set_fails "failslab"
> +set_fails "fail_page_alloc"
> +
> +# Enable fault injection on kernel side for all transactions.
> +# NB: nft --fuzzer will auto-disable this immediately and then
> +# re-enable it just for the final fuzzer stage, when data is sent
> +# to kernel.  Otherwise, even hook scripts and parser/evaluation
> +# stage fuzzing would be subject to fault injections.
> +nftpid=$PPID
> +test -f /proc/$nftpid/make-it-fail && echo 1 > /proc/$nftpid/make-it-fail
> diff --git a/tests/afl++/hooks/post-commit b/tests/afl++/hooks/post-commit
> new file mode 100755
> index 000000000000..be81ce6747b4
> --- /dev/null
> +++ b/tests/afl++/hooks/post-commit
> @@ -0,0 +1,8 @@
> +#!/bin/bash
> +
> +# This script is run right after "nft --fuzzer" has committed a
> +# fuzzer-generated ruleset to the kernel.
> +
> +test -f $NFT_AFL_WORKDIR/TAINT || exit 0
> +
> +exec nft list ruleset > $NFT_AFL_WORKDIR/post_taint_ruleset
> diff --git a/tests/afl++/hooks/post-restart b/tests/afl++/hooks/post-restart
> new file mode 100755
> index 000000000000..1848709a4ea1
> --- /dev/null
> +++ b/tests/afl++/hooks/post-restart
> @@ -0,0 +1,8 @@
> +#!/bin/bash
> +
> +# This script is run when afl-fuzz has re-started nft with a new fuzzer
> +# input round and after "nft --fuzzer" has read the existing input files
> +# from its workdir ($NFT_AFL_WORKDIR).
> +
> +# start each round with a clean kernel state
> +nft flush ruleset
> diff --git a/tests/afl++/hooks/pre-commit b/tests/afl++/hooks/pre-commit
> new file mode 100755
> index 000000000000..c89167475907
> --- /dev/null
> +++ b/tests/afl++/hooks/pre-commit
> @@ -0,0 +1,6 @@
> +#!/bin/bash
> +
> +# This script is run right before "nft --fuzzer" is going to submit a new
> +# fuzzer-generated ruleset to the kernel.
> +
> +nft list ruleset > $NFT_AFL_WORKDIR/last_precommit_ruleset
> diff --git a/tests/afl++/hooks/pre-restart b/tests/afl++/hooks/pre-restart
> new file mode 100755
> index 000000000000..4595dfb05889
> --- /dev/null
> +++ b/tests/afl++/hooks/pre-restart
> @@ -0,0 +1,12 @@
> +#!/bin/bash
> +
> +# This script is run when afl-fuzz has re-started nft with a new fuzzer
> +# input round and before "nft --fuzzer" has read the existing input files
> +# from its workdir ($NFT_AFL_WORKDIR).
> +
> +test -f /sys/kernel/debug/kmemleak && echo "scan" > /sys/kernel/debug/kmemleak &
> +
> +# house-cleaning: delete old inputs
> +find $NFT_AFL_WORKDIR/*/ -type f -mmin +15 -exec rm -f {} + &
> +
> +nft list ruleset > $NFT_AFL_WORKDIR/current_ruleset
> diff --git a/tests/afl++/run-afl.sh b/tests/afl++/run-afl.sh
> new file mode 100755
> index 000000000000..2651b0859be9
> --- /dev/null
> +++ b/tests/afl++/run-afl.sh
> @@ -0,0 +1,41 @@
> +#!/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 <<EOF
> +"\"quoted\""
> +"{"
> +"}"
> +"["
> +"]"
> +";"
> +":"
> +","
> +"."
> +"192.168.0.1"
> +"65535"
> +";;"
> +"\x09"
> +"\x0d\x0a"
> +"\x0a"
> +EOF
> +
> +for d in "tests/afl++/in/" "tests/afl++/out/" "tests/afl++/nft-afl-work/"; do
> +	test -d "$d" || mkdir "$d"
> +done
> +
> +find tests/shell/testcases -type f -name '*.nft' -exec install -m 644 {} "tests/afl++/in/" \;

This loses some due to clashing names:

| % find tests/shell/testcases \
| 	-type f -name '*.nft' -exec basename '{}' \; | \
| 	sort | uniq -c | grep -v '^ *1 '
|    2 0003table_0.nft
|    2 comments_0.nft
|    2 typeof_raw_0.nft

> +
> +echo "built initial set of inputs to fuzz from shell test case dump files."
> +echo "sample invocation:"
> +echo "unshare -n afl-fuzz -g 16 -G 8192 -t 10000 -i tests/afl++/in -o tests/afl++/out -- src/nft --fuzzer netlink-ro"

Cheers, Phil




[Index of Archives]     [Netfitler Users]     [Berkeley Packet Filter]     [LARTC]     [Bugtraq]     [Yosemite Forum]

  Powered by Linux