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@xxxxxxxxx> --- 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@xxxxxx', @@ -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