summaryrefslogtreecommitdiffstats
path: root/src/fuzz.c
diff options
context:
space:
mode:
authorPablo Neira Ayuso <pablo@netfilter.org>2025-06-04 23:12:30 +0200
committerPablo Neira Ayuso <pablo@netfilter.org>2025-06-05 00:03:59 +0200
commit009cabe369b71cf5452286fd338eae382718789a (patch)
tree9f14eb626cee252ddf79d34cda9b98b57f5a9454 /src/fuzz.c
initial commitHEADmaster
knft is a tool to improve test coverage for the low-level nftables kernel API by providing a relatively simple way to define a transaction batch with nftables objects without having to mingle with netlink. A set of tests 612 test (.t) files are included. knft also provides a rudimentary deterministic fuzzer (via -f option) along with several fuzzing modes that mangle existing tests in different ways to improve coverage for error unwinding paths: deltable \ delbasechain \ delchain \ delrule | - delete object in this batch delset / delelem / delobj / flushset - flush set dup - duplicate object reverse-commit - turn commit into abort reverse-abort - turn abort into commit table-dormant - inject table dormant flag table-wakeup - inject table wake-up flag swap - swap objects bogus - inject bogus object to make the transaction fail To inspect how the selected fuzzing mode mangles the test, you can use the -d option to enable debugging along with -c to run it in dry-run mode, eg. # src/./knft -c -f deltable -d tests/expr/meta/03-mark_ok.t tests/expr/meta/03-mark_ok.t... [FUZZING] tests/expr/meta/03-mark_ok.t (deltable) >>>> fuzz_loop at index 0 in state=0 add_table(NFPROTO_IPV4, "test", NULL, NULL, NULL); del_table(NFPROTO_IPV4, "test", NULL); add_chain("test", NULL, NULL, NULL, NULL); add_rule("test", "0x1", NULL, NULL, NULL); meta(NULL, "NFT_REG32_15", "3"); cmp("NFT_REG32_15", "0", "ffffffff"); commit(); <<<< fuzz_loop backtrack STACK limit reached ==== still more tries at index 0 in state=0 add_table(NFPROTO_IPV4, "test", NULL, NULL, NULL); add_chain("test", NULL, NULL, NULL, NULL); del_table(NFPROTO_IPV4, "test", NULL); add_rule("test", "0x1", NULL, NULL, NULL); meta(NULL, "NFT_REG32_15", "3"); cmp("NFT_REG32_15", "0", "ffffffff"); commit(); <<<< fuzz_loop backtrack STACK limit reached ... knft provides a few more options: -e to display the error reported by the kernel. -n to perform test runs without flushing the existing ruleset. This tool requires libmnl to build and to parse the netlink messages that are sent and received by the kernel. This tool is released under the GPLv2 (or any later). This project is funded through the NGI0 Entrust established by NLnet (https://nlnet.nl) with support from the European Commission's Next Generation Internet programme.
Diffstat (limited to 'src/fuzz.c')
-rw-r--r--src/fuzz.c541
1 files changed, 541 insertions, 0 deletions
diff --git a/src/fuzz.c b/src/fuzz.c
new file mode 100644
index 0000000..28da976
--- /dev/null
+++ b/src/fuzz.c
@@ -0,0 +1,541 @@
+/*
+ * Copyright (c) 2024-2025 Pablo Neira Ayuso <pablo@netfilter.org>
+ *
+ * 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.
+ */
+
+/* Funded through the NGI0 Entrust established by NLnet (https://nlnet.nl)
+ * with support from the European Commission's Next Generation Internet
+ * programme.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/time.h>
+
+#include "fuzz.h"
+
+static const char *fuzz_type_name[] = {
+ [FUZZ_DEL_TABLE] = "deltable",
+ [FUZZ_DEL_BASECHAIN] = "delbasechain",
+ [FUZZ_DEL_CHAIN] = "delchain",
+ [FUZZ_DEL_RULE] = "delrule",
+ [FUZZ_DEL_SET] = "delset",
+ [FUZZ_FLUSH_SET] = "flushset",
+ [FUZZ_DEL_ELEM] = "delelem",
+ [FUZZ_DEL_OBJ] = "delobj",
+ [FUZZ_DUP] = "dup",
+ [FUZZ_REVERSE_COMMIT] = "reverse-commit",
+ [FUZZ_REVERSE_ABORT] = "reverse-abort",
+ [FUZZ_TABLE_DORMANT] = "table-dormant",
+ [FUZZ_TABLE_WAKEUP] = "table-wakeup",
+ [FUZZ_SWAP] = "swap",
+ [FUZZ_BOGUS] = "bogus",
+};
+
+void print_fuzz_modes(void)
+{
+ int i;
+
+ for (i = 0; i < FUZZ_MAX; i++)
+ printf("\t%s\n", fuzz_type_name[i]);
+}
+
+const char *fuzz_type_to_name(uint32_t type)
+{
+ if (type >= FUZZ_MAX)
+ return "unknown";
+
+ return fuzz_type_name[type];
+}
+
+int fuzz_type_to_value(const char *str)
+{
+ int i;
+
+ for (i = 0; i < FUZZ_MAX; i++) {
+ if (!strcmp(fuzz_type_name[i], str))
+ return i;
+ }
+
+ return -1;
+}
+
+struct line_args {
+ char n1[1024];
+ char n2[1024];
+ char n3[1024];
+ char n4[1024];
+ char n5[1024];
+ char n6[1024];
+ char n7[1024];
+ char n8[1024];
+ char n9[1024];
+ char n10[1024];
+ char n11[1024];
+ char n12[1024];
+ char n13[1024];
+ char n14[1024];
+ char n15[1024];
+};
+
+#define BUF_SIZE 1024000
+
+struct fuzz_state_ctx {
+ char buf[1024000];
+ uint32_t len;
+
+ char saved_line[1024];
+
+ uint32_t state;
+ uint32_t line;
+ uint32_t line_from;
+
+ struct line_args saved_args;
+
+ uint32_t more;
+ bool found;
+ bool found2;
+ bool done;
+};
+
+static void add_line(struct fuzz_state_ctx *ctx, const char *line)
+{
+ snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "%s", line);
+ ctx->len += strlen(line);
+}
+
+static void reverse_cmd(struct fuzz_state_ctx *ctx, const char *line, int type)
+{
+ struct line_args *args = &ctx->saved_args;
+ int ret;
+
+ add_line(ctx, line);
+
+ if (!strcmp(line, "commit();\n") ||
+ !strcmp(line, "abort();\n"))
+ return;
+
+ if (ctx->done) {
+ ctx->more++;
+ return;
+ }
+
+ switch (type) {
+ case FUZZ_DEL_TABLE:
+ case FUZZ_TABLE_DORMANT:
+ case FUZZ_TABLE_WAKEUP:
+ if (sscanf(line, "add_table(%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5) == 5) {
+ ctx->found2 = 1;
+ }
+ break;
+ case FUZZ_DEL_BASECHAIN:
+ if (sscanf(line, "add_basechain(%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6, args->n7, args->n8, args->n9, args->n10, args->n11, args->n12, args->n13) == 13) {
+ ctx->found2 = 1;
+ }
+ break;
+ case FUZZ_DEL_CHAIN:
+ if (sscanf(line, "add_chain(%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5) == 5) {
+ ctx->found2 = 1;
+ }
+ break;
+ case FUZZ_DEL_RULE:
+ if (sscanf(line, "add_rule(%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5) == 5) {
+ ctx->found2 = 1;
+ }
+ break;
+ case FUZZ_DEL_SET:
+ case FUZZ_FLUSH_SET:
+ if (sscanf(line, "add_set(%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6, args->n7, args->n8, args->n9, args->n10, args->n11, args->n12, args->n13, args->n14, args->n15) == 15) {
+ ctx->found2 = 1;
+ }
+ break;
+ case FUZZ_DEL_OBJ:
+ if (sscanf(line, "add_obj_counter(%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5) == 5) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_quota(%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6) == 6) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_connlimit(%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5) == 5) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_secmark(%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4) == 4) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_synproxy(%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6) == 6) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_ct_timeout(%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6) == 6) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_ct_helper(%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6) == 6) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_ct_expect(%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6, args->n7, args->n8) == 8) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_limit(%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6, args->n7, args->n8) == 8) {
+ ctx->found2 = 1;
+ } else if (sscanf(line, "add_obj_tunnel(%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args->n1, args->n2, args->n3, args->n4, args->n5, args->n6, args->n7, args->n8, args->n9, args->n10, args->n11, args->n12, args->n13, args->n14) == 14) {
+ ctx->found2 = 1;
+ }
+ break;
+ }
+
+ if (!ctx->found2)
+ return;
+
+ switch (type) {
+ case FUZZ_DEL_TABLE:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_table(%s,%s, NULL);\n", args->n1, args->n2);
+ break;
+ case FUZZ_DEL_BASECHAIN:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_basechain(%s,%s, NULL, NULL, NULL, NULL, NULL, NULL);\n", args->n1, args->n2);
+ break;
+ case FUZZ_DEL_CHAIN:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_chain(%s);\n", args->n1);
+ break;
+ case FUZZ_DEL_RULE:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_rule(%s,%s, NULL);\n", args->n1, args->n2);
+ break;
+ case FUZZ_DEL_SET:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_set(%s, NULL);\n", args->n1);
+ break;
+ case FUZZ_FLUSH_SET:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "flush_set(%s, NULL);\n", args->n1);
+ break;
+ case FUZZ_TABLE_DORMANT:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "add_table(%s,%s, \"0x1\",%s,%s);\n", args->n1, args->n2, args->n4, args->n5);
+ break;
+ case FUZZ_TABLE_WAKEUP:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "add_table(%s,%s, \"0x0\",%s,%s);\n", args->n1, args->n2, args->n4, args->n5);
+ break;
+ case FUZZ_DEL_OBJ:
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_obj_unknown(%s, NULL, NULL, \"1\");\n", args->n1);
+ break;
+ }
+ ctx->line_from = ctx->line;
+ ctx->done = 1;
+}
+
+static void del_elem(struct fuzz_state_ctx *ctx, const char *line)
+{
+ struct line_args args = {};
+
+ add_line(ctx, line);
+
+ if (sscanf(line, "add_elem(%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^)]);",
+ args.n1, args.n2, args.n3, args.n4, args.n5, args.n6, args.n7, args.n8, args.n9) != 9)
+ return;
+
+ if (ctx->found) {
+ ctx->more++;
+ return;
+ }
+
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_elem(%s, NULL, NULL);\n", args.n1);
+ ctx->line_from = ctx->line;
+ ctx->found = 1;
+}
+
+static void dup_line(struct fuzz_state_ctx *ctx, const char *line)
+{
+ add_line(ctx, line);
+
+ if (!strcmp(line, "commit();\n") ||
+ !strcmp(line, "abort();\n"))
+ return;
+
+ if (ctx->found) {
+ ctx->more++;
+ return;
+ }
+
+ snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "%s", line);
+ ctx->len += strlen(line);
+
+ ctx->line_from = ctx->line;
+ ctx->found = 1;
+}
+
+static void reverse_abort(struct fuzz_state_ctx *ctx, const char *line)
+{
+ if (strcmp(line, "abort();\n")) {
+ add_line(ctx, line);
+ return;
+ }
+
+ if (ctx->found) {
+ add_line(ctx, line);
+ ctx->more++;
+ return;
+ }
+
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "commit();\n");
+ ctx->line_from = ctx->line;
+ ctx->found = 1;
+}
+
+static void reverse_commit(struct fuzz_state_ctx *ctx, const char *line)
+{
+ if (strcmp(line, "commit();\n")) {
+ add_line(ctx, line);
+ return;
+ }
+
+ if (ctx->found) {
+ add_line(ctx, line);
+ ctx->more++;
+ return;
+ }
+
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "abort();\n");
+ ctx->line_from = ctx->line;
+ ctx->found = 1;
+}
+
+static void swap(struct fuzz_state_ctx *ctx, const char *line)
+{
+ if (!strcmp(line, "commit();\n") ||
+ !strcmp(line, "abort();\n")) {
+ if (ctx->done) {
+ if (!ctx->found) {
+ add_line(ctx, ctx->saved_line);
+ ctx->found = 1;
+ }
+ }
+ add_line(ctx, line);
+ return;
+ }
+
+ if (!strncmp(line, "add_table(", strlen("add_table(")) ||
+ !strncmp(line, "set_table(", strlen("set_table("))) {
+ add_line(ctx, line);
+ return;
+ }
+
+ if (ctx->done) {
+ add_line(ctx, line);
+ if (!ctx->found) {
+ add_line(ctx, ctx->saved_line);
+ ctx->found = 1;
+ }
+ ctx->more++;
+ return;
+ }
+
+ snprintf(ctx->saved_line, sizeof(ctx->saved_line), "%s", line);
+ ctx->line_from = ctx->line;
+ ctx->done = 1;
+}
+
+static void bogus(struct fuzz_state_ctx *ctx, const char *line)
+{
+ struct line_args args = {};
+
+ add_line(ctx, line);
+
+ if (!strcmp(line, "commit();\n") ||
+ !strcmp(line, "abort();\n"))
+ return;
+
+ if (ctx->done) {
+ ctx->more++;
+ return;
+ }
+
+ ctx->len += snprintf(&ctx->buf[ctx->len], BUF_SIZE - ctx->len, "del_table(NFPROTO_IPV4, \"bogus123456unknown\", NULL);\n");
+ ctx->line_from = ctx->line;
+ ctx->done = 1;
+}
+
+static void fuzz_step_cb(struct fuzz_state_ctx *ctx, const char *line)
+{
+ if (ctx->line++ < ctx->line_from) {
+ add_line(ctx, line);
+ return;
+ }
+
+ switch (ctx->state) {
+ case FUZZ_DEL_TABLE:
+ case FUZZ_DEL_BASECHAIN:
+ case FUZZ_DEL_CHAIN:
+ case FUZZ_DEL_RULE:
+ case FUZZ_DEL_SET:
+ case FUZZ_FLUSH_SET:
+ case FUZZ_DEL_OBJ:
+ case FUZZ_TABLE_DORMANT:
+ case FUZZ_TABLE_WAKEUP:
+ reverse_cmd(ctx, line, ctx->state);
+ break;
+ case FUZZ_DEL_ELEM:
+ del_elem(ctx, line);
+ break;
+ case FUZZ_DUP:
+ dup_line(ctx, line);
+ break;
+ case FUZZ_REVERSE_COMMIT:
+ reverse_commit(ctx, line);
+ break;
+ case FUZZ_REVERSE_ABORT:
+ reverse_abort(ctx, line);
+ break;
+ case FUZZ_SWAP:
+ swap(ctx, line);
+ break;
+ case FUZZ_BOGUS:
+ bogus(ctx, line);
+ break;
+ }
+}
+
+static FILE *fuzz_step(struct fuzz_state_ctx *ctx, FILE *fp,
+ void (*fuzz_step_cb)(struct fuzz_state_ctx *ctx, const char *line))
+{
+ const char *line;
+ char buf[1024];
+ FILE *new_fp;
+
+ ctx->line = 0;
+ ctx->len = 0;
+ ctx->more = 0;
+ ctx->found = 0;
+ ctx->done = 0;
+
+ line = fgets(buf, sizeof(buf), fp);
+ while (line) {
+ fuzz_step_cb(ctx, line);
+ line = fgets(buf, sizeof(buf), fp);
+ }
+
+ new_fp = fmemopen(ctx->buf, ctx->len, "r");
+ if (!new_fp) {
+ fprintf(stderr, "cannot open memfile\n");
+ return NULL;
+ }
+
+ return new_fp;
+}
+
+static int fuzz_print(FILE *fp)
+{
+ const char *line;
+ char buf[1024];
+ FILE *fp2;
+
+ line = fgets(buf, sizeof(buf), fp);
+ while (line) {
+ fprintf(stdout, "%s", line);
+ line = fgets(buf, sizeof(buf), fp);
+ }
+ rewind(fp);
+
+ return 0;
+}
+
+int fuzz_loop(struct fuzz_ctx *ctx, FILE *fp, int (*fuzz_cb)(FILE *fp))
+{
+ struct fuzz_state_ctx state_ctx = {
+ .state = ctx->type[ctx->index],
+ };
+ struct timeval tv_cur, tv_diff;
+ FILE *new_fp;
+ int ret = 0;
+
+ if (ctx->index >= ctx->num_types) {
+ if (ctx->debug)
+ printf("<<<< fuzz_loop backtrack STACK limit reached\n");
+
+ ctx->index--;
+ return 0;
+ }
+
+ if (ctx->debug) {
+ printf(">>>> fuzz_loop at index %d in state=%x\n",
+ ctx->index, ctx->type[ctx->index]);
+ }
+
+ while (1) {
+ new_fp = fuzz_step(&state_ctx, fp, fuzz_step_cb);
+ if (!new_fp)
+ break;
+
+ ctx->fuzz_steps++;
+ ctx->total_fuzz_steps++;
+ if (ctx->fuzz_steps % 100 == 0) {
+ gettimeofday(&tv_cur, NULL);
+ timersub(&tv_cur, &ctx->tv_start, &tv_diff);
+ printf("... fuzz steps: %u in %u.%u seconds\n",
+ ctx->fuzz_steps, tv_diff.tv_sec, tv_diff.tv_usec);
+ }
+
+ if (ctx->debug)
+ fuzz_print(new_fp);
+
+ if (fuzz_cb(new_fp) < 0) {
+ ret = -1;
+ fclose(new_fp);
+ break;
+ }
+
+ if (ctx->index < ctx->num_types) {
+ ctx->index++;
+ if (fuzz_loop(ctx, new_fp, fuzz_cb) < 0) {
+ ret = -1;
+ fclose(new_fp);
+ break;
+ }
+ }
+
+ fclose(new_fp);
+
+ if (!state_ctx.more) {
+ if (ctx->debug) {
+ printf("<<<< fuzz_loop backtrack NO MORE at index %u in state=%x\n",
+ ctx->index, ctx->type[ctx->index]);
+ }
+ if (ctx->index > 0)
+ ctx->index--;
+ break;
+ } else {
+ if (ctx->debug) {
+ printf("==== still more tries at index %u in state=%x\n",
+ ctx->index, ctx->type[ctx->index]);
+ }
+ }
+
+ rewind(fp);
+ }
+
+ return ret;
+}
+
+int fuzz(struct fuzz_ctx *ctx, FILE *fp, int (*fuzz_cb)(FILE *fp))
+{
+ struct timeval tv_cur;
+ int ret;
+
+ gettimeofday(&ctx->tv_start, NULL);
+ ctx->fuzz_steps = 0;
+
+ ret = fuzz_loop(ctx, fp, fuzz_cb);
+
+ gettimeofday(&tv_cur, NULL);
+ timersub(&tv_cur, &ctx->tv_start, &ctx->tv_delta);
+
+ return ret;
+}