This patch includes the harness for the network filter driver. It fuzzes
one or more rules in the <filter> definition at a time. NWFilter protobuf
definitions are generated separately and linked to this fuzzer.
This patch also includes handling of some datatypes to fuzz certain
attributes: IPSet, TCPFlag, ARP opcode, state flags, etc.
Signed-off-by: Rayhan Faizel <rayhan.faizel(a)gmail.com>
---
scripts/relaxng-to-proto.py | 16 +++
tests/fuzz/meson.build | 34 ++++++
tests/fuzz/proto_custom_datatypes.cc | 88 +++++++++++++++
tests/fuzz/proto_header_common.h | 4 +
tests/fuzz/protos/meson.build | 9 ++
tests/fuzz/protos/xml_datatypes.proto | 21 ++++
tests/fuzz/protos/xml_nwfilter.proto | 9 ++
tests/fuzz/xml_nwfilter_fuzz.cc | 149 ++++++++++++++++++++++++++
8 files changed, 330 insertions(+)
create mode 100644 tests/fuzz/protos/xml_nwfilter.proto
create mode 100644 tests/fuzz/xml_nwfilter_fuzz.cc
diff --git a/scripts/relaxng-to-proto.py b/scripts/relaxng-to-proto.py
index f13d6f7e40..9c1203ff1b 100644
--- a/scripts/relaxng-to-proto.py
+++ b/scripts/relaxng-to-proto.py
@@ -51,6 +51,22 @@ custom_ref_table = {
"irq": {"type": "uint32"},
"iobase": {"type": "uint32"},
"uniMacAddr": {"type": "MacAddr"},
+
+ # NWFilter types
+
+ "addrIP": {"type": "IPAddr"},
+ "addrIPv6": {"type": "IPAddr"},
+ "addrMAC": {"type": "MacAddr"},
+ "uint16range": {"type": "uint32"},
+ "uint32range": {"type": "uint32"},
+ "sixbitrange": {"type": "uint32"},
+ "stateflags-type": {"type":
"StateFlags"},
+ "tcpflags-type": {"type": "TCPFlags"},
+ "ipset-flags-type": {"type":
"IPSetFlags"},
+ "arpOpcodeType": {"types": ["uint32",
"DummyString"],
+ "values": ["reply",
"request", "reply_reverse", "request_reverse",
+ "DRARP_reply",
"DRARP_request", "DRARP_error", "INARP_request",
+ "ARP_NAK"]},
}
net_model_names = ["virtio", "virtio-transitional",
"virtio-non-transitional", "e1000", "e1000e",
"igb",
diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build
index 417b8dc1ef..2e796b5726 100644
--- a/tests/fuzz/meson.build
+++ b/tests/fuzz/meson.build
@@ -31,6 +31,23 @@ fuzz_autogen_xml_domain_dep = declare_dependency(
]
)
+fuzz_autogen_xml_nwfilter_lib = static_library(
+ 'fuzz_autogen_xml_nwfilter_lib',
+ [
+ autogen_xml_nwfilter_src,
+ xml_datatypes_proto_src,
+ 'proto_custom_datatypes.cc',
+ ],
+ dependencies: [ fuzz_dep ],
+)
+
+fuzz_autogen_xml_nwfilter_dep = declare_dependency(
+ link_whole: [ fuzz_autogen_xml_nwfilter_lib ],
+ include_directories: [
+ fuzz_autogen_xml_nwfilter_lib.private_dir_include(),
+ ]
+)
+
if conf.has('WITH_QEMU')
fuzzer_src = [
'qemu_xml_domain_fuzz.cc',
@@ -108,6 +125,23 @@ if conf.has('WITH_LIBXL')
]
endif
+if conf.has('WITH_NWFILTER')
+ fuzzer_src = [
+ 'xml_nwfilter_fuzz.cc',
+ 'proto_to_xml.cc',
+ ]
+
+ nwfilter_libs = [
+ test_utils_lib,
+ libvirt_lib,
+ nwfilter_driver_impl,
+ ]
+
+ xml_fuzzers += [
+ { 'name': 'xml_nwfilter_fuzz', 'src': [ fuzzer_src,
xml_nwfilter_proto_src ], 'libs': nwfilter_libs, 'macro':
'-DXML_NWFILTER', 'deps': [ fuzz_autogen_xml_nwfilter_dep ] },
+ ]
+endif
+
foreach fuzzer: xml_fuzzers
xml_domain_fuzz = executable(fuzzer['name'],
fuzzer['src'],
diff --git a/tests/fuzz/proto_custom_datatypes.cc b/tests/fuzz/proto_custom_datatypes.cc
index d89a6d4f59..a4a54c0116 100644
--- a/tests/fuzz/proto_custom_datatypes.cc
+++ b/tests/fuzz/proto_custom_datatypes.cc
@@ -87,6 +87,29 @@ std::string convertIPAddr(const Message &message) {
}
+static
+std::string convertIPSetFlags(const Message &message)
+{
+ std::string value = "";
+ const libvirt::IPSetFlags &ipset_flags = (libvirt::IPSetFlags &) message;
+
+ uint32_t max_count = ipset_flags.max_count() % 7;
+ uint32_t bitmap = ipset_flags.bitarray() & 0x1f;
+
+ for (size_t i = 0; i < max_count; i++) {
+ if ((bitmap >> i) & 1)
+ value += "src,";
+ else
+ value += "dst,";
+ }
+
+ if (value != "")
+ value.pop_back();
+
+ return value;
+}
+
+
static
std::string convertMacAddr(const Message &message) {
char value[64] = {0};
@@ -104,6 +127,34 @@ std::string convertMacAddr(const Message &message) {
}
+static
+std::string convertStateFlags(const Message &message)
+{
+ std::string value = "";
+ const libvirt::StateFlags &state_flags = (libvirt::StateFlags &) message;
+
+ if (state_flags.newflag())
+ value += "NEW,";
+
+ if (state_flags.established())
+ value += "ESTABLISHED,";
+
+ if (state_flags.related())
+ value += "RELATED,";
+
+ if (state_flags.invalid())
+ value += "INVALID,";
+
+ if (value == "")
+ return "NONE";
+
+ /* Remove trailing comma */
+ value.pop_back();
+
+ return value;
+}
+
+
static
std::string convertDiskTarget(const Message &message)
{
@@ -118,12 +169,49 @@ std::string convertDiskTarget(const Message &message)
}
+static
+std::string convertTCPFlags(const Message &message)
+{
+ std::string value = "";
+ const libvirt::TCPFlags &tcp_flags = (libvirt::TCPFlags &) message;
+
+ if (tcp_flags.syn())
+ value += "SYN,";
+
+ if (tcp_flags.ack())
+ value += "ACK,";
+
+ if (tcp_flags.urg())
+ value += "URG,";
+
+ if (tcp_flags.psh())
+ value += "PSH,";
+
+ if (tcp_flags.fin())
+ value += "FIN,";
+
+ if (tcp_flags.rst())
+ value += "RST,";
+
+ if (value == "")
+ return "NONE";
+
+ /* Remove trailing comma */
+ value.pop_back();
+
+ return value;
+}
+
+
std::unordered_map<std::string, typeHandlerPtr> type_handler_table = {
{"libvirt.CPUSet", convertCPUSet},
{"libvirt.EmulatorString", convertEmulatorString},
{"libvirt.IPAddr", convertIPAddr},
+ {"libvirt.IPSetFlags", convertIPSetFlags},
{"libvirt.MacAddr", convertMacAddr},
+ {"libvirt.StateFlags", convertStateFlags},
{"libvirt.TargetDev", convertDiskTarget},
+ {"libvirt.TCPFlags", convertTCPFlags},
};
diff --git a/tests/fuzz/proto_header_common.h b/tests/fuzz/proto_header_common.h
index 3f135c48e1..4e4beb787b 100644
--- a/tests/fuzz/proto_header_common.h
+++ b/tests/fuzz/proto_header_common.h
@@ -39,6 +39,10 @@
#include "xml_hotplug.pb.h"
#endif
+#ifdef XML_NWFILTER
+#include "xml_nwfilter.pb.h"
+#endif
+
#define FUZZ_COMMON_INIT(...) \
if (virErrorInitialize() < 0) \
diff --git a/tests/fuzz/protos/meson.build b/tests/fuzz/protos/meson.build
index 0731ef1eca..df276aee8b 100644
--- a/tests/fuzz/protos/meson.build
+++ b/tests/fuzz/protos/meson.build
@@ -4,6 +4,7 @@ protos = [
'xml_domain_disk_only.proto',
'xml_domain_interface_only.proto',
'xml_hotplug.proto',
+ 'xml_nwfilter.proto',
]
autogen_proto_xml_domain_proto = custom_target('autogen_xml_domain.proto',
@@ -12,6 +13,12 @@ autogen_proto_xml_domain_proto =
custom_target('autogen_xml_domain.proto',
command : [relaxng_to_proto_prog, '@INPUT@', '@OUTPUT@'],
)
+autogen_proto_xml_nwfilter_proto = custom_target('autogen_xml_nwfilter.proto',
+ output : 'autogen_xml_nwfilter.proto',
+ input : meson.project_source_root() / 'src' / 'conf' /
'schemas' / 'nwfilter.rng',
+ command : [relaxng_to_proto_prog, '@INPUT@', '@OUTPUT@'],
+)
+
protoc_generator = generator(protoc_prog,
output: [
'@BASENAME@.pb.cc',
@@ -25,10 +32,12 @@ protoc_generator = generator(protoc_prog,
],
depends: [
autogen_proto_xml_domain_proto,
+ autogen_proto_xml_nwfilter_proto,
],
)
autogen_xml_domain_proto_src = protoc_generator.process(autogen_proto_xml_domain_proto)
+autogen_xml_nwfilter_src = protoc_generator.process(autogen_proto_xml_nwfilter_proto)
foreach proto: protos
proto_src_name = proto.split('.')[0].underscorify()
diff --git a/tests/fuzz/protos/xml_datatypes.proto
b/tests/fuzz/protos/xml_datatypes.proto
index 1229b9810f..7bf19051cd 100644
--- a/tests/fuzz/protos/xml_datatypes.proto
+++ b/tests/fuzz/protos/xml_datatypes.proto
@@ -70,3 +70,24 @@ message CPUSet {
}
message EmulatorString {}
+
+message TCPFlags {
+ required bool syn = 1;
+ required bool ack = 2;
+ required bool urg = 3;
+ required bool psh = 4;
+ required bool fin = 5;
+ required bool rst = 6;
+}
+
+message StateFlags {
+ required bool newflag = 1;
+ required bool established = 2;
+ required bool related = 3;
+ required bool invalid = 4;
+}
+
+message IPSetFlags {
+ required uint32 max_count = 1;
+ required uint32 bitarray = 2;
+}
diff --git a/tests/fuzz/protos/xml_nwfilter.proto b/tests/fuzz/protos/xml_nwfilter.proto
new file mode 100644
index 0000000000..459a10f840
--- /dev/null
+++ b/tests/fuzz/protos/xml_nwfilter.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+
+import "autogen_xml_nwfilter.proto";
+
+package libvirt;
+
+message MainObj {
+ required filterTag T_filter = 1;
+}
diff --git a/tests/fuzz/xml_nwfilter_fuzz.cc b/tests/fuzz/xml_nwfilter_fuzz.cc
new file mode 100644
index 0000000000..a2c25a38eb
--- /dev/null
+++ b/tests/fuzz/xml_nwfilter_fuzz.cc
@@ -0,0 +1,149 @@
+/*
+ * xml_nwfilter_fuzz.cc: NWFilter fuzzing harness
+ *
+ * Copyright (C) 2024 Rayhan Faizel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see
+ * <
http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include "proto_header_common.h"
+
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+
+extern "C" {
+#include "testutils.h"
+#include "nwfilter/nwfilter_ebiptables_driver.h"
+#include "virbuffer.h"
+
+#define LIBVIRT_VIRCOMMANDPRIV_H_ALLOW
+#include "vircommandpriv.h"
+}
+
+#include "port/protobuf.h"
+#include "proto_to_xml.h"
+#include "src/libfuzzer/libfuzzer_macro.h"
+
+bool enable_xml_dump = false;
+
+uint64_t parse_pass = 0;
+uint64_t apply_rules_pass = 0;
+uint64_t success = 0;
+
+static int
+fuzzNWFilterDefToRules(virNWFilterDef *def)
+{
+ size_t i;
+ virNWFilterRuleDef *rule;
+ virNWFilterRuleInst *ruleinst;
+
+ virNWFilterRuleInst **ruleinsts = NULL;
+ size_t nrules = 0;
+
+ g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
+ g_autoptr(virCommandDryRunToken) dryRunToken = virCommandDryRunTokenNew();
+
+ int ret = -1;
+
+ /* This line is needed to avoid actually running iptables/ebtables */
+ virCommandSetDryRun(dryRunToken, &buf, true, true, NULL, NULL);
+
+ for (i = 0; i < (size_t) def->nentries; i++) {
+ /* We handle only <rule> elements. <filterref> is ignored */
+ if (!(rule = def->filterEntries[i]->rule))
+ continue;
+
+ ruleinst = g_new0(virNWFilterRuleInst, 1);
+
+ ruleinst->chainSuffix = def->chainsuffix;
+ ruleinst->chainPriority = def->chainPriority;
+ ruleinst->def = rule;
+ ruleinst->priority = rule->priority;
+ ruleinst->vars = virHashNew(virNWFilterVarValueHashFree);
+
+ VIR_APPEND_ELEMENT(ruleinsts, nrules, ruleinst);
+ }
+
+
+ if (ebiptables_driver.applyNewRules("vnet0", ruleinsts, nrules) < 0)
+ goto cleanup;
+
+ ret = 0;
+
+ cleanup:
+ for (i = 0; i < nrules; i++) {
+ g_clear_pointer(&ruleinsts[i]->vars, g_hash_table_unref);
+ g_free(ruleinsts[i]);
+ ruleinsts[i] = NULL;
+ }
+
+ if (nrules != 0)
+ g_free(ruleinsts);
+
+ return ret;
+}
+
+
+static void
+fuzzNWFilterXML(const char *xml)
+{
+ virNWFilterDef *def = NULL;
+
+ parse_pass++;
+ if (!(def = virNWFilterDefParse(xml, NULL, 0)))
+ goto cleanup;
+
+ apply_rules_pass++;
+
+ if (fuzzNWFilterDefToRules(def) < 0)
+ goto cleanup;
+
+ success++;
+
+ cleanup:
+ virNWFilterDefFree(def);
+}
+
+
+DEFINE_PROTO_FUZZER(const libvirt::MainObj &message)
+{
+ static bool initialized = false;
+ static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT");
+
+ std::string xml = "";
+
+ if (!initialized) {
+ FUZZ_COMMON_INIT();
+
+ /* Enable printing of XML to stdout (useful for debugging crashes) */
+ if (dump_xml_env && STREQ(dump_xml_env, "YES"))
+ enable_xml_dump = true;
+
+ initialized = true;
+ }
+
+ convertProtoToXML(message, xml);
+
+ if (enable_xml_dump)
+ printf("%s\n", xml.c_str());
+
+ fuzzNWFilterXML(xml.c_str());
+
+ if (parse_pass % 1000 == 0)
+ printf("[FUZZ METRICS] Parse: %lu, Apply Rules: %lu, Success: %lu\n",
+ parse_pass, apply_rules_pass, success);
+}
--
2.34.1