diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 872ed6de12d4..6f31b9343f5d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,2 +1,6 @@
 ADD_SUBDIRECTORY(cram)
 ADD_SUBDIRECTORY(shunit2)
+
+IF(CMAKE_C_COMPILER_ID STREQUAL "Clang")
+  ADD_SUBDIRECTORY(fuzz)
+ENDIF()
diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt
new file mode 100644
index 000000000000..1533c46ca971
--- /dev/null
+++ b/tests/fuzz/CMakeLists.txt
@@ -0,0 +1,18 @@
+FILE(GLOB test_cases "test-*.c")
+
+MACRO(ADD_FUZZER_TEST name)
+  ADD_EXECUTABLE(${name} ${name}.c)
+  TARGET_COMPILE_OPTIONS(${name} PRIVATE -g -O1 -fno-omit-frame-pointer -fsanitize=fuzzer,address,leak,undefined)
+  TARGET_INCLUDE_DIRECTORIES(${name} PRIVATE ${PROJECT_SOURCE_DIR})
+  TARGET_LINK_OPTIONS(${name} PRIVATE -stdlib=libc++ -fsanitize=fuzzer,address,leak,undefined)
+  TARGET_LINK_LIBRARIES(${name} uci)
+  ADD_TEST(
+    NAME ${name}
+	COMMAND ${name} -max_len=256 -timeout=10 -max_total_time=300 -dict=${CMAKE_CURRENT_SOURCE_DIR}/dict/uci.dict ${CMAKE_CURRENT_SOURCE_DIR}/corpus
+  )
+ENDMACRO(ADD_FUZZER_TEST)
+
+FOREACH(test_case ${test_cases})
+  GET_FILENAME_COMPONENT(test_case ${test_case} NAME_WE)
+  ADD_FUZZER_TEST(${test_case})
+ENDFOREACH(test_case)
diff --git a/tests/fuzz/corpus/231ee80a172b8e1749b9d91867989d88e4faf7bb b/tests/fuzz/corpus/231ee80a172b8e1749b9d91867989d88e4faf7bb
new file mode 100644
index 0000000000000000000000000000000000000000..9c17457fb85abbcc5b28d861981937b3b2dc0fb2
GIT binary patch
diff --git a/tests/fuzz/corpus/26a6253fc1eb695b61a2fc7640ee4c03c19e438e b/tests/fuzz/corpus/26a6253fc1eb695b61a2fc7640ee4c03c19e438e
new file mode 100644
index 0000000000000000000000000000000000000000..4e71cf69915767a7092e79800fabf341a74fbce3
GIT binary patch
diff --git a/tests/fuzz/corpus/29a6e206439d792afba5e8e9c1fdf55e65a1145d b/tests/fuzz/corpus/29a6e206439d792afba5e8e9c1fdf55e65a1145d
new file mode 100644
index 0000000000000000000000000000000000000000..f065bd7508aa513425a67a22a7e39e3e5fbfea4c
GIT binary patch
diff --git a/tests/fuzz/corpus/51045ac5401085f5727c6d3c1cac5f8cc32a2927 b/tests/fuzz/corpus/51045ac5401085f5727c6d3c1cac5f8cc32a2927
new file mode 100644
index 000000000000..f47ebca01bc0
--- /dev/null
+++ b/tests/fuzz/corpus/51045ac5401085f5727c6d3c1cac5f8cc32a2927
@@ -0,0 +1 @@
+ #
\ No newline at end of file
diff --git a/tests/fuzz/corpus/845dcf3f15f3c28235e6be148a690e7f03b07f65 b/tests/fuzz/corpus/845dcf3f15f3c28235e6be148a690e7f03b07f65
new file mode 100644
index 0000000000000000000000000000000000000000..cf5c1b00e262ac0b3fcf9c8c7ae43099d4f8488e
GIT binary patch
diff --git a/tests/fuzz/corpus/bb589d0621e5472f470fa3425a234c74b1e202e8 b/tests/fuzz/corpus/bb589d0621e5472f470fa3425a234c74b1e202e8
new file mode 100644
index 000000000000..ad2823b48f78
--- /dev/null
+++ b/tests/fuzz/corpus/bb589d0621e5472f470fa3425a234c74b1e202e8
@@ -0,0 +1 @@
+'
\ No newline at end of file
diff --git a/tests/fuzz/corpus/ea387894a296772f96706df8b999a52d9334c746 b/tests/fuzz/corpus/ea387894a296772f96706df8b999a52d9334c746
new file mode 100644
index 0000000000000000000000000000000000000000..2a72a978d4f0beaee9eb9d174e591243272d7413
GIT binary patch
diff --git a/tests/fuzz/corpus/id:000000,sig:11,src:000001,op:flip1,pos:24 b/tests/fuzz/corpus/id:000000,sig:11,src:000001,op:flip1,pos:24
new file mode 100644
index 0000000000000000000000000000000000000000..3f4e40d09a685f9641cc1e1ebea1f264ab1080dc
GIT binary patch
diff --git a/tests/fuzz/corpus/id:000008,sig:11,src:000022,op:arith8,pos:42,val:+26 b/tests/fuzz/corpus/id:000008,sig:11,src:000022,op:arith8,pos:42,val:+26
new file mode 100644
index 0000000000000000000000000000000000000000..20bc48d514b1ad3d25ee34221f2de2586c77c205
GIT binary patch
diff --git a/tests/fuzz/dict/uci.dict b/tests/fuzz/dict/uci.dict
new file mode 100644
index 000000000000..73c86ecbe7c1
--- /dev/null
+++ b/tests/fuzz/dict/uci.dict
@@ -0,0 +1,18 @@
+"c"
+"config"
+"p"
+"package"
+"o"
+"option"
+"l"
+"list"
+"'"
+"\""
+" "
+"\x00"
+"\x0a"
+"\x0d"
+"\x09"
+"#"
+";"
+"\\"
diff --git a/tests/fuzz/inputs/dhcp b/tests/fuzz/inputs/dhcp
new file mode 100644
index 000000000000..2bc0d312207d
--- /dev/null
+++ b/tests/fuzz/inputs/dhcp
@@ -0,0 +1,49 @@
+
+config dnsmasq
+	option domainneeded '1'
+	option boguspriv '1'
+	option filterwin2k '0'
+	option localise_queries '1'
+	option rebind_protection '1'
+	option rebind_localhost '1'
+	option local '/lan/'
+	option domain 'lan'
+	option expandhosts '1'
+	option nonegcache '0'
+	option authoritative '1'
+	option readethers '1'
+	option leasefile '/tmp/dhcp.leases'
+	option resolvfile '/tmp/resolv.conf.d/resolv.conf.auto'
+	option nonwildcard '1'
+	option localservice '1'
+
+config dhcp 'lan'
+	option interface 'lan'
+	option start '100'
+	option limit '150'
+	option leasetime '12h'
+	option dhcpv6 'server'
+	option ra 'server'
+	option ra_slaac '1'
+	list ra_flags 'managed-config'
+	list ra_flags 'other-config'
+
+config dhcp 'wan'
+	option interface 'wan'
+	option ignore '1'
+
+config odhcpd 'odhcpd'
+	option maindhcp '0'
+	option leasefile '/tmp/hosts/odhcpd'
+	option leasetrigger '/usr/sbin/odhcpd-update'
+	option loglevel '4'
+
+config host
+        option name 'foo'
+        option ip '192.168.1.90'
+        option mac '20:de:ad:be:ef:99'
+
+config host
+        option name 'moo-ap01'
+        option ip '192.168.1.2'
+        option mac '00:dd:8d:f7:e6:6f'
diff --git a/tests/fuzz/inputs/firewall b/tests/fuzz/inputs/firewall
new file mode 100644
index 000000000000..5e22f984ce9f
--- /dev/null
+++ b/tests/fuzz/inputs/firewall
@@ -0,0 +1,208 @@
+config defaults
+	option syn_flood	1
+	option input		ACCEPT
+	option output		ACCEPT
+	option forward		REJECT
+# Uncomment this line to disable ipv6 rules
+#	option disable_ipv6	1
+
+config zone
+	option name		lan
+	list   network		'lan'
+	option input		ACCEPT
+	option output		ACCEPT
+	option forward		ACCEPT
+
+config zone
+	option name		wan
+	list   network		'wan'
+	list   network		'wan6'
+	option input		REJECT
+	option output		ACCEPT
+	option forward		REJECT
+	option masq		1
+	option mtu_fix		1
+
+config forwarding
+	option src		lan
+	option dest		wan
+
+# We need to accept udp packets on port 68,
+# see https://dev.openwrt.org/ticket/4108
+config rule
+	option name		Allow-DHCP-Renew
+	option src		wan
+	option proto		udp
+	option dest_port	68
+	option target		ACCEPT
+	option family		ipv4
+
+# Allow IPv4 ping
+config rule
+	option name		Allow-Ping
+	option src		wan
+	option proto		icmp
+	option icmp_type	echo-request
+	option family		ipv4
+	option target		ACCEPT
+
+config rule
+	option name		Allow-IGMP
+	option src		wan
+	option proto		igmp
+	option family		ipv4
+	option target		ACCEPT
+
+# Allow DHCPv6 replies
+# see https://dev.openwrt.org/ticket/10381
+config rule
+	option name		Allow-DHCPv6
+	option src		wan
+	option proto		udp
+	option src_ip		fc00::/6
+	option dest_ip		fc00::/6
+	option dest_port	546
+	option family		ipv6
+	option target		ACCEPT
+
+config rule
+	option name		Allow-MLD
+	option src		wan
+	option proto		icmp
+	option src_ip		fe80::/10
+	list icmp_type		'130/0'
+	list icmp_type		'131/0'
+	list icmp_type		'132/0'
+	list icmp_type		'143/0'
+	option family		ipv6
+	option target		ACCEPT
+
+# Allow essential incoming IPv6 ICMP traffic
+config rule
+	option name		Allow-ICMPv6-Input
+	option src		wan
+	option proto	icmp
+	list icmp_type		echo-request
+	list icmp_type		echo-reply
+	list icmp_type		destination-unreachable
+	list icmp_type		packet-too-big
+	list icmp_type		time-exceeded
+	list icmp_type		bad-header
+	list icmp_type		unknown-header-type
+	list icmp_type		router-solicitation
+	list icmp_type		neighbour-solicitation
+	list icmp_type		router-advertisement
+	list icmp_type		neighbour-advertisement
+	option limit		1000/sec
+	option family		ipv6
+	option target		ACCEPT
+
+# Allow essential forwarded IPv6 ICMP traffic
+config rule
+	option name		Allow-ICMPv6-Forward
+	option src		wan
+	option dest		*
+	option proto		icmp
+	list icmp_type		echo-request
+	list icmp_type		echo-reply
+	list icmp_type		destination-unreachable
+	list icmp_type		packet-too-big
+	list icmp_type		time-exceeded
+	list icmp_type		bad-header
+	list icmp_type		unknown-header-type
+	option limit		1000/sec
+	option family		ipv6
+	option target		ACCEPT
+
+config rule
+	option name		Allow-IPSec-ESP
+	option src		wan
+	option dest		lan
+	option proto		esp
+	option target		ACCEPT
+
+config rule
+	option name		Allow-ISAKMP
+	option src		wan
+	option dest		lan
+	option dest_port	500
+	option proto		udp
+	option target		ACCEPT
+
+# allow interoperability with traceroute classic
+# note that traceroute uses a fixed port range, and depends on getting
+# back ICMP Unreachables.  if we're operating in DROP mode, it won't
+# work so we explicitly REJECT packets on these ports.
+config rule
+	option name		Support-UDP-Traceroute
+	option src		wan
+	option dest_port	33434:33689
+	option proto		udp
+	option family		ipv4
+	option target		REJECT
+	option enabled		false
+
+# include a file with users custom iptables rules
+config include
+	option path /etc/firewall.user
+
+
+### EXAMPLE CONFIG SECTIONS
+# do not allow a specific ip to access wan
+#config rule
+#	option src		lan
+#	option src_ip	192.168.45.2
+#	option dest		wan
+#	option proto	tcp
+#	option target	REJECT
+
+# block a specific mac on wan
+#config rule
+#	option dest		wan
+#	option src_mac	00:11:22:33:44:66
+#	option target	REJECT
+
+# block incoming ICMP traffic on a zone
+#config rule
+#	option src		lan
+#	option proto	ICMP
+#	option target	DROP
+
+# port redirect port coming in on wan to lan
+#config redirect
+#	option src			wan
+#	option src_dport	80
+#	option dest			lan
+#	option dest_ip		192.168.16.235
+#	option dest_port	80
+#	option proto		tcp
+
+# port redirect of remapped ssh port (22001) on wan
+#config redirect
+#	option src		wan
+#	option src_dport	22001
+#	option dest		lan
+#	option dest_port	22
+#	option proto		tcp
+
+### FULL CONFIG SECTIONS
+#config rule
+#	option src		lan
+#	option src_ip	192.168.45.2
+#	option src_mac	00:11:22:33:44:55
+#	option src_port	80
+#	option dest		wan
+#	option dest_ip	194.25.2.129
+#	option dest_port	120
+#	option proto	tcp
+#	option target	REJECT
+
+#config redirect
+#	option src		lan
+#	option src_ip	192.168.45.2
+#	option src_mac	00:11:22:33:44:55
+#	option src_port		1024
+#	option src_dport	80
+#	option dest_ip	194.25.2.129
+#	option dest_port	120
+#	option proto	tcp
diff --git a/tests/fuzz/test-fuzz.c b/tests/fuzz/test-fuzz.c
new file mode 100644
index 000000000000..9946a5786ce1
--- /dev/null
+++ b/tests/fuzz/test-fuzz.c
@@ -0,0 +1,60 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <string.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "uci.h"
+
+static void fuzz_uci_import(const uint8_t *input, size_t size)
+{
+	int r;
+	int fd;
+	FILE *fs = NULL;
+	struct uci_context *ctx = NULL;
+	struct uci_package *package = NULL;
+
+	fd = open("/dev/shm", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR);
+	if (fd < 0) {
+		perror("unable to create temp file");
+		exit(-1);
+	}
+
+	r = write(fd, input, size);
+	if (r < 0) {
+		perror("unable to write()");
+		exit(-1);
+	}
+
+	fs = fdopen(fd, "r");
+	if (fs == NULL) {
+		perror("unable to fdopen()");
+		exit(-1);
+	}
+
+	fseek(fs, 0L, SEEK_SET);
+
+	ctx = uci_alloc_context();
+	if (ctx == NULL) {
+		perror("unable to uci_alloc_context()");
+		exit(-1);
+	}
+
+	uci_import(ctx, fs, NULL, &package, false);
+	uci_free_context(ctx);
+	close(fd);
+	fclose(fs);
+}
+
+int LLVMFuzzerTestOneInput(const uint8_t *input, size_t size)
+{
+	fuzz_uci_import(input, size);
+	return 0;
+}
