[PATCH 13/14] fuzz: Implement NWFilter XML fuzzer

Rayhan Faizel posted 14 patches 3 months ago
[PATCH 13/14] fuzz: Implement NWFilter XML fuzzer
Posted by Rayhan Faizel 3 months ago
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@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