[PATCH 00/14] Introduce fuzzing of XML formats

This series introduces multiple fuzzers developed as part of Google Summer of Code 2024. We adopt a structure-aware fuzzing approach to fuzz libvirt XML formats. The fuzzing methodology makes use of libFuzzer and libprotobuf-mutator. The fuzzers work by mutating intermediate protobufs and converting them to XML. The fuzzing method in use requires inclusion of C++ sources. However, C++ compilation will be done only if '-Dfuzz' is enabled. Otherwise, libvirt will compile normally as before. The fuzzing method works only on clang compilers which support libFuzzer. This series introduces a total of six fuzzers: 1. QEMU XML domain 2. QEMU XML hotplug 3. CH XML domain 4. VMX XML domain 5. libXL XML domain 6. NWFilter XML In terms of the number of crashes discovered, QEMU XML domain, QEMU XML hotplug and libXL fuzzers are the most interesting ones. The setup process is documented at the end of the series (patch 14). Rayhan Faizel (14): src: Tweak source code to allow C++ compilation meson: Add support for clang/LLVM coverage instrumentation tests: Export handlers for fake secondary drivers schemas: Refactor relaxNG schema to ease protobuf conversion scripts: Add script to convert relaxNG to protobuf fuzz: Implement base fuzzing setup for XML domain fuzz: Implement QEMU XML domain fuzzer fuzz: Implement QEMU XML hotplug fuzzer ch: Remove unused variables fuzz: Implement CH XML domain fuzzer fuzz: Implement VMX XML domain fuzzer fuzz: Implement libXL XML domain fuzzer fuzz: Implement NWFilter XML fuzzer docs: Document the fuzzers build-aux/syntax-check.mk | 1 + docs/kbase/index.rst | 3 + docs/kbase/internals/meson.build | 1 + docs/kbase/internals/xml-fuzzing.rst | 120 ++++ meson.build | 55 ++ meson_options.txt | 5 +- scripts/meson.build | 1 + scripts/relaxng-to-proto.py | 521 ++++++++++++++++++ src/ch/ch_monitor.c | 2 +- src/ch/ch_monitor.h | 3 + src/ch/ch_process.c | 2 - src/conf/domain_conf.c | 18 +- src/conf/domain_conf.h | 6 +- src/conf/netdev_vport_profile_conf.c | 2 +- src/conf/schemas/basictypes.rng | 20 +- src/conf/schemas/domaincommon.rng | 11 +- src/conf/schemas/networkcommon.rng | 14 +- src/qemu/qemu_hotplug.c | 4 + src/qemu/qemu_monitor.c | 6 +- src/qemu/qemu_monitor.h | 2 +- src/util/virfile.h | 2 +- src/util/virnetdev.h | 12 +- src/util/virnetdevip.h | 2 +- src/util/virnetdevmacvlan.h | 2 +- src/util/virnetdevvportprofile.c | 2 +- src/util/virnetdevvportprofile.h | 2 +- src/util/virnvme.c | 4 +- src/util/virnvme.h | 2 +- src/util/viruuid.h | 2 +- tests/commandhelper.c | 8 +- tests/fuzz/README.rst | 131 +++++ tests/fuzz/ch_xml_domain_fuzz.cc | 157 ++++++ tests/fuzz/libxl_xml_domain_fuzz.cc | 159 ++++++ tests/fuzz/llvm_symbolizer_wrapper.c | 11 + tests/fuzz/meson.build | 183 ++++++ tests/fuzz/proto_custom_datatypes.cc | 234 ++++++++ tests/fuzz/proto_custom_datatypes.h | 30 + tests/fuzz/proto_header_common.h | 51 ++ tests/fuzz/proto_to_xml.cc | 277 ++++++++++ tests/fuzz/proto_to_xml.h | 39 ++ tests/fuzz/protos/meson.build | 46 ++ tests/fuzz/protos/xml_datatypes.proto | 93 ++++ tests/fuzz/protos/xml_domain.proto | 62 +++ tests/fuzz/protos/xml_domain_disk_only.proto | 21 + .../protos/xml_domain_interface_only.proto | 21 + tests/fuzz/protos/xml_hotplug.proto | 38 ++ tests/fuzz/protos/xml_nwfilter.proto | 9 + tests/fuzz/qemu_xml_domain_fuzz.cc | 277 ++++++++++ tests/fuzz/qemu_xml_hotplug_fuzz.cc | 340 ++++++++++++ tests/fuzz/run_fuzz.in | 142 +++++ tests/fuzz/vmx_xml_domain_fuzz.cc | 208 +++++++ tests/fuzz/xml_nwfilter_fuzz.cc | 149 +++++ tests/meson.build | 5 + tests/qemumonitortestutils.c | 48 ++ tests/qemumonitortestutils.h | 6 + tests/qemuxmlconftest.c | 249 --------- tests/testutilsqemu.c | 256 +++++++++ tests/testutilsqemu.h | 57 ++ 58 files changed, 3832 insertions(+), 302 deletions(-) create mode 100644 docs/kbase/internals/xml-fuzzing.rst create mode 100644 scripts/relaxng-to-proto.py create mode 100644 tests/fuzz/README.rst create mode 100644 tests/fuzz/ch_xml_domain_fuzz.cc create mode 100644 tests/fuzz/libxl_xml_domain_fuzz.cc create mode 100644 tests/fuzz/llvm_symbolizer_wrapper.c create mode 100644 tests/fuzz/meson.build create mode 100644 tests/fuzz/proto_custom_datatypes.cc create mode 100644 tests/fuzz/proto_custom_datatypes.h create mode 100644 tests/fuzz/proto_header_common.h create mode 100644 tests/fuzz/proto_to_xml.cc create mode 100644 tests/fuzz/proto_to_xml.h create mode 100644 tests/fuzz/protos/meson.build create mode 100644 tests/fuzz/protos/xml_datatypes.proto create mode 100644 tests/fuzz/protos/xml_domain.proto create mode 100644 tests/fuzz/protos/xml_domain_disk_only.proto create mode 100644 tests/fuzz/protos/xml_domain_interface_only.proto create mode 100644 tests/fuzz/protos/xml_hotplug.proto create mode 100644 tests/fuzz/protos/xml_nwfilter.proto create mode 100644 tests/fuzz/qemu_xml_domain_fuzz.cc create mode 100644 tests/fuzz/qemu_xml_hotplug_fuzz.cc create mode 100644 tests/fuzz/run_fuzz.in create mode 100644 tests/fuzz/vmx_xml_domain_fuzz.cc create mode 100644 tests/fuzz/xml_nwfilter_fuzz.cc -- 2.34.1

Since the fuzzers require C++ and will include various libvirt headers, some minor fixups will be needed. 1. 'this' and 'namespace' are reserved C++ keywords so replace them. 2. There is an enum and struct sharing the same name virNetDevVPortProfile. This is not allowed under C++. 3. G_NO_INLINE works differently under C++. Some compile errors occur because of this. To work around this, we rearrange G_NO_INLINE to allow compilation while allowing libvirt to compile normally as before without fuzzing enabled. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- src/conf/domain_conf.c | 18 +++++++++--------- src/conf/domain_conf.h | 4 ++-- src/conf/netdev_vport_profile_conf.c | 2 +- src/qemu/qemu_monitor.c | 6 +++--- src/qemu/qemu_monitor.h | 2 +- src/util/virfile.h | 2 +- src/util/virnetdev.h | 12 ++++++------ src/util/virnetdevip.h | 2 +- src/util/virnetdevmacvlan.h | 2 +- src/util/virnetdevvportprofile.c | 2 +- src/util/virnetdevvportprofile.h | 2 +- src/util/virnvme.c | 4 ++-- src/util/virnvme.h | 2 +- src/util/viruuid.h | 2 +- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c index d950921667..61418c4bc2 100644 --- a/src/conf/domain_conf.c +++ b/src/conf/domain_conf.c @@ -29863,15 +29863,15 @@ virDomainAudioBackendIsEqual(virDomainAudioDef *this, bool -virDomainAudioIsEqual(virDomainAudioDef *this, - virDomainAudioDef *that) -{ - return this->type == that->type && - this->id == that->id && - this->timerPeriod == that->timerPeriod && - virDomainAudioIOCommonIsEqual(&this->input, &that->input) && - virDomainAudioIOCommonIsEqual(&this->output, &that->output) && - virDomainAudioBackendIsEqual(this, that); +virDomainAudioIsEqual(virDomainAudioDef *first, + virDomainAudioDef *second) +{ + return first->type == second->type && + first->id == second->id && + first->timerPeriod == second->timerPeriod && + virDomainAudioIOCommonIsEqual(&first->input, &second->input) && + virDomainAudioIOCommonIsEqual(&first->output, &second->output) && + virDomainAudioBackendIsEqual(first, second); } diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h index eae621f900..3e97dd6293 100644 --- a/src/conf/domain_conf.h +++ b/src/conf/domain_conf.h @@ -4346,8 +4346,8 @@ virDomainSoundModelSupportsCodecs(virDomainSoundDef *def); bool virDomainAudioIOCommonIsSet(virDomainAudioIOCommon *common); bool -virDomainAudioIsEqual(virDomainAudioDef *this, - virDomainAudioDef *that); +virDomainAudioIsEqual(virDomainAudioDef *first, + virDomainAudioDef *second); const char *virDomainChrSourceDefGetPath(virDomainChrSourceDef *chr); diff --git a/src/conf/netdev_vport_profile_conf.c b/src/conf/netdev_vport_profile_conf.c index 032a3147d7..f815ac6bf5 100644 --- a/src/conf/netdev_vport_profile_conf.c +++ b/src/conf/netdev_vport_profile_conf.c @@ -181,7 +181,7 @@ void virNetDevVPortProfileFormat(const virNetDevVPortProfile *virtPort, virBuffer *buf) { - enum virNetDevVPortProfile type; + virNetDevVPortProfileType type; bool noParameters; if (!virtPort) diff --git a/src/qemu/qemu_monitor.c b/src/qemu/qemu_monitor.c index 7f65c23748..0a6acc5470 100644 --- a/src/qemu/qemu_monitor.c +++ b/src/qemu/qemu_monitor.c @@ -3464,16 +3464,16 @@ qemuMonitorNBDServerStart(qemuMonitor *mon, int qemuMonitorNBDServerAdd(qemuMonitor *mon, const char *deviceID, - const char *export, + const char *nbd_export, bool writable, const char *bitmap) { - VIR_DEBUG("deviceID=%s, export=%s, bitmap=%s", deviceID, NULLSTR(export), + VIR_DEBUG("deviceID=%s, export=%s, bitmap=%s", deviceID, NULLSTR(nbd_export), NULLSTR(bitmap)); QEMU_CHECK_MONITOR(mon); - return qemuMonitorJSONNBDServerAdd(mon, deviceID, export, writable, + return qemuMonitorJSONNBDServerAdd(mon, deviceID, nbd_export, writable, bitmap); } diff --git a/src/qemu/qemu_monitor.h b/src/qemu/qemu_monitor.h index 57d1b45bf5..0baf237f92 100644 --- a/src/qemu/qemu_monitor.h +++ b/src/qemu/qemu_monitor.h @@ -1197,7 +1197,7 @@ int qemuMonitorNBDServerStart(qemuMonitor *mon, ATTRIBUTE_NONNULL(2); int qemuMonitorNBDServerAdd(qemuMonitor *mon, const char *deviceID, - const char *export, + const char *nbd_export, bool writable, const char *bitmap); int qemuMonitorNBDServerStop(qemuMonitor *mon); diff --git a/src/util/virfile.h b/src/util/virfile.h index 7df3fcb840..871aa06f14 100644 --- a/src/util/virfile.h +++ b/src/util/virfile.h @@ -215,7 +215,7 @@ void virFileActivateDirOverrideForLib(void); off_t virFileLength(const char *path, int fd) ATTRIBUTE_NONNULL(1); bool virFileIsDir (const char *file) ATTRIBUTE_NONNULL(1); -bool virFileExists(const char *file) ATTRIBUTE_NONNULL(1) G_NO_INLINE; +bool virFileExists(const char *file) G_NO_INLINE ATTRIBUTE_NONNULL(1); bool virFileIsExecutable(const char *file) ATTRIBUTE_NONNULL(1); bool virFileIsRegular(const char *file) ATTRIBUTE_NONNULL(1); diff --git a/src/util/virnetdev.h b/src/util/virnetdev.h index c287a7b272..db22519a15 100644 --- a/src/util/virnetdev.h +++ b/src/util/virnetdev.h @@ -168,11 +168,11 @@ int virNetDevSetupControl(const char *ifname, G_GNUC_WARN_UNUSED_RESULT; int virNetDevExists(const char *brname) - ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT; int virNetDevSetOnline(const char *ifname, bool online) - ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT; int virNetDevGetOnline(const char *ifname, bool *online) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT; @@ -180,7 +180,7 @@ int virNetDevGetOnline(const char *ifname, int virNetDevSetMAC(const char *ifname, const virMacAddr *macaddr) - ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT; int virNetDevGetMAC(const char *ifname, virMacAddr *macaddr) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT; @@ -202,7 +202,7 @@ int virNetDevSetCoalesce(const char *ifname, int virNetDevSetMTU(const char *ifname, int mtu) - ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT; int virNetDevSetMTUFromDevice(const char *ifname, const char *otherifname) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT; @@ -323,8 +323,8 @@ int virNetDevGetRcvAllMulti(const char *ifname, bool *receive) int virNetDevSysfsFile(char **pf_sysfs_device_link, const char *ifname, const char *file) - ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) ATTRIBUTE_NONNULL(3) - G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) ATTRIBUTE_NONNULL(3) + G_GNUC_WARN_UNUSED_RESULT; int virNetDevRunEthernetScript(const char *ifname, const char *script) G_NO_INLINE; diff --git a/src/util/virnetdevip.h b/src/util/virnetdevip.h index fdf116f509..2b45d360ae 100644 --- a/src/util/virnetdevip.h +++ b/src/util/virnetdevip.h @@ -59,7 +59,7 @@ int virNetDevIPAddrAdd(const char *ifname, virSocketAddr *addr, virSocketAddr *peer, unsigned int prefix) - ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2) G_GNUC_WARN_UNUSED_RESULT; int virNetDevIPRouteAdd(const char *ifname, virSocketAddr *addr, unsigned int prefix, diff --git a/src/util/virnetdevmacvlan.h b/src/util/virnetdevmacvlan.h index a5c34d6417..5846018df1 100644 --- a/src/util/virnetdevmacvlan.h +++ b/src/util/virnetdevmacvlan.h @@ -47,7 +47,7 @@ typedef enum { } virNetDevMacVLanCreateFlags; bool virNetDevMacVLanIsMacvtap(const char *ifname) - ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT G_NO_INLINE; + G_NO_INLINE ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT; int virNetDevMacVLanCreate(const char *ifname, const virMacAddr *macaddress, diff --git a/src/util/virnetdevvportprofile.c b/src/util/virnetdevvportprofile.c index c755fa79ec..221e0888b3 100644 --- a/src/util/virnetdevvportprofile.c +++ b/src/util/virnetdevvportprofile.c @@ -279,7 +279,7 @@ static int virNetDevVPortProfileMerge(virNetDevVPortProfile *orig, const virNetDevVPortProfile *mods) { - enum virNetDevVPortProfile otype; + virNetDevVPortProfileType otype; if (!orig || !mods) return 0; diff --git a/src/util/virnetdevvportprofile.h b/src/util/virnetdevvportprofile.h index 600b2093c5..1714116e9c 100644 --- a/src/util/virnetdevvportprofile.h +++ b/src/util/virnetdevvportprofile.h @@ -25,7 +25,7 @@ #define LIBVIRT_IFLA_VF_PORT_PROFILE_MAX 40 -typedef enum virNetDevVPortProfile { +typedef enum _virNetDevVPortProfileType { VIR_NETDEV_VPORT_PROFILE_NONE, VIR_NETDEV_VPORT_PROFILE_8021QBG, VIR_NETDEV_VPORT_PROFILE_8021QBH, diff --git a/src/util/virnvme.c b/src/util/virnvme.c index 37333d515b..e996ae2c5b 100644 --- a/src/util/virnvme.c +++ b/src/util/virnvme.c @@ -63,7 +63,7 @@ VIR_ONCE_GLOBAL_INIT(virNVMe); virNVMeDevice * virNVMeDeviceNew(const virPCIDeviceAddress *address, - unsigned long namespace, + unsigned long nvme_namespace, bool managed) { virNVMeDevice *dev = NULL; @@ -71,7 +71,7 @@ virNVMeDeviceNew(const virPCIDeviceAddress *address, dev = g_new0(virNVMeDevice, 1); virPCIDeviceAddressCopy(&dev->address, address); - dev->namespace = namespace; + dev->namespace = nvme_namespace; dev->managed = managed; return dev; diff --git a/src/util/virnvme.h b/src/util/virnvme.h index ceef402c4b..f1d0bb5da0 100644 --- a/src/util/virnvme.h +++ b/src/util/virnvme.h @@ -33,7 +33,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(virNVMeDeviceList, virObjectUnref); virNVMeDevice * virNVMeDeviceNew(const virPCIDeviceAddress *address, - unsigned long namespace, + unsigned long nvme_namespace, bool managed); void diff --git a/src/util/viruuid.h b/src/util/viruuid.h index 9667bd3200..9d0b6d1b00 100644 --- a/src/util/viruuid.h +++ b/src/util/viruuid.h @@ -41,7 +41,7 @@ int virSetHostUUIDStr(const char *host_uuid); -int virGetHostUUID(unsigned char *host_uuid) ATTRIBUTE_NONNULL(1) G_NO_INLINE; +int virGetHostUUID(unsigned char *host_uuid) G_NO_INLINE ATTRIBUTE_NONNULL(1); bool virUUIDIsValid(const unsigned char *uuid); -- 2.34.1

Clang coverage is the de-facto standard for generating coverage reports in libFuzzer based fuzzers, so add support for the same. __LLVM_PROFILE_RT_INIT_ONCE is added automatically which breaks commandhelper so filter the same. Note that the existing gcov support can also be used to generate coverage. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- meson.build | 11 +++++++++++ meson_options.txt | 3 ++- tests/commandhelper.c | 8 ++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index f31485c395..904524abc6 100644 --- a/meson.build +++ b/meson.build @@ -2141,6 +2141,17 @@ if get_option('test_coverage') ] endif +if get_option('test_coverage_clang') + if cc.get_id() != 'clang' + error('test_coverage_clang can only be used with the Clang compiler.') + endif + + coverage_flags = [ + '-fprofile-instr-generate', + '-fcoverage-mapping', + ] +endif + # Various definitions diff --git a/meson_options.txt b/meson_options.txt index 2d440c63d8..153e325cb5 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -8,7 +8,8 @@ option('unitdir', type: 'string', value: '', description: 'directory for systemd option('sysusersdir', type: 'string', value: '', description: 'directory for sysusers files') # dep:tests option('expensive_tests', type: 'feature', value: 'auto', description: 'set the default for enabling expensive tests (long timeouts)') -option('test_coverage', type: 'boolean', value: false, description: 'turn on code coverage instrumentation') +option('test_coverage', type: 'boolean', value: false, description: 'turn on code coverage instrumentation (gcov)') +option('test_coverage_clang', type: 'boolean', value: false, description: 'turn on code coverage instrumentation (clang)') option('git_werror', type: 'feature', value: 'auto', description: 'use -Werror if building from GIT') option('rpath', type: 'feature', value: 'auto', description: 'whether to include rpath information in installed binaries and libraries') option('docdir', type: 'string', value: '', description: 'documentation installation directory') diff --git a/tests/commandhelper.c b/tests/commandhelper.c index d4629d824e..6f562c3e28 100644 --- a/tests/commandhelper.c +++ b/tests/commandhelper.c @@ -172,9 +172,13 @@ static int printEnvironment(FILE *log) for (i = 0; i < length; i++) { /* Ignore the variables used to instruct the loader into * behaving differently, as they could throw the tests off. - * Also ignore __CF_USER_TEXT_ENCODING, which is set by macOS. */ + * Also ignore __CF_USER_TEXT_ENCODING, which is set by macOS. + * + * If LLVM coverage is enabled, __LLVM_PROFILE_RT_INIT_ONCE is + * automatically set, which should be ignored. */ if (!STRPREFIX(newenv[i], "LD_") && - !STRPREFIX(newenv[i], "__CF_USER_TEXT_ENCODING=")) { + !STRPREFIX(newenv[i], "__CF_USER_TEXT_ENCODING=") && + !STRPREFIX(newenv[i], "__LLVM_PROFILE_RT_INIT_ONCE=")) { fprintf(log, "ENV:%s\n", newenv[i]); } } -- 2.34.1

This patch exports handlers for fake network, storage and secret drivers which will be used by the QEMU XML domain fuzzer. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- tests/qemuxmlconftest.c | 249 -------------------------------------- tests/testutilsqemu.c | 256 ++++++++++++++++++++++++++++++++++++++++ tests/testutilsqemu.h | 57 +++++++++ 3 files changed, 313 insertions(+), 249 deletions(-) diff --git a/tests/qemuxmlconftest.c b/tests/qemuxmlconftest.c index 89292af300..35830257c7 100644 --- a/tests/qemuxmlconftest.c +++ b/tests/qemuxmlconftest.c @@ -33,52 +33,6 @@ static virQEMUDriver driver; -static unsigned char * -fakeSecretGetValue(virSecretPtr obj G_GNUC_UNUSED, - size_t *value_size, - unsigned int fakeflags G_GNUC_UNUSED) -{ - char *secret; - secret = g_strdup("AQCVn5hO6HzFAhAAq0NCv8jtJcIcE+HOBlMQ1A"); - *value_size = strlen(secret); - return (unsigned char *) secret; -} - -static virSecretPtr -fakeSecretLookupByUsage(virConnectPtr conn, - int usageType, - const char *usageID) -{ - unsigned char uuid[VIR_UUID_BUFLEN]; - if (usageType == VIR_SECRET_USAGE_TYPE_VOLUME) { - if (!STRPREFIX(usageID, "/storage/guest_disks/")) { - virReportError(VIR_ERR_INTERNAL_ERROR, - "test provided invalid volume storage prefix '%s'", - usageID); - return NULL; - } - } else if (STRNEQ(usageID, "mycluster_myname") && - STRNEQ(usageID, "client.admin secret")) { - virReportError(VIR_ERR_INTERNAL_ERROR, - "test provided incorrect usage '%s'", usageID); - return NULL; - } - - if (virUUIDGenerate(uuid) < 0) - return NULL; - - return virGetSecret(conn, uuid, usageType, usageID); -} - -static virSecretPtr -fakeSecretLookupByUUID(virConnectPtr conn, - const unsigned char *uuid) -{ - /* NB: This mocked value could be "tls" or "volume" depending on - * which test is being run, we'll leave at NONE (or 0) */ - return virGetSecret(conn, uuid, VIR_SECRET_USAGE_TYPE_NONE, ""); -} - static virSecretDriver fakeSecretDriver = { .connectNumOfSecrets = NULL, .connectListSecrets = NULL, @@ -92,118 +46,6 @@ static virSecretDriver fakeSecretDriver = { }; -# define STORAGE_POOL_XML_PATH "storagepoolxml2xmlout/" -static const unsigned char fakeUUID[VIR_UUID_BUFLEN] = "fakeuuid"; - -static virStoragePoolPtr -fakeStoragePoolLookupByName(virConnectPtr conn, - const char *name) -{ - g_autofree char *xmlpath = NULL; - - if (STRNEQ(name, "inactive")) { - xmlpath = g_strdup_printf("%s/%s%s.xml", abs_srcdir, - STORAGE_POOL_XML_PATH, name); - - if (!virFileExists(xmlpath)) { - virReportError(VIR_ERR_NO_STORAGE_POOL, - "File '%s' not found", xmlpath); - return NULL; - } - } - - return virGetStoragePool(conn, name, fakeUUID, NULL, NULL); -} - - -static virStorageVolPtr -fakeStorageVolLookupByName(virStoragePoolPtr pool, - const char *name) -{ - g_auto(GStrv) volinfo = NULL; - - if (STREQ(pool->name, "inactive")) { - virReportError(VIR_ERR_OPERATION_INVALID, - "storage pool '%s' is not active", pool->name); - return NULL; - } - - if (STREQ(name, "nonexistent")) { - virReportError(VIR_ERR_NO_STORAGE_VOL, - "no storage vol with matching name '%s'", name); - return NULL; - } - - if (!(volinfo = g_strsplit(name, "+", 2))) - return NULL; - - if (!volinfo[1]) { - return virGetStorageVol(pool->conn, pool->name, name, "block", NULL, NULL); - } - - return virGetStorageVol(pool->conn, pool->name, volinfo[1], volinfo[0], - NULL, NULL); -} - -static int -fakeStorageVolGetInfo(virStorageVolPtr vol, - virStorageVolInfoPtr info) -{ - memset(info, 0, sizeof(*info)); - - info->type = virStorageVolTypeFromString(vol->key); - - if (info->type < 0) { - virReportError(VIR_ERR_INTERNAL_ERROR, - "Invalid volume type '%s'", vol->key); - return -1; - } - - return 0; -} - - -static char * -fakeStorageVolGetPath(virStorageVolPtr vol) -{ - return g_strdup_printf("/some/%s/device/%s", vol->key, vol->name); -} - - -static char * -fakeStoragePoolGetXMLDesc(virStoragePoolPtr pool, - unsigned int flags_unused G_GNUC_UNUSED) -{ - g_autofree char *xmlpath = NULL; - char *xmlbuf = NULL; - - if (STREQ(pool->name, "inactive")) { - virReportError(VIR_ERR_NO_STORAGE_POOL, NULL); - return NULL; - } - - xmlpath = g_strdup_printf("%s/%s%s.xml", abs_srcdir, STORAGE_POOL_XML_PATH, - pool->name); - - if (virTestLoadFile(xmlpath, &xmlbuf) < 0) { - virReportError(VIR_ERR_INTERNAL_ERROR, - "failed to load XML file '%s'", - xmlpath); - return NULL; - } - - return xmlbuf; -} - -static int -fakeStoragePoolIsActive(virStoragePoolPtr pool) -{ - if (STREQ(pool->name, "inactive")) - return 0; - - return 1; -} - /* Test storage pool implementation * * These functions aid testing of storage pool related stuff when creating a @@ -257,97 +99,6 @@ static virNWFilterDriver fakeNWFilterDriver = { }; -/* name of the fake network shall be constructed as: - * NETWORKXMLNAME;NETWORKPORTXMLNAME - * where: - * NETWORKXMLNAME resolves to abs_srcdir/networkxml2xmlin/NETWORKXMLNAME.xml - * NETWORKPORTXMLNAME resolves to abs_srcdir/virnetworkportxml2xmldata/NETWORKPORTXMLNAME.xml - */ -static virNetworkPtr -fakeNetworkLookupByName(virConnectPtr conn, - const char *name) -{ - unsigned char uuid[VIR_UUID_BUFLEN]; - g_autofree char *netname = g_strdup(name); - g_autofree char *path = NULL; - char *tmp; - - memset(uuid, 0, VIR_UUID_BUFLEN); - - if ((tmp = strchr(netname, ';'))) { - *tmp = '\0'; - } else { - virReportError(VIR_ERR_NO_NETWORK, - "Malformed fake network name '%s'. See fakeNetworkLookupByName.", - name); - return NULL; - } - - path = g_strdup_printf(abs_srcdir "/networkxml2xmlin/%s.xml", netname); - - if (!virFileExists(path)) { - virReportError(VIR_ERR_NO_NETWORK, "fake network '%s' not found", path); - return NULL; - } - - return virGetNetwork(conn, name, uuid); -} - - -static char * -fakeNetworkGetXMLDesc(virNetworkPtr network, - unsigned int noflags G_GNUC_UNUSED) -{ - g_autofree char *netname = g_strdup(network->name); - g_autofree char *path = NULL; - char *xml = NULL; - - *(strchr(netname, ';')) = '\0'; - - path = g_strdup_printf(abs_srcdir "/networkxml2xmlin/%s.xml", netname); - - if (virFileReadAll(path, 4 * 1024, &xml) < 0) - return NULL; - - return xml; -} - - -static virNetworkPortPtr -fakeNetworkPortCreateXML(virNetworkPtr net, - const char *xmldesc G_GNUC_UNUSED, - unsigned int noflags G_GNUC_UNUSED) -{ - unsigned char uuid[VIR_UUID_BUFLEN]; - g_autofree char *portname = g_strdup(strchr(net->name, ';') + 1); - g_autofree char *path = g_strdup_printf(abs_srcdir "/virnetworkportxml2xmldata/%s.xml", portname); - - memset(uuid, 0, VIR_UUID_BUFLEN); - - if (!virFileExists(path)) { - virReportError(VIR_ERR_NO_NETWORK_PORT, "fake network port '%s' not found", path); - return NULL; - } - - return virGetNetworkPort(net, uuid); -} - - -static char * -fakeNetworkPortGetXMLDesc(virNetworkPortPtr port, - unsigned int noflags G_GNUC_UNUSED) -{ - g_autofree char *portname = g_strdup(strchr(port->net->name, ';') + 1); - g_autofree char *path = g_strdup_printf(abs_srcdir "/virnetworkportxml2xmldata/%s.xml", portname); - char *xml = NULL; - - if (virFileReadAll(path, 4 * 1024, &xml) < 0) - return NULL; - - return xml; -} - - static virNetworkDriver fakeNetworkDriver = { .networkLookupByName = fakeNetworkLookupByName, .networkGetXMLDesc = fakeNetworkGetXMLDesc, diff --git a/tests/testutilsqemu.c b/tests/testutilsqemu.c index ee6cae218a..4445fafd23 100644 --- a/tests/testutilsqemu.c +++ b/tests/testutilsqemu.c @@ -13,6 +13,7 @@ # include "virstring.h" # include "virfilecache.h" # include "virtpm.h" +# include "conf/storage_conf.h" # include <sys/types.h> # include <fcntl.h> @@ -1076,3 +1077,258 @@ testQemuPrepareHostBackendChardevOne(virDomainDeviceDef *dev, return 0; } + + +# define STORAGE_POOL_XML_PATH "storagepoolxml2xmlout/" +static const unsigned char fakeUUID[VIR_UUID_BUFLEN] = "fakeuuid"; + +virStoragePoolPtr +fakeStoragePoolLookupByName(virConnectPtr conn, + const char *name) +{ + g_autofree char *xmlpath = NULL; + + if (STRNEQ(name, "inactive")) { + xmlpath = g_strdup_printf("%s/%s%s.xml", abs_srcdir, + STORAGE_POOL_XML_PATH, name); + + if (!virFileExists(xmlpath)) { + virReportError(VIR_ERR_NO_STORAGE_POOL, + "File '%s' not found", xmlpath); + return NULL; + } + } + + return virGetStoragePool(conn, name, fakeUUID, NULL, NULL); +} + + +virStorageVolPtr +fakeStorageVolLookupByName(virStoragePoolPtr pool, + const char *name) +{ + g_auto(GStrv) volinfo = NULL; + + if (STREQ(pool->name, "inactive")) { + virReportError(VIR_ERR_OPERATION_INVALID, + "storage pool '%s' is not active", pool->name); + return NULL; + } + + if (STREQ(name, "nonexistent")) { + virReportError(VIR_ERR_NO_STORAGE_VOL, + "no storage vol with matching name '%s'", name); + return NULL; + } + + if (!(volinfo = g_strsplit(name, "+", 2))) + return NULL; + + if (!volinfo[1]) { + return virGetStorageVol(pool->conn, pool->name, name, "block", NULL, NULL); + } + + return virGetStorageVol(pool->conn, pool->name, volinfo[1], volinfo[0], + NULL, NULL); +} + + +int +fakeStorageVolGetInfo(virStorageVolPtr vol, + virStorageVolInfoPtr info) +{ + memset(info, 0, sizeof(*info)); + + info->type = virStorageVolTypeFromString(vol->key); + + if (info->type < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "Invalid volume type '%s'", vol->key); + return -1; + } + + return 0; +} + + +char * +fakeStorageVolGetPath(virStorageVolPtr vol) +{ + return g_strdup_printf("/some/%s/device/%s", vol->key, vol->name); +} + + +char * +fakeStoragePoolGetXMLDesc(virStoragePoolPtr pool, + unsigned int flags_unused G_GNUC_UNUSED) +{ + g_autofree char *xmlpath = NULL; + char *xmlbuf = NULL; + + if (STREQ(pool->name, "inactive")) { + virReportError(VIR_ERR_NO_STORAGE_POOL, NULL); + return NULL; + } + + xmlpath = g_strdup_printf("%s/%s%s.xml", abs_srcdir, STORAGE_POOL_XML_PATH, + pool->name); + + if (virTestLoadFile(xmlpath, &xmlbuf) < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "failed to load XML file '%s'", + xmlpath); + return NULL; + } + + return xmlbuf; +} + + +int +fakeStoragePoolIsActive(virStoragePoolPtr pool) +{ + if (STREQ(pool->name, "inactive")) + return 0; + + return 1; +} + + +/* name of the fake network shall be constructed as: + * NETWORKXMLNAME;NETWORKPORTXMLNAME + * where: + * NETWORKXMLNAME resolves to abs_srcdir/networkxml2xmlin/NETWORKXMLNAME.xml + * NETWORKPORTXMLNAME resolves to abs_srcdir/virnetworkportxml2xmldata/NETWORKPORTXMLNAME.xml + */ +virNetworkPtr +fakeNetworkLookupByName(virConnectPtr conn, + const char *name) +{ + unsigned char uuid[VIR_UUID_BUFLEN]; + g_autofree char *netname = g_strdup(name); + g_autofree char *path = NULL; + char *tmp; + + memset(uuid, 0, VIR_UUID_BUFLEN); + + if ((tmp = strchr(netname, ';'))) { + *tmp = '\0'; + } else { + virReportError(VIR_ERR_NO_NETWORK, + "Malformed fake network name '%s'. See fakeNetworkLookupByName.", + name); + return NULL; + } + + path = g_strdup_printf(abs_srcdir "/networkxml2xmlin/%s.xml", netname); + + if (!virFileExists(path)) { + virReportError(VIR_ERR_NO_NETWORK, "fake network '%s' not found", path); + return NULL; + } + + return virGetNetwork(conn, name, uuid); +} + + +char * +fakeNetworkGetXMLDesc(virNetworkPtr network, + unsigned int noflags G_GNUC_UNUSED) +{ + g_autofree char *netname = g_strdup(network->name); + g_autofree char *path = NULL; + char *xml = NULL; + + *(strchr(netname, ';')) = '\0'; + + path = g_strdup_printf(abs_srcdir "/networkxml2xmlin/%s.xml", netname); + + if (virFileReadAll(path, 4 * 1024, &xml) < 0) + return NULL; + + return xml; +} + + +virNetworkPortPtr +fakeNetworkPortCreateXML(virNetworkPtr net, + const char *xmldesc G_GNUC_UNUSED, + unsigned int noflags G_GNUC_UNUSED) +{ + unsigned char uuid[VIR_UUID_BUFLEN]; + g_autofree char *portname = g_strdup(strchr(net->name, ';') + 1); + g_autofree char *path = g_strdup_printf(abs_srcdir "/virnetworkportxml2xmldata/%s.xml", portname); + + memset(uuid, 0, VIR_UUID_BUFLEN); + + if (!virFileExists(path)) { + virReportError(VIR_ERR_NO_NETWORK_PORT, "fake network port '%s' not found", path); + return NULL; + } + + return virGetNetworkPort(net, uuid); +} + + +char * +fakeNetworkPortGetXMLDesc(virNetworkPortPtr port, + unsigned int noflags G_GNUC_UNUSED) +{ + g_autofree char *portname = g_strdup(strchr(port->net->name, ';') + 1); + g_autofree char *path = g_strdup_printf(abs_srcdir "/virnetworkportxml2xmldata/%s.xml", portname); + char *xml = NULL; + + if (virFileReadAll(path, 4 * 1024, &xml) < 0) + return NULL; + + return xml; +} + + +unsigned char * +fakeSecretGetValue(virSecretPtr obj G_GNUC_UNUSED, + size_t *value_size, + unsigned int fakeflags G_GNUC_UNUSED) +{ + char *secret; + secret = g_strdup("AQCVn5hO6HzFAhAAq0NCv8jtJcIcE+HOBlMQ1A"); + *value_size = strlen(secret); + return (unsigned char *) secret; +} + + +virSecretPtr +fakeSecretLookupByUsage(virConnectPtr conn, + int usageType, + const char *usageID) +{ + unsigned char uuid[VIR_UUID_BUFLEN]; + if (usageType == VIR_SECRET_USAGE_TYPE_VOLUME) { + if (!STRPREFIX(usageID, "/storage/guest_disks/")) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "test provided invalid volume storage prefix '%s'", + usageID); + return NULL; + } + } else if (STRNEQ(usageID, "mycluster_myname") && + STRNEQ(usageID, "client.admin secret")) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "test provided incorrect usage '%s'", usageID); + return NULL; + } + + if (virUUIDGenerate(uuid) < 0) + return NULL; + + return virGetSecret(conn, uuid, usageType, usageID); +} + + +virSecretPtr +fakeSecretLookupByUUID(virConnectPtr conn, + const unsigned char *uuid) +{ + /* NB: This mocked value could be "tls" or "volume" depending on + * which test is being run, we'll leave at NONE (or 0) */ + return virGetSecret(conn, uuid, VIR_SECRET_USAGE_TYPE_NONE, ""); +} diff --git a/tests/testutilsqemu.h b/tests/testutilsqemu.h index 90632031ff..99d34fcaf5 100644 --- a/tests/testutilsqemu.h +++ b/tests/testutilsqemu.h @@ -188,4 +188,61 @@ testQemuInsertRealCaps(virFileCache *cache, GHashTable *capsCache, GHashTable *schemaCache, GHashTable **schema); + +void +testQemuPrepareDef(virDomainObj *vm, + testQemuInfo *info); + +virStoragePoolPtr +fakeStoragePoolLookupByName(virConnectPtr conn, + const char *name); + +virStorageVolPtr +fakeStorageVolLookupByName(virStoragePoolPtr pool, + const char *name); + +int +fakeStorageVolGetInfo(virStorageVolPtr vol, + virStorageVolInfoPtr info); + +char * +fakeStorageVolGetPath(virStorageVolPtr vol); + +char * +fakeStoragePoolGetXMLDesc(virStoragePoolPtr pool, + unsigned int flags_unused); + +int +fakeStoragePoolIsActive(virStoragePoolPtr pool); + +virNetworkPtr +fakeNetworkLookupByName(virConnectPtr conn, + const char *name); + +char * +fakeNetworkGetXMLDesc(virNetworkPtr network, + unsigned int noflags); + +virNetworkPortPtr +fakeNetworkPortCreateXML(virNetworkPtr net, + const char *xmldesc, + unsigned int noflags); + +char * +fakeNetworkPortGetXMLDesc(virNetworkPortPtr port, + unsigned int noflags); + +unsigned char * +fakeSecretGetValue(virSecretPtr obj, + size_t *value_size, + unsigned int fakeflags); + +virSecretPtr +fakeSecretLookupByUsage(virConnectPtr conn, + int usageType, + const char *usageID); + +virSecretPtr +fakeSecretLookupByUUID(virConnectPtr conn, + const unsigned char *uuid); #endif -- 2.34.1

We make small modifications to the rng files to allow the relaxng-to-proto script to infer XML attribute types better. 1. Make iobase and irq refs so we can override their types in the script. 2. Replace deviceName with absFilePath so that vdpa devices get fuzzed correctly with file paths. 3. The relaxng-to-proto script does not handle regexes, so convert some of them to <choice> containing <value>. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- src/conf/schemas/basictypes.rng | 20 ++++++++++++++------ src/conf/schemas/domaincommon.rng | 11 +++++++---- src/conf/schemas/networkcommon.rng | 14 ++++++++------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/conf/schemas/basictypes.rng b/src/conf/schemas/basictypes.rng index 2931e316b7..db05c085fe 100644 --- a/src/conf/schemas/basictypes.rng +++ b/src/conf/schemas/basictypes.rng @@ -555,19 +555,27 @@ </element> </define> + <define name="iobase"> + <data type="string"> + <param name="pattern">0x[a-fA-F0-9]{1,4}</param> + </data> + </define> + + <define name="irq"> + <data type="string"> + <param name="pattern">0x[a-fA-F0-9]</param> + </data> + </define> + <define name="isaaddress"> <optional> <attribute name="iobase"> - <data type="string"> - <param name="pattern">0x[a-fA-F0-9]{1,4}</param> - </data> + <ref name="iobase"/> </attribute> </optional> <optional> <attribute name="irq"> - <data type="string"> - <param name="pattern">0x[a-fA-F0-9]</param> - </data> + <ref name="irq"/> </attribute> </optional> </define> diff --git a/src/conf/schemas/domaincommon.rng b/src/conf/schemas/domaincommon.rng index 05ba697924..d635d2b179 100644 --- a/src/conf/schemas/domaincommon.rng +++ b/src/conf/schemas/domaincommon.rng @@ -3670,7 +3670,7 @@ <interleave> <element name="source"> <attribute name="dev"> - <ref name="deviceName"/> + <ref name="absFilePath"/> </attribute> </element> <ref name="interface-options"/> @@ -8495,9 +8495,12 @@ </data> </define> <define name="bridgeMode"> - <data type="string"> - <param name="pattern">(vepa|bridge|private|passthrough)</param> - </data> + <choice> + <value>vepa</value> + <value>bridge</value> + <value>private</value> + <value>passthrough</value> + </choice> </define> <define name="addrIPorName"> <choice> diff --git a/src/conf/schemas/networkcommon.rng b/src/conf/schemas/networkcommon.rng index 6df6d43f54..0254bac693 100644 --- a/src/conf/schemas/networkcommon.rng +++ b/src/conf/schemas/networkcommon.rng @@ -199,14 +199,16 @@ </data> </define> <define name="protocol"> - <data type="string"> - <param name="pattern">(tcp)|(udp)</param> - </data> + <choice> + <value>tcp</value> + <value>udp</value> + </choice> </define> <define name="addr-family"> - <data type="string"> - <param name="pattern">(ipv4)|(ipv6)</param> - </data> + <choice> + <value>ipv4</value> + <value>ipv6</value> + </choice> </define> <define name="vlan"> -- 2.34.1

This script converts relaxNG schemas to an equivalent protobuf definition file. The script captures the general structure of the XML schema and tries to guess the attribute datatypes. The protobuf definitions give the fuzzers awareness of the XML schema. The protobuf files will be used by the fuzzers to mutate protobuf data and serialize them to XML. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- build-aux/syntax-check.mk | 1 + scripts/meson.build | 1 + scripts/relaxng-to-proto.py | 505 ++++++++++++++++++++++++++++++++++++ 3 files changed, 507 insertions(+) create mode 100644 scripts/relaxng-to-proto.py diff --git a/build-aux/syntax-check.mk b/build-aux/syntax-check.mk index 0759372b2b..a60e4a8082 100644 --- a/build-aux/syntax-check.mk +++ b/build-aux/syntax-check.mk @@ -844,6 +844,7 @@ http_sites += www.inkscape.org http_sites += www.innotek.de http_sites += www.w3.org http_sites += xmlns +http_sites += relaxng.org # Links in licenses http_sites += scripts.sil.org diff --git a/scripts/meson.build b/scripts/meson.build index 2798e302ab..7249346e45 100644 --- a/scripts/meson.build +++ b/scripts/meson.build @@ -32,6 +32,7 @@ scripts = [ 'mock-noinline.py', 'prohibit-duplicate-header.py', 'qemu-replies-tool.py', + 'relaxng-to-proto.py', ] foreach name : scripts diff --git a/scripts/relaxng-to-proto.py b/scripts/relaxng-to-proto.py new file mode 100644 index 0000000000..f13d6f7e40 --- /dev/null +++ b/scripts/relaxng-to-proto.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 + +import re +import sys +import xml.etree.ElementTree as ET +import argparse + +# Track XML tree objects of all <define> tags +define_table = {} + +# Store parsed tree of <define> tag +define_trees = {} + +relaxng_ns = "{http://relaxng.org/ns/structure/1.0}" + +integer_refs = ["positiveInteger", "unsignedInt", "uint8", "uint16", "uint24", "uint32", "hexuint"] +integer_datatypes = ["positiveInteger", "unsignedInt", "int", "long", "unsignedLong", "integer"] + +#Override attribute based on ref +custom_ref_table = { + "virYesNo": {"type": "bool"}, + "virOnOff": {"type": "Switch"}, + "ipAddr": {"type": "IPAddr"}, + "ipv4Addr": {"type": "IPAddr"}, + "ipv6Addr": {"type": "IPAddr"}, + "diskTargetDev": {"type": "TargetDev"}, + "UUID": {"type": "DummyUUID"}, + "usbIdDefault": {"values": ["-1",]}, + "usbClass": {"type": "uint32"}, + "usbId": {"type": "uint32"}, + "usbVersion": {"type": "uint32"}, + "usbAddr": {"type": "uint32"}, + "usbPort": {"type": "uint32"}, + "virtioserialPort": {"type": "uint32"}, + "timeDelta": {"type": "uint32"}, + "absFilePath": {"type": "DummyPath"}, + "filePath": {"type": "DummyPath"}, + "absDirPath": {"type": "DummyPath"}, + "dirPath": {"type": "DummyPath"}, + "cpuset": {"type": "CPUSet"}, + "pciSlot": {"type": "uint32"}, + "pciFunc": {"type": "uint32"}, + "ccidSlot": {"type": "uint32"}, + "ccwCssidRange": {"type": "uint32"}, + "ccwSsidRange": {"type": "uint32"}, + "ccwDevnoRange": {"type": "uint32"}, + "driveController": {"type": "uint32"}, + "driveBus": {"type": "uint32"}, + "driveSCSITarget": {"type": "uint32"}, + "driveUnit": {"type": "uint32"}, + "irq": {"type": "uint32"}, + "iobase": {"type": "uint32"}, + "uniMacAddr": {"type": "MacAddr"}, + } + +net_model_names = ["virtio", "virtio-transitional", "virtio-non-transitional", "e1000", "e1000e", "igb", + "rtl8139", "netfront", "usb-net", "spapr-vlan", "lan9118", "scm91c111", "vlance", "vmxnet", + "vmxnet2", "vmxnet3", "Am79C970A", "Am79C973", "82540EM", "82545EM", "82543GC"] + +# Override attribute based on paths +attr_path_table = { + "domain.devices.interface.model.type": {"values": net_model_names}, + } + +# Tag paths end with a dot while attributes don't. +xml_modify_path = { + "domain.devices.smartcard.certificate.": {"repeated": True} + } + +def tree_add_tag(tree, element_name, set_repeat): + if "tags" not in tree: + tree["tags"] = {} + + if element_name not in tree["tags"]: + tree["tags"][element_name] = {} + + if set_repeat: + tree["tags"][element_name]["repeated"] = True + +def tree_add_attribute(tree, child, attrib_name, path): + if "attributes" not in tree: + tree["attributes"] = {} + + if attrib_name not in tree["attributes"]: + tree["attributes"][attrib_name] = {} + + if "type_list" not in tree["attributes"][attrib_name]: + tree["attributes"][attrib_name]["type_list"] = [] + + if "value_list" not in tree["attributes"][attrib_name]: + tree["attributes"][attrib_name]["value_list"] = [] + + parse_attribute(child, attrib_name, tree["attributes"][attrib_name], path + attrib_name) + +def parse_datatype(root): + datatype = root.attrib["type"] + if datatype in integer_datatypes: + return "uint32" + else: + return "DummyString" + +# Parse <ref>, which may point to either a primitive data type, a type +# defined in custom_ref_table or simply an XML tree pointer. +def parse_ref_node(ref_node): + ref_tree = define_table[ref_node.attrib["name"]] + ref_name = ref_node.attrib["name"] + + if ref_name in integer_refs: + return {"type": "uint32"} + elif ref_name in custom_ref_table: + ref_type = custom_ref_table[ref_name] + return ref_type + else: + return {"tree": ref_tree} + +def add_to_attr_list(l, new_val): + if new_val not in l: + l.append(new_val) + +# Map custom ref table entries to appropriate values in value_list +# and type_list of an attribute tree. +def add_ref_type_to_attr_lists(attr_type, value_list, type_list): + if "types" in attr_type: + for type_name in attr_type["types"]: + add_to_attr_list(type_list, type_name) + elif "type" in attr_type: + add_to_attr_list(type_list, attr_type["type"]) + + if "values" in attr_type: + for value in attr_type["values"]: + add_to_attr_list(value_list, value) + +# Parse <choice> inside <attribute>. +# +# <choice> may contain one or more <data>, <value> or <ref> tags +def parse_attribute_choices(attribute_node, value_list, type_list): + for value_node in attribute_node: + if (value_node.tag == relaxng_ns + "value"): + value = value_node.text + add_to_attr_list(value_list, value) + elif (value_node.tag == relaxng_ns + "ref"): + ref_parse = parse_ref_node(value_node) + + if "tree" not in ref_parse: + add_ref_type_to_attr_lists(ref_parse, value_list, type_list) + else: + ref_tree = ref_parse["tree"] + parse_attribute_choices(ref_tree, value_list, type_list) + elif (value_node.tag == relaxng_ns + "data"): + datatype = parse_datatype(value_node) + add_to_attr_list(type_list, datatype) + elif (value_node.tag == relaxng_ns + "text"): + add_to_attr_list(type_list, "DummyString") + else: + parse_attribute_choices(value_node, value_list, type_list) + +# Parse <attribute> and generate an attribute tree +# +# An attribute tree consists of: +# 1. 'type_list': List of data types (Eg: uint32, string, etc.) +# 2. 'value_list': List of enum values. +# +# type_list and value_list can be extended further throughout the +# parsing of the XML tree. +def parse_attribute(root, attribute_name, attribute_tree, path): + type_list = attribute_tree["type_list"] + value_list = attribute_tree["value_list"] + + if path in attr_path_table: + add_ref_type_to_attr_lists(attr_path_table[path], value_list, type_list) + return + + if (len(root) == 0): + # If there is nothing in <attribute>, assuming string. + add_to_attr_list(type_list, "DummyString") + return + + attribute_node = root[0] + + if attribute_node.tag == relaxng_ns + "value": + # Single <value> corresponds to mono-valued enum + value = attribute_node.text + add_to_attr_list(value_list, value) + elif attribute_node.tag == relaxng_ns + "choice": + # Parse <choice> + parse_attribute_choices(attribute_node, value_list, type_list) + elif attribute_node.tag == relaxng_ns + "data": + # Primitive datatypes can be mapped to protobuf types directly + data_type = parse_datatype(attribute_node) + add_to_attr_list(type_list, data_type) + elif attribute_node.tag == relaxng_ns + "ref": + ref_name = attribute_node.attrib["name"] + ref_parse = parse_ref_node(attribute_node) + if "tree" not in ref_parse: + add_ref_type_to_attr_lists(ref_parse, value_list, type_list) + else: + # Recurse into ref + parse_attribute(define_table[ref_name], attribute_name, attribute_tree, path) + return + elif attribute_node.tag == relaxng_ns + "text": + # <text> is simply a generic string + add_to_attr_list(type_list, "DummyString") + else: + # We should never reach here + raise ValueError(f"Attribute {attribute_name} has unknown datatype") + +# Store XML text node data +def initialize_text_tree(tree): + if "text" not in tree: + tree["text"] = {"value_list": [], "type_list": []} + +# Parse <define> and store data in intermediate tree. +# +# An intermediate tree will consist of +# 1. 'tags': List of nested tag trees, which may contain other tags or attributes. +# 2. 'attributes': List of attribute trees +# 3. 'text': Similar in structure to attribute tree, representing an XML text node. +def parse_define(root, tree, ref_traverse, path="", set_repeat=False): + if path in xml_modify_path: + # TODO: Allow overriding more stuff when required + xml_modify_path_entry = xml_modify_path[path] + if "repeated" in xml_modify_path_entry: + tree["repeated"] = xml_modify_path_entry["repeated"] + + for child in root: + tag = child.tag + attrib = child.attrib + + # Handle <element> tags which will be represented as T_ fields in + # the protobuf. + if tag == relaxng_ns + "element": + if "name" not in attrib: + continue + + element_name = attrib["name"] + + tree_add_tag(tree, element_name, set_repeat) + + parse_define(child, tree["tags"][element_name], ref_traverse, path + element_name + ".") + + # Handle <attribute> tags which will be represented as A_ fields in + # the protobuf. + elif tag == relaxng_ns + "attribute": + attrib_name = attrib["name"] + + tree_add_attribute(tree, child, attrib_name, path) + + # <ref> points to another <define> which is recursively traversed. + elif tag == relaxng_ns + "ref": + ref_name = attrib["name"] + + # If ref encapsulates datatype, generate V_ field instead of traversing inside + ref_parse = parse_ref_node(child) + if ("tree" not in ref_parse): + initialize_text_tree(tree) + add_ref_type_to_attr_lists(ref_parse, tree["text"]["value_list"], tree["text"]["type_list"]) + continue + + # Handle infinitely recursive refs + if (define_table[ref_name] in ref_traverse): + pass + else: + parse_define(define_table[ref_name], tree, ref_traverse + [define_table[ref_name]], path, set_repeat) + + # If <oneOrMore> or <zeroOrMore> is used, + # immediate elements under it will have 'repeated' specifier in the + # final protobuf. + elif tag == relaxng_ns + "oneOrMore" or tag == relaxng_ns + "zeroOrMore": + parse_define(child, tree, ref_traverse, path, True) + + # <value>, <data> or <text> residing outside of <attribute> are + # XML text nodes, represented by V_ fields. + elif tag == relaxng_ns + "value": + initialize_text_tree(tree) + add_to_attr_list(tree["text"]["value_list"], child.text) + elif tag == relaxng_ns + "data": + initialize_text_tree(tree) + add_to_attr_list(tree["text"]["type_list"], parse_datatype(child)) + elif tag == relaxng_ns + "text": + initialize_text_tree(tree) + add_to_attr_list(tree["text"]["type_list"], "DummyString") + else: + parse_define(child, tree, ref_traverse, path, set_repeat) + +# Find all <define> tags and store them to resolve <ref> tags later +# +# Also parse all <include> tags in order to add more <define> tags to the +# table +def get_defines(schema_path): + schema_tree = ET.parse(schema_path) + root = schema_tree.getroot() + + for child in root: + tag = child.tag + attrib = child.attrib + + if tag == relaxng_ns + "start": + define_table["rng_entrypoint"] = child + if tag == relaxng_ns + "define": + define_name = attrib["name"] + define_table[define_name] = child + elif tag == relaxng_ns + "include": + include_href = attrib["href"] + get_defines(f"../src/conf/schemas/{include_href}") + +def padding(text, level): + return " " * level * 4 + text + +# Generate enum protobuf +def enum_to_proto(tree, level, scope): + proto = "" + enum_index = 0 + restricted_words = ["unix", "linux"] + for value in tree["values"]: + formatted_value = re.sub("[^a-zA-Z0-9_]", "_", value) + + if re.match("^[0-9]", formatted_value): + formatted_value = "_" + formatted_value + + if formatted_value in restricted_words: + formatted_value = "const_" + formatted_value + + while formatted_value in scope: + formatted_value = "_" + formatted_value + + proto += padding(f"{formatted_value} = {enum_index}", level) + + if formatted_value != value: + proto += f" [(real_value) = '{value}'];\n" + else: + proto += ";\n" + + scope.add(formatted_value) + enum_index += 1 + + return proto + +# Generate oneof protobuf containing multiple protobuf fields. +def oneof_to_proto(tree, attribute, protobuf_index, level, proto_opt, scope): + proto = "" + if "enum" in tree["types"]: + proto += padding(f"enum {attribute}Enum {{\n", level) + proto += enum_to_proto(tree["types"]["enum"], level + 1, scope) + proto += padding("}\n", level) + + optnum = 0 + proto += padding(f"oneof {attribute}Option {{\n", level) + for datatype in tree["types"]: + if datatype == "enum": + datatype = f"{attribute}Enum" + proto += padding(f"{datatype} A_OPT{str(optnum).zfill(2)}_{attribute} = {protobuf_index}{proto_opt};\n", level + 1) + protobuf_index += 1 + optnum += 1 + + proto += padding(f"}}\n", level) + + return (proto, protobuf_index - 1) + +# Given an attribute tree with type_list and value_list, +# determine how the protobuf field must be generated, i.e +# what field type it is and if it can take on multiple types. +def generate_attribute_type(attribute_tree): + result = {} + + type_list = attribute_tree["type_list"] + value_list = attribute_tree["value_list"] + # Number of data types possible for an attribute + # (enum values count as an additional type) + type_count = len(type_list) + (1 if len(value_list) > 0 else 0) + + if type_count == 1: + if len(type_list) == 1: + result["type"] = type_list[0] + elif len(value_list) > 0: + result["type"] = "enum" + result["values"] = value_list + else: + # If there are more than two data types for the attribute, + # it should be oneof in the protobuf. + result["type"] = "oneof" + result["types"] = {} + for datatype in type_list: + result["types"][datatype] = {"type": datatype} + + if (len(value_list) > 0): + result["types"]["enum"] = {"type": "enum", "values": value_list} + + return result + +# Convert intermediate tree to protobuf +def define_tree_to_proto(tree, level): + tags = tree.get("tags", {}) + attributes = tree.get("attributes", {}) + content_type = tree.get("content_type", None) + + # Due to how protobuf scoping works, we can't have the same enum idenitifers + # under the same message. We need to keep track of the scope ourselves. + current_scope = set() + + proto = "" + protobuf_index = 1 + + for attribute in attributes: + renamed_attr = attribute + proto_opt = "" + if re.search("[^a-zA-Z0-9_]", attribute): + renamed_attr = re.sub("[^a-zA-Z0-9_]", "_", attribute) + proto_opt = f" [(real_name) = '{attribute}']" + + attribute_type = generate_attribute_type(attributes[attribute]) + datatype = attribute_type["type"] + + if datatype == "oneof": + new_proto, new_index = oneof_to_proto(attribute_type, renamed_attr, protobuf_index, level, proto_opt, current_scope) + proto += new_proto + protobuf_index = new_index + elif datatype == "enum": + proto += padding(f"enum {renamed_attr}Enum {{\n", level) + proto += enum_to_proto(attribute_type, level + 1, current_scope) + proto += padding("}\n", level) + proto += padding(f"optional {renamed_attr}Enum A_{renamed_attr} = {protobuf_index}{proto_opt};\n", level) + else: + proto += padding(f"optional {datatype} A_{renamed_attr} = {protobuf_index}{proto_opt};\n", level) + + protobuf_index += 1 + + protobuf_tag_index = 10000 + + if "text" in tree: + # Note that if both V_ and T_ fields are present, V_ will be favoured + # if its presence returns true (since it's optional), otherwise T_ fields + # will be used. + text_tree = tree["text"] + text_type = generate_attribute_type(text_tree) + datatype = text_type["type"] + + if datatype == "oneof": + print("WARN: oneof of V_ not yet supported!") + elif datatype == "enum": + proto += padding(f"enum ValueEnum {{\n", level) + proto += enum_to_proto(text_type, level + 1, current_scope) + proto += padding("}\n", level) + proto += padding(f"optional ValueEnum V_value = {protobuf_tag_index};\n", level) + else: + proto += padding(f"optional {datatype} V_value = {protobuf_tag_index};\n", level) + + protobuf_tag_index += 1 + + for tag in tags: + renamed_tag = tag + proto_opt = "" + if re.search("[^a-zA-Z0-9_]", tag): + renamed_tag = re.sub("[^a-zA-Z0-9_]", "_", tag) + proto_opt += f" [(real_name) = '{tag}']" + + proto += padding(f"message {renamed_tag}Tag {{\n", level) + proto += define_tree_to_proto(tags[tag], level + 1) + proto += padding("}\n", level) + + specifier = "optional" + if (tags[tag].get("repeated", False)): + specifier = "repeated" + + if level != 0: + proto += padding(f"{specifier} {renamed_tag}Tag T_{renamed_tag} = {protobuf_tag_index}{proto_opt};\n", level) + + protobuf_tag_index += 1 + + return proto + +parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description="RelaxNG schema to protobuf converter") + +parser.add_argument('rngfile', help='Specify .rng file to process') + +parser.add_argument('protofile', help='Specify .proto file to output') + +parser.add_argument('--defines', nargs='*', default=[ 'rng_entrypoint' ], + help='Specify defines to be converted to equivalent protobuf messages',) + +args = parser.parse_args() + +allowed_defines = args.defines +infile = args.rngfile +outfile = args.protofile + +get_defines(infile) + +for define_name in allowed_defines: + define_trees[define_name] = {} + parse_define(define_table[define_name], define_trees[define_name], []) + +prologue = """\ +syntax = 'proto2'; +package libvirt; + +import 'xml_datatypes.proto'; +""" + +with open(outfile, "w") as out_file: + out_file.write(prologue) + + for define_name in allowed_defines: + out_file.write(define_tree_to_proto(define_trees[define_name], 0)) + out_file.write("\n") -- 2.34.1

This patch sets up most of the baseline code which will be used by various fuzzers as well as the build setup. proto_to_xml.cc includes logic to convert protobuf to XML. It does in a generalized fashion by using reflection APIs to recursively iterate through all protobuf fields and generating XML tags, attributes or text based on their identifiers and field types. This way, different protobuf definitions can be used with minimal modifications. proto_custom_datatypes.cc includes certain attribute datatypes that cannot be encapsulated by existing protobuf datatypes, so we implement various handlers to handle their XML serialization. run_fuzz is a helper script which sets up necessary libFuzzer flags and environment variables before executing the actual fuzzer binaries. Three protobuf definitions are provided which serve as a base for all XML domain fuzzers to follow: 1. xml_domain: Most of the XML domain format 2. xml_domain_disk_only: Only the <disk> definition 3. xml_domain_interface_only: Only the <interface> definition Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- meson.build | 44 ++++ meson_options.txt | 2 + tests/fuzz/meson.build | 59 +++++ tests/fuzz/proto_custom_datatypes.cc | 146 +++++++++++++ tests/fuzz/proto_custom_datatypes.h | 30 +++ tests/fuzz/proto_header_common.h | 43 ++++ tests/fuzz/proto_to_xml.cc | 205 ++++++++++++++++++ tests/fuzz/proto_to_xml.h | 28 +++ tests/fuzz/protos/meson.build | 36 +++ tests/fuzz/protos/xml_datatypes.proto | 72 ++++++ tests/fuzz/protos/xml_domain.proto | 62 ++++++ tests/fuzz/protos/xml_domain_disk_only.proto | 21 ++ .../protos/xml_domain_interface_only.proto | 21 ++ tests/fuzz/run_fuzz.in | 122 +++++++++++ tests/meson.build | 5 + 15 files changed, 896 insertions(+) create mode 100644 tests/fuzz/meson.build create mode 100644 tests/fuzz/proto_custom_datatypes.cc create mode 100644 tests/fuzz/proto_custom_datatypes.h create mode 100644 tests/fuzz/proto_header_common.h create mode 100644 tests/fuzz/proto_to_xml.cc create mode 100644 tests/fuzz/proto_to_xml.h create mode 100644 tests/fuzz/protos/meson.build create mode 100644 tests/fuzz/protos/xml_datatypes.proto create mode 100644 tests/fuzz/protos/xml_domain.proto create mode 100644 tests/fuzz/protos/xml_domain_disk_only.proto create mode 100644 tests/fuzz/protos/xml_domain_interface_only.proto create mode 100644 tests/fuzz/run_fuzz.in diff --git a/meson.build b/meson.build index 904524abc6..abc67e0062 100644 --- a/meson.build +++ b/meson.build @@ -2153,6 +2153,49 @@ if get_option('test_coverage_clang') endif +# fuzzing + +if get_option('fuzz').enabled() + add_languages('cpp') + cxx = meson.get_compiler('cpp') + + libprotobuf_mutator_dep = dependency('libprotobuf-mutator') + + if get_option('external_protobuf_dir') != '' + external_protobuf_dir = get_option('external_protobuf_dir') + protoc_prog = find_program(external_protobuf_dir / 'bin' / 'protoc') + + protobuf_inc = external_protobuf_dir / 'include' + protobuf_libdir = external_protobuf_dir / 'lib' + + protobuf_list_cmd = run_command('find', protobuf_libdir, '-name', '*.a', '-printf', '%f\n', check: true) + protobuf_lib_files = protobuf_list_cmd.stdout().strip().split('\n') + + protobuf_libs = [] + + foreach lib: protobuf_lib_files + protobuf_libs += cxx.find_library(lib.substring(3, -2), dirs: protobuf_libdir) + endforeach + + protobuf_dep = declare_dependency( + dependencies: [ protobuf_libs ], + include_directories: [ protobuf_inc ], + ) + else + protobuf_dep = dependency('protobuf') + protoc_prog = find_program('protoc') + endif + + if cc.has_argument('-fsanitize=fuzzer') and cc.has_argument('-fsanitize=fuzzer-no-link') + add_project_arguments('-fsanitize=fuzzer-no-link', language: [ 'c', 'cpp' ]) + else + error('Your compiler does not support libFuzzer') + endif + + conf.set('WITH_FUZZ', 1) +endif + + # Various definitions # Python3 < 3.7 treats the C locale as 7-bit only. We must force env vars so @@ -2389,6 +2432,7 @@ summary(win_summary, section: 'Windows', bool_yn: true) test_summary = { 'Expensive': use_expensive_tests, 'Coverage': coverage_flags.length() > 0, + 'Fuzzing': conf.has('WITH_FUZZ'), } summary(test_summary, section: 'Test suite', bool_yn: true) diff --git a/meson_options.txt b/meson_options.txt index 153e325cb5..b961192ee5 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,6 +15,8 @@ option('rpath', type: 'feature', value: 'auto', description: 'whether to include option('docdir', type: 'string', value: '', description: 'documentation installation directory') option('docs', type: 'feature', value: 'auto', description: 'whether to generate documentation') option('tests', type: 'feature', value: 'auto', description: 'whether to build and run tests') +option('fuzz', type: 'feature', value: 'auto', description: 'whether to build and run fuzzing engines') +option('external_protobuf_dir', type: 'string', value: '', description: 'external protobuf directory for fuzzer compilation') # build dependencies options diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build new file mode 100644 index 0000000000..d0488c3e36 --- /dev/null +++ b/tests/fuzz/meson.build @@ -0,0 +1,59 @@ +fuzz_dep = declare_dependency( + dependencies: [ + tests_dep, + libprotobuf_mutator_dep, + protobuf_dep, + ], + include_directories: [ + test_dir, + ], + ) + +subdir('protos') + +available_fuzzers = '' +xml_fuzzers = [] + +fuzz_autogen_xml_domain_lib = static_library( + 'fuzz_autogen_xml_domain_lib', + [ + autogen_xml_domain_proto_src, + xml_datatypes_proto_src, + 'proto_custom_datatypes.cc', + ], + dependencies: [ fuzz_dep ], +) + +fuzz_autogen_xml_domain_dep = declare_dependency( + link_whole: [ fuzz_autogen_xml_domain_lib ], + include_directories: [ + fuzz_autogen_xml_domain_lib.private_dir_include(), + ] +) + +foreach fuzzer: xml_fuzzers + xml_domain_fuzz = executable(fuzzer['name'], + fuzzer['src'], + dependencies: [ fuzz_dep, fuzzer['deps'] ], + cpp_args: [ '-fsanitize=fuzzer', fuzzer['macro'] ], + link_args: [ '-fsanitize=fuzzer', libvirt_no_indirect ], + link_with: fuzzer['libs'], + ) + + available_fuzzers += '"' + fuzzer['name'] + '"' + ',' +endforeach + +run_conf = configuration_data({ + 'abs_builddir': meson.project_build_root(), + 'available_fuzzers': available_fuzzers, + 'sanitizers': get_option('b_sanitize'), + 'coverage_clang': get_option('test_coverage_clang'), +}) + +configure_file( + input: 'run_fuzz.in', + output: '@BASENAME@', + configuration: run_conf, +) + +run_command('chmod', 'a+x', meson.current_build_dir() / 'run_fuzz', check: true) diff --git a/tests/fuzz/proto_custom_datatypes.cc b/tests/fuzz/proto_custom_datatypes.cc new file mode 100644 index 0000000000..d89a6d4f59 --- /dev/null +++ b/tests/fuzz/proto_custom_datatypes.cc @@ -0,0 +1,146 @@ +/* + * proto_custom_datatypes.cc: Custom protobuf-based datatype handlers + * + * 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 "xml_datatypes.pb.h" +#include "port/protobuf.h" + +#include <unordered_map> + +using google::protobuf::Message; +using google::protobuf::Reflection; +using google::protobuf::EnumDescriptor; + +typedef std::string (*typeHandlerPtr)(const Message &); +std::string emulator = ""; + +static +std::string convertCPUSet(const Message &message) +{ + std::string value; + const libvirt::CPUSet &cpuset = (libvirt::CPUSet &) message; + uint64_t bitmap = cpuset.bitmap(); + + int cpu = 0; + for (uint8_t bit = bitmap & 1; bitmap != 0; bitmap = bitmap >> 1, bit = bitmap & 1, cpu++) + { + if (bit) { + if (value.empty()) + value += std::to_string(cpu); + else + value += "," + std::to_string(cpu); + } + } + + return value; +} + + +static +std::string convertEmulatorString(const Message &message) +{ + (void) message; /* Silence warning about unused parameter */ + return emulator; +} + + +static +std::string convertIPAddr(const Message &message) { + char value[64] = {0}; + const libvirt::IPAddr &ip_addr = (libvirt::IPAddr &) message; + + if (ip_addr.ipv6() == true) { + sprintf(value, "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x", + (uint16_t) ip_addr.lo_qword() & 0xffff, + (uint16_t) (ip_addr.lo_qword() >> 16) & 0xffff, + (uint16_t) (ip_addr.lo_qword() >> 32) & 0xffff, + (uint16_t) (ip_addr.lo_qword() >> 48) & 0xffff, + (uint16_t) ip_addr.hi_qword() & 0xffff, + (uint16_t) (ip_addr.hi_qword() >> 16) & 0xffff, + (uint16_t) (ip_addr.hi_qword() >> 32) & 0xffff, + (uint16_t) (ip_addr.hi_qword() >> 48) & 0xffff); + } else { + sprintf(value, "%d.%d.%d.%d", + (uint8_t) ip_addr.lo_qword() & 0xff, + (uint8_t) (ip_addr.lo_qword() >> 8) & 0xff, + (uint8_t) (ip_addr.lo_qword() >> 16) & 0xff, + (uint8_t) (ip_addr.lo_qword() >> 24) & 0xff); + } + + return std::string(value); +} + + +static +std::string convertMacAddr(const Message &message) { + char value[64] = {0}; + const libvirt::MacAddr &mac_addr = (libvirt::MacAddr &) message; + + sprintf(value, "%02x:%02x:%02x:%02x:%02x:%02x", + (uint8_t) mac_addr.qword() & 0xff, + (uint8_t) (mac_addr.qword() >> 8) & 0xff, + (uint8_t) (mac_addr.qword() >> 16) & 0xff, + (uint8_t) (mac_addr.qword() >> 24) & 0xff, + (uint8_t) (mac_addr.qword() >> 32) & 0xff, + (uint8_t) (mac_addr.qword() >> 40) & 0xff); + + return std::string(value); +} + + +static +std::string convertDiskTarget(const Message &message) +{ + std::string value; + const libvirt::TargetDev &target_dev = (libvirt::TargetDev &) message; + const EnumDescriptor *enum_desc = libvirt::TargetDev_TargetDevEnum_descriptor(); + std::string prefix = enum_desc->FindValueByNumber(target_dev.prefix())->name(); + + value = prefix + "a"; + + return value; +} + + +std::unordered_map<std::string, typeHandlerPtr> type_handler_table = { + {"libvirt.CPUSet", convertCPUSet}, + {"libvirt.EmulatorString", convertEmulatorString}, + {"libvirt.IPAddr", convertIPAddr}, + {"libvirt.MacAddr", convertMacAddr}, + {"libvirt.TargetDev", convertDiskTarget}, +}; + + +void handleCustomDatatype(const Message &message, + std::string tag, + bool is_attr, + std::string &xml) +{ + std::string type_name = message.GetTypeName(); + std::string value = ""; + + if (type_handler_table.find(type_name) != type_handler_table.end()) { + value = type_handler_table[type_name](message); + + if (is_attr) + xml += " " + tag + "=" + "'" + value + "'"; + else + xml += value; + } +} diff --git a/tests/fuzz/proto_custom_datatypes.h b/tests/fuzz/proto_custom_datatypes.h new file mode 100644 index 0000000000..9446b6235b --- /dev/null +++ b/tests/fuzz/proto_custom_datatypes.h @@ -0,0 +1,30 @@ +/* + * proto_custom_datatypes.h: Custom protobuf-based datatype handlers + * + * 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/>. + */ + +#pragma once + +#include "port/protobuf.h" + +using google::protobuf::Message; + +void handleCustomDatatype(const Message &message, + std::string tag, + bool is_attr, + std::string &xml); diff --git a/tests/fuzz/proto_header_common.h b/tests/fuzz/proto_header_common.h new file mode 100644 index 0000000000..5ee510896d --- /dev/null +++ b/tests/fuzz/proto_header_common.h @@ -0,0 +1,43 @@ +/* + * proto_header_common.h: Protobuf header selection and common code + * + * 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/>. + */ + +#pragma once + +#pragma GCC diagnostic ignored "-Wignored-attributes" +#pragma GCC diagnostic ignored "-Wextern-c-compat" + +#ifdef XML_DOMAIN +#include "xml_domain.pb.h" +#endif + +#ifdef XML_DOMAIN_DISK_ONLY +#include "xml_domain_disk_only.pb.h" +#endif + +#ifdef XML_DOMAIN_INTERFACE_ONLY +#include "xml_domain_interface_only.pb.h" +#endif + + +#define FUZZ_COMMON_INIT(...) \ + if (virErrorInitialize() < 0) \ + exit(EXIT_FAILURE); \ + if (g_getenv("LPM_EXE_PATH")) \ + virFileActivateDirOverrideForProg(g_getenv("LPM_EXE_PATH")); diff --git a/tests/fuzz/proto_to_xml.cc b/tests/fuzz/proto_to_xml.cc new file mode 100644 index 0000000000..13393ecb34 --- /dev/null +++ b/tests/fuzz/proto_to_xml.cc @@ -0,0 +1,205 @@ +/* + * proto_to_xml.cc: Protobuf to XML converter + * + * 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 "proto_header_common.h" +#include "port/protobuf.h" +#include "proto_custom_datatypes.h" + + +using google::protobuf::Message; +using google::protobuf::Descriptor; +using google::protobuf::FieldDescriptor; +using google::protobuf::EnumValueDescriptor; +using google::protobuf::OneofDescriptor; +using google::protobuf::Reflection; + + +static void +convertProtoFieldToXML(const Message &message, + const FieldDescriptor *field, + std::string tag, + bool is_attr, + std::string &xml) +{ + const Reflection *reflection = message.GetReflection(); + const EnumValueDescriptor *enum_desc; + std::string value = ""; + std::string alternate_value; + bool value_set = false; + + switch (field->cpp_type()) { + case FieldDescriptor::CPPTYPE_MESSAGE: + { + const Message &field_message = reflection->GetMessage(message, field); + handleCustomDatatype(field_message, tag, is_attr, xml); + return; + } + break; + case FieldDescriptor::CPPTYPE_ENUM: + enum_desc = reflection->GetEnum(message, field); + + /* Use extension to fill certain XML enum values containing special characters */ + alternate_value = enum_desc->options().GetExtension(libvirt::real_value); + + if (!alternate_value.empty()) { + value = alternate_value; + } else { + value = enum_desc->name(); + } + + value_set = true; + break; + case FieldDescriptor::CPPTYPE_BOOL: + value = reflection->GetBool(message, field) ? "yes" : "no"; + value_set = true; + break; + case FieldDescriptor::CPPTYPE_UINT32: + value = std::to_string(reflection->GetUInt32(message, field)); + value_set = true; + break; + case FieldDescriptor::CPPTYPE_UINT64: + value = std::to_string(reflection->GetUInt64(message, field)); + value_set = true; + break; + case FieldDescriptor::CPPTYPE_STRING: + value = reflection->GetString(message, field); + value_set = true; + break; + default: + break; + } + + if (value_set) { + if (is_attr) + xml += " " + tag + "='" + value + "'"; + else + xml += value; + } +} + + +static int +convertProtoToXMLInternal(const Message &message, + std::string &xml, + bool root) +{ + const Descriptor *descriptor = message.GetDescriptor(); + const Reflection *reflection = message.GetReflection(); + size_t field_count = descriptor->field_count(); + int children = 0; + bool opening_tag_close = false; + size_t i, j; + size_t repeated_size = 0; + + for (i = 0; i < field_count; i++) { + const FieldDescriptor *field = descriptor->field(i); + std::string field_name = field->name(); + std::string tag = field_name.substr(2, std::string::npos); + char field_type = field_name[0]; + std::string alternate_name = field->options().GetExtension(libvirt::real_name); + + /* Fields inside oneof are prefixed with A_OPTXX_ */ + if (tag.substr(0, 3) == "OPT") + tag = tag.substr(6, std::string::npos); + + /* Use extension to generate XML tags with names having special characters */ + if (!alternate_name.empty()) + tag = alternate_name; + + if (!field->is_repeated() && !reflection->HasField(message, field)) { + continue; + } + + switch (field_type) { + /* Handle XML attributes/properties */ + case 'A': + /* Attributes must come before inner tags in the protobuf definition */ + assert(!opening_tag_close); + + convertProtoFieldToXML(message, field, tag, true, xml); + break; + + /* Handle inner tags of current XML tag */ + case 'T': + /* We don't want to accidentally close off the tag on empty repeated fields */ + if (field->is_repeated()) { + repeated_size = reflection->FieldSize(message, field); + if (repeated_size == 0) + continue; + } + + if (!opening_tag_close && !root) { + xml += ">\n"; + opening_tag_close = true; + } + + if (field->is_repeated()) { + for (j = 0; j < repeated_size; j++) { + const Message &field_message = reflection->GetRepeatedMessage(message, field, j); + children += 1; + xml += "<" + tag; + int inner_children = convertProtoToXMLInternal(field_message, xml, false); + if (inner_children == 0) { + xml += "/>\n"; + } else { + xml += "</" + tag + ">\n"; + } + } + } else { + const Message &field_message = reflection->GetMessage(message, field); + children += 1; + xml += "<" + tag; + int inner_children = convertProtoToXMLInternal(field_message, xml, false); + + if (inner_children == 0) { + xml += "/>\n"; + } else { + xml += "</" + tag + ">\n"; + } + } + break; + + /* Handle XML text nodes. Eg: <tag>(text here)</tag> */ + case 'V': + if (!opening_tag_close && !root) { + xml += ">"; + opening_tag_close = true; + } + assert(children == 0); + children = 1; + + convertProtoFieldToXML(message, field, "", false, xml); + + return children; + + default: + break; + } + } + + return children; +} + + +void convertProtoToXML(const libvirt::MainObj &message, + std::string &xml) +{ + convertProtoToXMLInternal(message, xml, true); +} diff --git a/tests/fuzz/proto_to_xml.h b/tests/fuzz/proto_to_xml.h new file mode 100644 index 0000000000..7fe9597a19 --- /dev/null +++ b/tests/fuzz/proto_to_xml.h @@ -0,0 +1,28 @@ +/* + * proto_to_xml.h: Protobuf to XML converter + * + * 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/>. + */ + +#pragma once + +#include "proto_header_common.h" +#include "port/protobuf.h" + + +void convertProtoToXML(const libvirt::MainObj &message, + std::string &xml); diff --git a/tests/fuzz/protos/meson.build b/tests/fuzz/protos/meson.build new file mode 100644 index 0000000000..42c3a7f6a9 --- /dev/null +++ b/tests/fuzz/protos/meson.build @@ -0,0 +1,36 @@ +protos = [ + 'xml_datatypes.proto', + 'xml_domain.proto', + 'xml_domain_disk_only.proto', + 'xml_domain_interface_only.proto', +] + +autogen_proto_xml_domain_proto = custom_target('autogen_xml_domain.proto', + output : 'autogen_xml_domain.proto', + input : meson.project_source_root() / 'src' / 'conf' / 'schemas' / 'domain.rng', + command : [relaxng_to_proto_prog, '@INPUT@', '@OUTPUT@'], +) + +protoc_generator = generator(protoc_prog, + output: [ + '@BASENAME@.pb.cc', + '@BASENAME@.pb.h', + ], + arguments: [ + '--proto_path=@CURRENT_SOURCE_DIR@', + '--proto_path=@BUILD_ROOT@/tests/fuzz/protos/', + '--cpp_out=@BUILD_DIR@', + '@INPUT@', + ], + depends: [ + autogen_proto_xml_domain_proto, + ], +) + +autogen_xml_domain_proto_src = protoc_generator.process(autogen_proto_xml_domain_proto) + +foreach proto: protos + proto_src_name = proto.split('.')[0].underscorify() + proto_src = protoc_generator.process(proto) + set_variable('@0@_proto_src'.format(proto_src_name), proto_src) +endforeach diff --git a/tests/fuzz/protos/xml_datatypes.proto b/tests/fuzz/protos/xml_datatypes.proto new file mode 100644 index 0000000000..1229b9810f --- /dev/null +++ b/tests/fuzz/protos/xml_datatypes.proto @@ -0,0 +1,72 @@ +syntax = "proto2"; +package libvirt; + +import "google/protobuf/descriptor.proto"; + +/* + * Use EnumValueOptions extension to insert XML attribute values that do + * contain characters outside [A-Za-z0-9_] + */ + +extend google.protobuf.EnumValueOptions { + optional string real_value = 10000; +} + +/* + * Use FieldOptions extension to create XML tags/attributes + * with identifiers containing characters outside [A-Za-z0-9_] + */ + +extend google.protobuf.FieldOptions { + optional string real_name = 10000; +} + +enum Switch { + on = 0; + off = 1; +} + +/* If we use string, it could blow up the size of the protobuf in-memory */ +enum DummyString { + dummy = 0; + dummy2 = 1; + dummy3 = 2; + dummy4 = 3; +} + +enum DummyUUID { + dummy_uuid = 0 [(real_value) = "df6fdea1-10c3-474c-ae62-e63def80de0b"]; +} + +message IPAddr { + required bool ipv6 = 1; + required uint64 lo_qword = 2; + required uint64 hi_qword = 3; +} + +message MacAddr { + required uint64 qword = 1; +} + +message TargetDev { + enum TargetDevEnum { + hd = 0; + sd = 1; + vd = 2; + xvd = 3; + ubd = 4; + } + required TargetDevEnum prefix = 1; +} + +enum DummyPath { + devnull = 0 [(real_value) = "/dev/null"]; + devzero = 1 [(real_value) = "/dev/zero"]; + invalidpath = 2 [(real_value) = "/this/path/doesnt/exist"]; +} + +message CPUSet { + required uint64 bitmap = 1; +} + +message EmulatorString {} diff --git a/tests/fuzz/protos/xml_domain.proto b/tests/fuzz/protos/xml_domain.proto new file mode 100644 index 0000000000..9720af70d3 --- /dev/null +++ b/tests/fuzz/protos/xml_domain.proto @@ -0,0 +1,62 @@ +syntax = "proto2"; + +import "autogen_xml_domain.proto"; +import "xml_datatypes.proto"; + +package libvirt; + +/* + * <devices> container tag + * For more effective coverage, it might be a good idea to comment + * out some protobuf fields and fuzz only a few devices at a time. + */ + +message devicesTag { + repeated domainTag.devicesTag.soundTag T_sound = 1; + repeated domainTag.devicesTag.filesystemTag T_filesystem = 2; + repeated domainTag.devicesTag.inputTag T_input = 3; + repeated domainTag.devicesTag.diskTag T_disk = 4; + repeated domainTag.devicesTag.interfaceTag T_interface = 5; + repeated domainTag.devicesTag.graphicsTag T_graphics = 6; + repeated domainTag.devicesTag.serialTag T_serial = 7; + repeated domainTag.devicesTag.parallelTag T_parallel = 8; + repeated domainTag.devicesTag.channelTag T_channel = 9; + repeated domainTag.devicesTag.consoleTag T_console = 10; + repeated domainTag.devicesTag.controllerTag T_controller = 11; + repeated domainTag.devicesTag.videoTag T_video = 12; + repeated domainTag.devicesTag.rngTag T_rng = 13; + repeated domainTag.devicesTag.watchdogTag T_watchdog = 14; + repeated domainTag.devicesTag.memballoonTag T_memballoon = 15; + repeated domainTag.devicesTag.smartcardTag T_smartcard = 16; + repeated domainTag.devicesTag.redirdevTag T_redirdev = 17; + repeated domainTag.devicesTag.audioTag T_audio = 18; + repeated domainTag.devicesTag.cryptoTag T_crypto = 19; + repeated domainTag.devicesTag.panicTag T_panic = 20; + repeated domainTag.devicesTag.tpmTag T_tpm = 21; + repeated domainTag.devicesTag.shmemTag T_shmem = 22; + repeated domainTag.devicesTag.hostdevTag T_hostdev = 23; + repeated domainTag.devicesTag.leaseTag T_lease = 24; + repeated domainTag.devicesTag.redirfilterTag T_redirfilter = 25; + repeated domainTag.devicesTag.iommuTag T_iommu = 26; + repeated domainTag.devicesTag.vsockTag T_vsock = 27; + repeated domainTag.devicesTag.nvramTag T_nvram = 28; + + message EmulatorTag { + required EmulatorString V_value = 1; + } + required EmulatorTag T_emulator = 1000; +} + +/* Full XML Domain */ + +message MainObj { + optional domainTag.clockTag T_clock = 1; + optional domainTag.vcpuTag T_vcpu = 2; + optional domainTag.vcpusTag T_vcpus = 3; + optional domainTag.cpuTag T_cpu = 4; + optional domainTag.cputuneTag T_vcputune = 5; + optional domainTag.memtuneTag T_memtune = 6; + optional domainTag.blkiotuneTag T_blkiotune = 7; + + required devicesTag T_devices = 1000; +} diff --git a/tests/fuzz/protos/xml_domain_disk_only.proto b/tests/fuzz/protos/xml_domain_disk_only.proto new file mode 100644 index 0000000000..a6af43003c --- /dev/null +++ b/tests/fuzz/protos/xml_domain_disk_only.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +import "autogen_xml_domain.proto"; +import "xml_datatypes.proto"; + +package libvirt; + +message devicesTag { + repeated domainTag.devicesTag.diskTag T_disk = 4; + + message EmulatorTag { + required EmulatorString V_value = 1; + } + required EmulatorTag T_emulator = 1000; +} + +/* Full XML Domain */ + +message MainObj { + required devicesTag T_devices = 1000; +} diff --git a/tests/fuzz/protos/xml_domain_interface_only.proto b/tests/fuzz/protos/xml_domain_interface_only.proto new file mode 100644 index 0000000000..3bac94bf79 --- /dev/null +++ b/tests/fuzz/protos/xml_domain_interface_only.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +import "autogen_xml_domain.proto"; +import "xml_datatypes.proto"; + +package libvirt; + +message devicesTag { + repeated domainTag.devicesTag.interfaceTag T_interface = 5; + + message EmulatorTag { + required EmulatorString V_value = 1; + } + required EmulatorTag T_emulator = 1000; +} + +/* Full XML Domain */ + +message MainObj { + required devicesTag T_devices = 1000; +} diff --git a/tests/fuzz/run_fuzz.in b/tests/fuzz/run_fuzz.in new file mode 100644 index 0000000000..da3c7935b7 --- /dev/null +++ b/tests/fuzz/run_fuzz.in @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys + +available_fuzzers = [ @available_fuzzers@ ] +abs_builddir = "@abs_builddir@" +fuzz_dir = f"{abs_builddir}/tests/fuzz/" +llvm_symbolizer_path = f"{fuzz_dir}/llvm-symbolizer-wrapper" + +sanitizers = "@sanitizers@" +coverage_clang = @coverage_clang@ + +parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description="Fuzzing Helper") + +parser.add_argument('fuzzer', + help='Select fuzzer binary to run', + choices=available_fuzzers) + +parser.add_argument('--testcase', nargs='+', + help='Individual test case') + +parser.add_argument('--corpus', + help='Corpus Directory') + +parser.add_argument('--dump-xml', action='store_true', + help='Log XML test cases') + +parser.add_argument('--format-xml', action='store_true', + help='Run XML domain format functions') + +parser.add_argument('--arch', + help='XML domain architecture to fuzz') + +parser.add_argument('--llvm-profile-file', + help='Collect coverage report') + +parser.add_argument('--total-time', type=int, + help='Fuzz for specific amount of time (in seconds)') + +parser.add_argument('--timeout', type=int, + help='Max duration for individual test case (in seconds)') + +parser.add_argument('-j', '--jobs', type=int, default=1, + help='Run parallel fuzzers') + +parser.add_argument('--parallel-mode', choices=["jobs", "fork"], default="jobs", + help='Set parallel fuzzing method') + +parser.add_argument('--use-value-profile', action='store_true', + help='Use value profile') + +parser.add_argument('--libfuzzer-options', + help='Pass additional raw libFuzzer flags') + +args = parser.parse_args() + +process_args = [fuzz_dir + args.fuzzer] +env = os.environ + +if args.corpus and args.testcase: + print("--testcase and --corpus can't be used together") + sys.exit(1) + +if args.corpus: + process_args.extend([args.corpus]) + +if args.testcase: + process_args.extend(args.testcase) + +if args.dump_xml: + env["LPM_XML_DUMP_INPUT"] = "YES" + +if args.format_xml: + env["LPM_XML_FORMAT_ENABLE"] = "YES" + +if args.arch: + env["LPM_FUZZ_ARCH"] = args.arch + +if args.llvm_profile_file: + if not coverage_clang: + print("Please recompile your build with -Dtest_coverage_clang=true") + sys.exit(1) + env["LLVM_PROFILE_FILE"] = args.llvm_profile_file + + if not args.corpus: + print("Coverage report requires --corpus. If you do not have a corpus, do normal fuzzing to prepare one.") + sys.exit(1) + + process_args.extend(["-runs=0"]) + +if args.total_time: + process_args.extend([f"-max_total_time={str(args.total_time)}"]) + +if args.timeout: + process_args.extend([f"-timeout={str(args.timeout)}"]) + +if args.use_value_profile: + process_args.extend(["-use_value_profile=1"]) + +if args.jobs > 1: + if args.parallel_mode == "fork": + process_args.extend([f"-fork={args.jobs}", "-ignore_crashes=1", "-ignore_ooms=1", "-ignore_timeouts=1"]) + elif args.parallel_mode == "jobs": + process_args.extend([f"-jobs={args.jobs}", f"-workers={args.jobs}"]) + +exe_path = fuzz_dir + args.fuzzer + +# If libvirt is not installed in system dir, some fuzzers (QEMU, etc.) will fail +env["LPM_EXE_PATH"] = exe_path + +process_args.extend(["-print_funcs=-1"]) + +if args.libfuzzer_options: + process_args.extend([x for x in args.libfuzzer_options.split(' ') if x != '']) + +print("Selected fuzzer:", args.fuzzer) +print("Active sanitizers:", sanitizers) + +os.execvpe(exe_path, process_args, env) diff --git a/tests/meson.build b/tests/meson.build index 2f1eda1f95..18743ac45e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -34,6 +34,7 @@ tests_dep = declare_dependency( ), ) +test_dir = include_directories('.') tests_env = [ 'abs_builddir=@0@'.format(meson.current_build_dir()), 'abs_srcdir=@0@'.format(meson.current_source_dir()), @@ -750,6 +751,10 @@ endforeach testenv = runutf8 testenv += 'VIR_TEST_FILE_ACCESS=1' +if conf.has('WITH_FUZZ') + subdir('fuzz') +endif + add_test_setup( 'access', env: testenv, -- 2.34.1

This patch implements the harness for the QEMU driver to fuzz XML parsing and command line generation. The harness uses existing test driver setup and test utilities under testutilsqemu and qemuxmlconftest. Three binaries are generated based on three protobuf definitions: xml_domain, xml_domain_disk_only and xml_domain_interface_only. This is useful to acheive deeper coverage of interfaces and disk definitions. The fuzzers also have the fake network, storage and secret drivers set up. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- tests/fuzz/meson.build | 20 +++ tests/fuzz/proto_to_xml.cc | 18 ++ tests/fuzz/proto_to_xml.h | 3 + tests/fuzz/qemu_xml_domain_fuzz.cc | 277 +++++++++++++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 tests/fuzz/qemu_xml_domain_fuzz.cc diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build index d0488c3e36..12f9a719f2 100644 --- a/tests/fuzz/meson.build +++ b/tests/fuzz/meson.build @@ -31,6 +31,26 @@ fuzz_autogen_xml_domain_dep = declare_dependency( ] ) +if conf.has('WITH_QEMU') + fuzzer_src = [ + 'qemu_xml_domain_fuzz.cc', + 'proto_to_xml.cc', + ] + + qemu_libs = [ + test_qemu_driver_lib, + test_utils_lib, + test_utils_qemu_lib, + libvirt_lib, + ] + + xml_fuzzers += [ + { 'name': 'qemu_xml_domain_fuzz', 'src': [ fuzzer_src, xml_domain_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN', 'deps': [ fuzz_autogen_xml_domain_dep ] }, + { 'name': 'qemu_xml_domain_fuzz_disk', 'src': [ fuzzer_src, xml_domain_disk_only_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN_DISK_ONLY', 'deps': [ fuzz_autogen_xml_domain_dep ] }, + { 'name': 'qemu_xml_domain_fuzz_interface', 'src': [ fuzzer_src, xml_domain_interface_only_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN_INTERFACE_ONLY', 'deps': [ fuzz_autogen_xml_domain_dep ] }, + ] +endif + foreach fuzzer: xml_fuzzers xml_domain_fuzz = executable(fuzzer['name'], fuzzer['src'], diff --git a/tests/fuzz/proto_to_xml.cc b/tests/fuzz/proto_to_xml.cc index 13393ecb34..36ad1028b1 100644 --- a/tests/fuzz/proto_to_xml.cc +++ b/tests/fuzz/proto_to_xml.cc @@ -198,6 +198,24 @@ convertProtoToXMLInternal(const Message &message, } +void convertProtoToQEMUXMLDomain(const libvirt::MainObj &message, + std::string arch, + std::string &xml) +{ + xml = "<domain type='qemu'>\n" + " <name>MyGuest</name>\n" + " <uuid>4dea22b3-1d52-d8f3-2516-782e98ab3fa0</uuid>\n" + " <os>\n" + " <type arch='" + arch + "'>hvm</type>\n" + " </os>\n" + " <memory>4096</memory>\n"; + + convertProtoToXMLInternal(message, xml, true); + + xml += "</domain>\n"; +} + + void convertProtoToXML(const libvirt::MainObj &message, std::string &xml) { diff --git a/tests/fuzz/proto_to_xml.h b/tests/fuzz/proto_to_xml.h index 7fe9597a19..a87547a319 100644 --- a/tests/fuzz/proto_to_xml.h +++ b/tests/fuzz/proto_to_xml.h @@ -24,5 +24,8 @@ #include "port/protobuf.h" +void convertProtoToQEMUXMLDomain(const libvirt::MainObj &message, + std::string arch, + std::string &xml); void convertProtoToXML(const libvirt::MainObj &message, std::string &xml); diff --git a/tests/fuzz/qemu_xml_domain_fuzz.cc b/tests/fuzz/qemu_xml_domain_fuzz.cc new file mode 100644 index 0000000000..e1618561cf --- /dev/null +++ b/tests/fuzz/qemu_xml_domain_fuzz.cc @@ -0,0 +1,277 @@ +/* + * qemu_xml_domain_fuzz.cc: QEMU XML domain 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> + +#include <unistd.h> +#include <sys/types.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdarg.h> + +extern "C" { +#include "internal.h" +#include "viralloc.h" +#include "viridentity.h" +#include "qemu/qemu_block.h" +#include "qemu/qemu_capabilities.h" +#include "qemu/qemu_domain.h" +#include "qemu/qemu_migration.h" +#include "qemu/qemu_process.h" +#include "qemu/qemu_slirp.h" +#include "datatypes.h" +#include "conf/storage_conf.h" +#include "virfilewrapper.h" +#include "configmake.h" + +#include "testutils.h" +#include "testutilsqemu.h" +} + +#include "port/protobuf.h" +#include "proto_to_xml.h" +#include "src/libfuzzer/libfuzzer_macro.h" + +uint64_t parse_pass = 0; +uint64_t format_pass = 0; +uint64_t command_line_pass = 0; +uint64_t success = 0; + +extern std::string emulator; + +bool enable_xml_dump = false; +bool enable_xml_format = false; + +static virCommand * +fuzzGenerateCommandLine(virQEMUDriver *driver, + virDomainObj *vm) +{ + if (qemuProcessCreatePretendCmdPrepare(driver, vm, NULL, + VIR_QEMU_PROCESS_START_COLD) < 0) + return NULL; + + return qemuProcessCreatePretendCmdBuild(vm, NULL); +} + + +static void +fuzzXMLToCommandLine(virQEMUDriver *driver, + const char *xml_string) +{ + virDomainDef *def = NULL; + g_autofree char *formatted_xml = NULL; + virDomainObj *vm = NULL; + virCommand *command = NULL; + + parse_pass++; + if (!(def = virDomainDefParseString(xml_string, driver->xmlopt, NULL, + VIR_DOMAIN_DEF_PARSE_INACTIVE))) + goto cleanup; + + if (enable_xml_format) { + format_pass++; + if (!(formatted_xml = virDomainDefFormat(def, driver->xmlopt, + VIR_DOMAIN_DEF_FORMAT_SECURE))) + goto cleanup; + } + + if (!(vm = virDomainObjNew(driver->xmlopt))) + goto cleanup; + + vm->def = def; + + command_line_pass++; + if (!(command = fuzzGenerateCommandLine(driver, vm))) + goto cleanup; + + success++; + + cleanup: + virCommandFree(command); + + if (vm) { + vm->def = NULL; + virObjectUnref(vm); + } + + virDomainDefFree(def); +} + + +static int +setupTestInfo(testQemuInfo *info, struct testQemuConf *testConf, ...) +{ + va_list ap; + + info->name = "fuzz"; + info->conf = testConf; + + va_start(ap, testConf); + testQemuInfoSetArgs(info, ap); + va_end(ap); + + if (testQemuInfoInitArgs(info) < 0) + return -1; + + return 0; +} + +/* Fake drivers identical to the one in the test suite. + * TODO: Try to replace some of the handlers such that file I/O is avoided. + */ + +static virStoragePoolPtr +fuzzStoragePoolLookupByName(virConnectPtr conn, + const char *name G_GNUC_UNUSED) +{ + return fakeStoragePoolLookupByName(conn, "pool-fs"); +} + + +static virNetworkPtr +fuzzNetworkLookupByName(virConnectPtr conn, + const char *name G_GNUC_UNUSED) +{ + return fakeNetworkLookupByName(conn, "nat-network;plug-network-basic"); +} + +static virStorageDriver fakeStorageDriver = { + .storagePoolLookupByName = fuzzStoragePoolLookupByName, + .storagePoolGetXMLDesc = fakeStoragePoolGetXMLDesc, + .storageVolLookupByName = fakeStorageVolLookupByName, + .storageVolGetInfo = fakeStorageVolGetInfo, + .storageVolGetPath = fakeStorageVolGetPath, + .storagePoolIsActive = fakeStoragePoolIsActive, +}; + +static virNetworkDriver fakeNetworkDriver = { + .networkLookupByName = fuzzNetworkLookupByName, + .networkGetXMLDesc = fakeNetworkGetXMLDesc, + .networkPortCreateXML = fakeNetworkPortCreateXML, + .networkPortGetXMLDesc = fakeNetworkPortGetXMLDesc, +}; + +static virSecretDriver fakeSecretDriver = { + .connectNumOfSecrets = NULL, + .connectListSecrets = NULL, + .secretLookupByUUID = fakeSecretLookupByUUID, + .secretLookupByUsage = fakeSecretLookupByUsage, + .secretDefineXML = NULL, + .secretGetXMLDesc = NULL, + .secretSetValue = NULL, + .secretGetValue = fakeSecretGetValue, + .secretUndefine = NULL, +}; + +DEFINE_PROTO_FUZZER(const libvirt::MainObj &message) +{ + static GHashTable *capscache = virHashNew(virObjectUnref); + static GHashTable *capslatest = testQemuGetLatestCaps(); + static GHashTable *qapiSchemaCache = virHashNew((GDestroyNotify) g_hash_table_unref); + + static struct testQemuConf testConf = { .capscache = capscache, + .capslatest = capslatest, + .qapiSchemaCache = qapiSchemaCache }; + + static virQEMUDriver driver; + static virConnect *conn; + static bool initialized = false; + + static testQemuInfo *info = g_new0(testQemuInfo, 1); + + static const char *arch_env = g_getenv("LPM_FUZZ_ARCH"); + static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT"); + static const char *format_xml_env = g_getenv("LPM_XML_FORMAT_ENABLE"); + + static std::string arch = ""; + + std::string xml = ""; + int ret = 0; + + /* + * One-time setup of QEMU driver and testQemuInfo. Re-running them in every + * iteration incurs a significant penalty to the speed of the fuzzer. + */ + if (!initialized) { + FUZZ_COMMON_INIT(); + + if (qemuTestDriverInit(&driver) < 0) + exit(EXIT_FAILURE); + + if (!(conn = virGetConnect())) + exit(EXIT_FAILURE); + + conn->networkDriver = &fakeNetworkDriver; + conn->storageDriver = &fakeStorageDriver; + conn->secretDriver = &fakeSecretDriver; + + virSetConnectNetwork(conn); + virSetConnectStorage(conn); + virSetConnectSecret(conn); + + if (arch_env) { + arch = arch_env; + } else { + arch = "x86_64"; + } + + ret = setupTestInfo(info, &testConf, ARG_CAPS_ARCH, arch.c_str(), + ARG_CAPS_VER, "latest", + ARG_END); + + emulator = "/usr/bin/qemu-system-" + arch; + + if (ret < 0) { + printf("Unable to set up test information (invalid arch?)\n"); + exit(EXIT_FAILURE); + } + + /* QEMU Capabilities Cache Setup */ + virFileCacheClear(driver.qemuCapsCache); + if (qemuTestCapsCacheInsert(driver.qemuCapsCache, info->qemuCaps) < 0) + exit(EXIT_FAILURE); + + /* Enable printing of XML to stdout (useful for debugging crashes) */ + if (dump_xml_env && STREQ(dump_xml_env, "YES")) + enable_xml_dump = true; + + /* Enable fuzzing of XML formatting */ + if (format_xml_env && STREQ(format_xml_env, "YES")) + enable_xml_format = true; + + initialized = true; + } + + convertProtoToQEMUXMLDomain(message, arch, xml); + + if (enable_xml_dump) + printf("%s\n", xml.c_str()); + + fuzzXMLToCommandLine(&driver, xml.c_str()); + + if (parse_pass % 1000 == 0) + printf("[FUZZ METRICS] Parse: %lu, Format: %lu, Cmdline: %lu, Success: %lu\n", + parse_pass, format_pass, command_line_pass, success); +} -- 2.34.1

This patch implements the harness for hotplugging devices under the QEMU driver. The fuzzer exercises both attachment and detachment of various devices. It uses its own proto file xml_hotplug to fuzz only one device type at a time. Monitor setup is done such that QMP commands are processed indefinitely and removal wait times are eliminated. A table of dummy responses is referred to for generating QMP responses. A semi-hardcoded domain XML is used to hotplug and unhotplug device XMLs against it. It consists of various controllers to allow successful hotplugging. Depending on the architecture selected, some controllers may be omitted or modified. LeakSanitizer slows down the fuzzer severely, so we disable it in run_fuzz for now. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- src/conf/domain_conf.h | 2 + src/qemu/qemu_hotplug.c | 4 + tests/fuzz/meson.build | 6 + tests/fuzz/proto_header_common.h | 4 + tests/fuzz/protos/meson.build | 1 + tests/fuzz/protos/xml_hotplug.proto | 38 ++++ tests/fuzz/qemu_xml_hotplug_fuzz.cc | 340 ++++++++++++++++++++++++++++ tests/fuzz/run_fuzz.in | 5 + tests/qemumonitortestutils.c | 48 ++++ tests/qemumonitortestutils.h | 6 + 10 files changed, 454 insertions(+) create mode 100644 tests/fuzz/protos/xml_hotplug.proto create mode 100644 tests/fuzz/qemu_xml_hotplug_fuzz.cc diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h index 3e97dd6293..1327604fbd 100644 --- a/src/conf/domain_conf.h +++ b/src/conf/domain_conf.h @@ -3288,6 +3288,8 @@ struct _virDomainObj { int taint; size_t ndeprecations; char **deprecations; + + bool fuzz; }; G_DEFINE_AUTOPTR_CLEANUP_FUNC(virDomainObj, virObjectUnref); diff --git a/src/qemu/qemu_hotplug.c b/src/qemu/qemu_hotplug.c index 75b97cf736..a4a5a32346 100644 --- a/src/qemu/qemu_hotplug.c +++ b/src/qemu/qemu_hotplug.c @@ -5455,6 +5455,10 @@ qemuDomainWaitForDeviceRemoval(virDomainObj *vm) qemuDomainObjPrivate *priv = vm->privateData; unsigned long long until; + /* Skip the wait entirely if fuzzing is active */ + if (vm->fuzz) + return 1; + if (virTimeMillisNow(&until) < 0) return 1; until += qemuDomainGetUnplugTimeout(vm); diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build index 12f9a719f2..9579f56749 100644 --- a/tests/fuzz/meson.build +++ b/tests/fuzz/meson.build @@ -37,6 +37,11 @@ if conf.has('WITH_QEMU') 'proto_to_xml.cc', ] + hotplug_src = [ + 'qemu_xml_hotplug_fuzz.cc', + 'proto_to_xml.cc', + ] + qemu_libs = [ test_qemu_driver_lib, test_utils_lib, @@ -48,6 +53,7 @@ if conf.has('WITH_QEMU') { 'name': 'qemu_xml_domain_fuzz', 'src': [ fuzzer_src, xml_domain_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN', 'deps': [ fuzz_autogen_xml_domain_dep ] }, { 'name': 'qemu_xml_domain_fuzz_disk', 'src': [ fuzzer_src, xml_domain_disk_only_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN_DISK_ONLY', 'deps': [ fuzz_autogen_xml_domain_dep ] }, { 'name': 'qemu_xml_domain_fuzz_interface', 'src': [ fuzzer_src, xml_domain_interface_only_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN_INTERFACE_ONLY', 'deps': [ fuzz_autogen_xml_domain_dep ] }, + { 'name': 'qemu_xml_hotplug_fuzz', 'src': [ hotplug_src, xml_hotplug_proto_src ], 'libs': [ qemu_libs, test_utils_qemu_monitor_lib ], 'macro': '-DXML_HOTPLUG', 'deps': [ fuzz_autogen_xml_domain_dep ] }, ] endif diff --git a/tests/fuzz/proto_header_common.h b/tests/fuzz/proto_header_common.h index 5ee510896d..3f135c48e1 100644 --- a/tests/fuzz/proto_header_common.h +++ b/tests/fuzz/proto_header_common.h @@ -35,6 +35,10 @@ #include "xml_domain_interface_only.pb.h" #endif +#ifdef XML_HOTPLUG +#include "xml_hotplug.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 42c3a7f6a9..0731ef1eca 100644 --- a/tests/fuzz/protos/meson.build +++ b/tests/fuzz/protos/meson.build @@ -3,6 +3,7 @@ protos = [ 'xml_domain.proto', 'xml_domain_disk_only.proto', 'xml_domain_interface_only.proto', + 'xml_hotplug.proto', ] autogen_proto_xml_domain_proto = custom_target('autogen_xml_domain.proto', diff --git a/tests/fuzz/protos/xml_hotplug.proto b/tests/fuzz/protos/xml_hotplug.proto new file mode 100644 index 0000000000..0490b2fdb6 --- /dev/null +++ b/tests/fuzz/protos/xml_hotplug.proto @@ -0,0 +1,38 @@ +syntax = "proto2"; + +import "autogen_xml_domain.proto"; + +package libvirt; + +message MainObj { + oneof new_device { + domainTag.devicesTag.soundTag T_sound = 1; + domainTag.devicesTag.filesystemTag T_filesystem = 2; + domainTag.devicesTag.inputTag T_input = 3; + domainTag.devicesTag.diskTag T_disk = 4; + domainTag.devicesTag.interfaceTag T_interface = 5; + domainTag.devicesTag.graphicsTag T_graphics = 6; + domainTag.devicesTag.serialTag T_serial = 7; + domainTag.devicesTag.parallelTag T_parallel = 8; + domainTag.devicesTag.channelTag T_channel = 9; + domainTag.devicesTag.consoleTag T_console = 10; + domainTag.devicesTag.controllerTag T_controller = 11; + domainTag.devicesTag.videoTag T_video = 12; + domainTag.devicesTag.rngTag T_rng = 13; + domainTag.devicesTag.watchdogTag T_watchdog = 14; + domainTag.devicesTag.memballoonTag T_memballoon = 15; + domainTag.devicesTag.smartcardTag T_smartcard = 16; + domainTag.devicesTag.redirdevTag T_redirdev = 17; + domainTag.devicesTag.audioTag T_audio = 18; + domainTag.devicesTag.cryptoTag T_crypto = 19; + domainTag.devicesTag.panicTag T_panic = 20; + domainTag.devicesTag.tpmTag T_tpm = 21; + domainTag.devicesTag.shmemTag T_shmem = 22; + domainTag.devicesTag.hostdevTag T_hostdev = 23; + domainTag.devicesTag.leaseTag T_lease = 24; + domainTag.devicesTag.redirfilterTag T_redirfilter = 25; + domainTag.devicesTag.iommuTag T_iommu = 26; + domainTag.devicesTag.vsockTag T_vsock = 27; + domainTag.devicesTag.nvramTag T_nvram = 28; + } +} diff --git a/tests/fuzz/qemu_xml_hotplug_fuzz.cc b/tests/fuzz/qemu_xml_hotplug_fuzz.cc new file mode 100644 index 0000000000..6e391c1f51 --- /dev/null +++ b/tests/fuzz/qemu_xml_hotplug_fuzz.cc @@ -0,0 +1,340 @@ +/* + * qemu_xml_hotplug_fuzz.cc: QEMU hotplug 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> + +#include <unistd.h> +#include <sys/types.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdarg.h> + +extern "C" { +#include "qemu/qemu_alias.h" +#include "qemu/qemu_conf.h" +#include "qemu/qemu_hotplug.h" +#include "qemumonitortestutils.h" +#include "testutils.h" +#include "testutilsqemu.h" +#include "testutilsqemuschema.h" +#include "virhostdev.h" +#include "virfile.h" +} + +#include "port/protobuf.h" +#include "proto_to_xml.h" +#include "src/libfuzzer/libfuzzer_macro.h" + +#define QEMU_HOTPLUG_FUZZ_DOMAIN_ID 7 + +uint64_t device_parse_pass = 0; +uint64_t device_attach_pass = 0; +uint64_t success_pass = 0; +uint64_t detach_success_pass = 0; + +bool enable_xml_dump = false; + +typedef struct { + virQEMUCaps *caps; + GHashTable *schema; +} qemuFuzzHotplugData; + +#define QMP_OK "{\"return\": {}}" +#define QMP_EMPTY_ARRAY "{\"return\": []}" + +std::string getBaseDomainXML(std::string arch) { + std::string result = "<domain type='qemu'>\n" + " <name>MyGuest</name>\n" + " <uuid>4dea22b3-1d52-d8f3-2516-782e98ab3fa0</uuid>\n" + " <os>\n" + " <type arch='" + arch + "'>hvm</type>\n" + " </os>\n" + " <memory>4096</memory>\n" + " <devices>\n"; + + if (arch == "aarch64" || arch == "armv7l" || + arch == "riscv64" || + arch == "loongarch64") { + result += "<controller type='pci' model='pcie-root'/>\n"; + } else { + result += "<controller type='pci' model='pci-root'/>\n"; + } + + /* s390x does not support USB */ + if (arch != "s390x") { + result += "<controller type='usb'/>\n"; + result += "<controller type='ccid'/>\n"; + } + + /* SATA is not supported on s390x and SPARC */ + if (arch != "s390x" && arch != "sparc") + result += "<controller type='sata'/>\n"; + + result += "<controller type='virtio-serial'/>\n"; + result += "<controller type='scsi'/>\n"; + + result += "<emulator>/usr/bin/qemu-system-" + arch + "</emulator>\n" + "</devices>\n" + "</domain>\n"; + + return result; +} + + +std::string getDeviceDeletedResponse(std::string dev) { + std::string result = "{" \ + " \"timestamp\": {" \ + " \"seconds\": 1374137171," \ + " \"microseconds\": 2659" \ + " }," \ + " \"event\": \"DEVICE_DELETED\"," \ + " \"data\": {" \ + " \"device\": \"" + dev + "\"," \ + " \"path\": \"/machine/peripheral/" + dev + "\"" \ + " }" \ + "}\r\n"; + + return result; +} + + +/* Table of QMP commands and dummy responses */ +std::unordered_map<std::string, std::string> qmp_cmd_table = { + {"device_add", QMP_OK}, + {"object-add", QMP_OK}, + {"object-del", QMP_OK}, + {"netdev_add", QMP_OK}, + {"netdev_del", QMP_OK}, + {"chardev-add", QMP_OK}, + {"chardev-remove", QMP_OK}, + {"blockdev-add", QMP_OK}, + {"blockdev-del", QMP_OK}, + {"qom-list", QMP_EMPTY_ARRAY}, + {"query-block", QMP_EMPTY_ARRAY}, + {"query-fds", QMP_EMPTY_ARRAY}, + {"set-action", QMP_OK}, + {"set_link", QMP_OK}, + {"query-fdsets", QMP_EMPTY_ARRAY}, + {"add-fd", "{ \"return\": { \"fdset-id\": 1, \"fd\": 95 }}"}, + {"block_set_io_throttle", QMP_OK}, +}; + + +char *qemuMonitorFuzzGetResponse(const char *command, + virJSONValue *cmdargs) +{ + if (STREQ(command, "device_del")) { + const char *id = virJSONValueObjectGetString(cmdargs, "id"); + std::string result = getDeviceDeletedResponse(id); + + return g_strdup(result.c_str()); + } + + if (qmp_cmd_table.find(command) != qmp_cmd_table.end()) { + return g_strdup(qmp_cmd_table[command].c_str()); + } + + /* If QMP command is unknown, assume QMP_OK and warn the user of the same. */ + printf("[FUZZ WARN]: Unknown QMP command: %s\n", command); + + return g_strdup(QMP_OK); +} + + +static void +fuzzXMLHotplug(virQEMUDriver *driver, + qemuFuzzHotplugData *hotplugData, + const char *domain_xml_string, + const char *device_xml_string) +{ + virDomainObj *vm = NULL; + virDomainDef *def = NULL; + virDomainDeviceDef *dev = NULL; + virDomainDeviceDef *remove_dev = NULL; + qemuDomainObjPrivate *priv = NULL; + g_autoptr(qemuMonitorTest) test_mon = NULL; + bool attach_success = false; + + if (!(def = virDomainDefParseString(domain_xml_string, driver->xmlopt, NULL, + VIR_DOMAIN_DEF_PARSE_INACTIVE))) { + + printf("Failed to parse domain XML!\n"); + exit(EXIT_FAILURE); + } + + if (!(vm = virDomainObjNew(driver->xmlopt))) + goto cleanup; + + vm->def = def; + priv = (qemuDomainObjPrivate *) vm->privateData; + priv->qemuCaps = hotplugData->caps; + + if (qemuDomainAssignAddresses(vm->def, hotplugData->caps, + driver, vm, true) < 0) { + goto cleanup; + } + + if (qemuAssignDeviceAliases(vm->def) < 0) + goto cleanup; + + vm->def->id = QEMU_HOTPLUG_FUZZ_DOMAIN_ID; + vm->fuzz = true; + + if (qemuDomainSetPrivatePaths(driver, vm) < 0) + goto cleanup; + + device_parse_pass++; + if (!(dev = virDomainDeviceDefParse(device_xml_string, vm->def, + driver->xmlopt, NULL, + VIR_DOMAIN_DEF_PARSE_INACTIVE))) + goto cleanup; + + /* Initialize test monitor + * + * Keep it after virDomainDeviceDefParse to avoid wasting time with monitor + * creation. + */ + + if (!(test_mon = qemuMonitorTestNew(driver->xmlopt, vm, NULL, hotplugData->schema))) + goto cleanup; + + /* Enable fuzzing mode of the test monitor */ + qemuMonitorTestFuzzSetup(test_mon, qemuMonitorFuzzGetResponse); + + priv->mon = qemuMonitorTestGetMonitor(test_mon); + + virObjectUnlock(priv->mon); + + device_attach_pass++; + + if (qemuDomainAttachDeviceLive(vm, dev, driver) == 0) { + success_pass++; + attach_success = true; + } + + if (attach_success) { + /* The previous virDomainDeviceDefParse cleared out the data in dev, so + * we need to reparse it before doing the detachment. + */ + remove_dev = virDomainDeviceDefParse(device_xml_string, vm->def, + driver->xmlopt, NULL, + 0); + + if (remove_dev && qemuDomainDetachDeviceLive(vm, remove_dev, driver, false) == 0) { + detach_success_pass++; + } + } + + virObjectLock(priv->mon); + + cleanup: + if (vm) { + priv->qemuCaps = NULL; + priv->mon = NULL; + vm->def = NULL; + + virDomainObjEndAPI(&vm); + } + + virDomainDeviceDefFree(remove_dev); + virDomainDeviceDefFree(dev); + virDomainDefFree(def); +} + + +DEFINE_PROTO_FUZZER(const libvirt::MainObj &message) +{ + static GHashTable *capscache = virHashNew(virObjectUnref); + static GHashTable *capslatest = testQemuGetLatestCaps(); + static GHashTable *qapiSchemaCache = virHashNew((GDestroyNotify) g_hash_table_unref); + + static qemuFuzzHotplugData *hotplugData = g_new0(qemuFuzzHotplugData, 1); + + static virQEMUDriver driver; + static bool initialized = false; + + static const char *arch_env = g_getenv("LPM_FUZZ_ARCH"); + static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT"); + + static std::string arch = ""; + static std::string domain_xml = ""; + + std::string device_xml = ""; + + /* + * One-time setup of QEMU driver. Re-running them in every + * iteration incurs a significant penalty to the speed of the fuzzer. + */ + if (!initialized) { + FUZZ_COMMON_INIT(); + + if (qemuTestDriverInit(&driver) < 0) + exit(EXIT_FAILURE); + + driver.lockManager = virLockManagerPluginNew("nop", "qemu", + driver.config->configBaseDir, + 0); + + if (arch_env) { + arch = arch_env; + } else { + arch = "x86_64"; + } + + if (!(hotplugData->caps = testQemuGetRealCaps(arch.c_str(), "latest", "", + capslatest, capscache, + qapiSchemaCache, &hotplugData->schema))) { + printf("Failed to setup QEMU capabilities (invalid arch?)\n"); + exit(EXIT_FAILURE); + } + + if (qemuTestCapsCacheInsert(driver.qemuCapsCache, hotplugData->caps) < 0) + exit(EXIT_FAILURE); + + if (!(driver.hostdevMgr = virHostdevManagerGetDefault())) + exit(EXIT_FAILURE); + + virEventRegisterDefaultImpl(); + + domain_xml = getBaseDomainXML(arch); + + /* 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, device_xml); + + if (enable_xml_dump) + printf("%s\n", device_xml.c_str()); + + fuzzXMLHotplug(&driver, hotplugData, domain_xml.c_str(), device_xml.c_str()); + + if (device_parse_pass % 1000 == 0) + printf("[FUZZ METRICS] Device parse: %lu, Device Attach: %lu, Attached: %lu, Detached: %lu\n", + device_parse_pass, device_attach_pass, success_pass, detach_success_pass); +} diff --git a/tests/fuzz/run_fuzz.in b/tests/fuzz/run_fuzz.in index da3c7935b7..414b99b6cf 100644 --- a/tests/fuzz/run_fuzz.in +++ b/tests/fuzz/run_fuzz.in @@ -113,6 +113,11 @@ env["LPM_EXE_PATH"] = exe_path process_args.extend(["-print_funcs=-1"]) +if args.fuzzer == "qemu_xml_hotplug_fuzz": + # LSAN slows down the hotplug fuzzer to a crawl, + # so we have to disable LeakSanitizer + env["ASAN_OPTIONS"] = "detect_leaks=0" + if args.libfuzzer_options: process_args.extend([x for x in args.libfuzzer_options.split(' ') if x != '']) diff --git a/tests/qemumonitortestutils.c b/tests/qemumonitortestutils.c index 88a369188e..448710957e 100644 --- a/tests/qemumonitortestutils.c +++ b/tests/qemumonitortestutils.c @@ -83,8 +83,14 @@ struct _qemuMonitorTest { virDomainObj *vm; GHashTable *qapischema; + + bool fuzz; + qemuMonitorFuzzResponseCallback fuzz_response_cb; }; +static int +qemuMonitorFuzzProcessCommandDefault(qemuMonitorTest *test, + const char *cmdstr); static void qemuMonitorTestItemFree(qemuMonitorTestItem *item) @@ -227,6 +233,11 @@ qemuMonitorTestProcessCommand(qemuMonitorTest *test, VIR_DEBUG("Processing string from monitor handler: '%s", cmdstr); + /* In fuzzing mode, process indefinite number of commands */ + if (test->fuzz) { + return qemuMonitorFuzzProcessCommandDefault(test, cmdstr); + } + if (test->nitems == 0) { qemuMonitorTestError("unexpected command: '%s'", cmdstr); } else { @@ -595,6 +606,34 @@ qemuMonitorTestAddItem(qemuMonitorTest *test, } +static int +qemuMonitorFuzzProcessCommandDefault(qemuMonitorTest *test, + const char *cmdstr) +{ + g_autoptr(virJSONValue) val = NULL; + virJSONValue *cmdargs = NULL; + + const char *cmdname = NULL; + g_autofree char *response = NULL; + + if (!(val = virJSONValueFromString(cmdstr))) + return -1; + + if (!(cmdname = virJSONValueObjectGetString(val, "execute"))) { + qemuMonitorTestError("Missing command name in %s", cmdstr); + return -1; + } + + cmdargs = virJSONValueObjectGet(val, "arguments"); + + response = test->fuzz_response_cb(cmdname, cmdargs); + + qemuMonitorTestAddResponse(test, response); + + return 0; +} + + static int qemuMonitorTestProcessCommandVerbatim(qemuMonitorTest *test, qemuMonitorTestItem *item, @@ -1021,6 +1060,15 @@ qemuMonitorTestSkipDeprecatedValidation(qemuMonitorTest *test, } +void +qemuMonitorTestFuzzSetup(qemuMonitorTest *test, + qemuMonitorFuzzResponseCallback response_cb) +{ + test->fuzz = true; + test->fuzz_response_cb = response_cb; +} + + static int qemuMonitorTestFullAddItem(qemuMonitorTest *test, const char *filename, diff --git a/tests/qemumonitortestutils.h b/tests/qemumonitortestutils.h index 6d26526f60..105bd10486 100644 --- a/tests/qemumonitortestutils.h +++ b/tests/qemumonitortestutils.h @@ -29,6 +29,8 @@ typedef struct _qemuMonitorTestItem qemuMonitorTestItem; typedef int (*qemuMonitorTestResponseCallback)(qemuMonitorTest *test, qemuMonitorTestItem *item, const char *message); +typedef char *(*qemuMonitorFuzzResponseCallback)(const char *command, + virJSONValue *cmdargs); void qemuMonitorTestAddHandler(qemuMonitorTest *test, @@ -61,6 +63,10 @@ void qemuMonitorTestSkipDeprecatedValidation(qemuMonitorTest *test, bool allowRemoved); +void +qemuMonitorTestFuzzSetup(qemuMonitorTest *test, + qemuMonitorFuzzResponseCallback response_cb); + int qemuMonitorTestAddItem(qemuMonitorTest *test, const char *command_name, -- 2.34.1

Unused variables in ch_process.c cause compilation errors so remove them. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- src/ch/ch_process.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ch/ch_process.c b/src/ch/ch_process.c index 9816509e49..12da020c7b 100644 --- a/src/ch/ch_process.c +++ b/src/ch/ch_process.c @@ -656,7 +656,6 @@ chProcessAddNetworkDevices(virCHDriver *driver, for (i = 0; i < vmdef->nnets; i++) { g_autofree int *tapfds = NULL; g_autofree char *payload = NULL; - g_autofree char *response = NULL; size_t tapfd_len; size_t payload_len; int saved_errno; @@ -967,7 +966,6 @@ virCHProcessStartRestore(virCHDriver *driver, virDomainObj *vm, const char *from g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER; g_auto(virBuffer) http_headers = VIR_BUFFER_INITIALIZER; g_autofree char *payload = NULL; - g_autofree char *response = NULL; VIR_AUTOCLOSE mon_sockfd = -1; g_autofree int *tapfds = NULL; g_autofree int *nicindexes = NULL; -- 2.34.1

This patch implements the harness for the Cloud-Hypervisor driver to fuzz XML parsing and XML-to-JSON generation including virCHMonitorBuildVMJson and virCHMonitorBuildNetJson. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- src/ch/ch_monitor.c | 2 +- src/ch/ch_monitor.h | 3 + tests/fuzz/ch_xml_domain_fuzz.cc | 157 +++++++++++++++++++++++++++++++ tests/fuzz/meson.build | 17 ++++ tests/fuzz/proto_to_xml.cc | 18 ++++ tests/fuzz/proto_to_xml.h | 2 + 6 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/fuzz/ch_xml_domain_fuzz.cc diff --git a/src/ch/ch_monitor.c b/src/ch/ch_monitor.c index 3e49902791..a10dd9f03d 100644 --- a/src/ch/ch_monitor.c +++ b/src/ch/ch_monitor.c @@ -423,7 +423,7 @@ virCHMonitorBuildDevicesJson(virJSONValue *content, return 0; } -static int +int virCHMonitorBuildVMJson(virCHDriver *driver, virDomainDef *vmdef, char **jsonstr) { diff --git a/src/ch/ch_monitor.h b/src/ch/ch_monitor.h index b35f5ea027..aec4a06de8 100644 --- a/src/ch/ch_monitor.h +++ b/src/ch/ch_monitor.h @@ -133,3 +133,6 @@ virCHMonitorBuildNetJson(virDomainNetDef *netdef, int virCHMonitorBuildRestoreJson(virDomainDef *vmdef, const char *from, char **jsonstr); +int +virCHMonitorBuildVMJson(virCHDriver *driver, virDomainDef *vmdef, + char **jsonstr); diff --git a/tests/fuzz/ch_xml_domain_fuzz.cc b/tests/fuzz/ch_xml_domain_fuzz.cc new file mode 100644 index 0000000000..6733f55378 --- /dev/null +++ b/tests/fuzz/ch_xml_domain_fuzz.cc @@ -0,0 +1,157 @@ +/* + * ch_xml_domain_fuzz.cc: Cloud-Hypervisor domain 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> + +#include <unistd.h> +#include <sys/types.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdarg.h> + +extern "C" { +#include <testutils.h> +#include "ch/ch_conf.h" +#include "ch/ch_monitor.h" +} + +#include "port/protobuf.h" +#include "proto_to_xml.h" +#include "src/libfuzzer/libfuzzer_macro.h" + +uint64_t parse_pass = 0; +uint64_t format_pass = 0; +uint64_t command_line_pass = 0; +uint64_t success = 0; +uint64_t net = 0; +uint64_t net_success = 0; + +bool enable_xml_dump = false; +bool enable_xml_format = false; + +extern std::string emulator; + +static void +fuzzXMLToCommandLine(virCHDriver *driver, + const char *xml_string) +{ + virDomainDef *def = NULL; + g_autofree char *formatted_xml = NULL; + g_autofree char *json_str = NULL; + size_t i; + + parse_pass++; + if (!(def = virDomainDefParseString(xml_string, driver->xmlopt, NULL, + VIR_DOMAIN_DEF_PARSE_INACTIVE))) + goto cleanup; + + if (enable_xml_format) { + format_pass++; + if (!(formatted_xml = virDomainDefFormat(def, driver->xmlopt, + VIR_DOMAIN_DEF_FORMAT_SECURE))) + goto cleanup; + } + + command_line_pass++; + + if (virCHMonitorBuildVMJson(driver, def, &json_str) < 0) + goto cleanup; + success++; + + for (i = 0; i < def->nnets; i++) { + net++; + if (virCHMonitorBuildNetJson(def->nets[i], i, &json_str) == 0) + net_success++; + } + + cleanup: + virDomainDefFree(def); +} + + +DEFINE_PROTO_FUZZER(const libvirt::MainObj &message) +{ + static virCHDriver *driver; + static bool initialized = false; + + static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT"); + static const char *format_xml_env = g_getenv("LPM_XML_FORMAT_ENABLE"); + + std::string xml = ""; + + + /* + * One-time setup of CH driver. Re-running them in every + * iteration incurs a significant penalty to the speed of the fuzzer. + */ + if (!initialized) { + FUZZ_COMMON_INIT(); + + driver = g_new0(virCHDriver, 1); + + if (!(driver->caps = virCHDriverCapsInit())) { + printf("Unable to initialize driver capabilities\n"); + exit(EXIT_FAILURE); + } + + virCapsGuest *guest = driver->caps->guests[0]; + + /* Add KVM and HYPERV capabilities */ + virCapabilitiesAddGuestDomain(guest, VIR_DOMAIN_VIRT_KVM, + NULL, NULL, 0, NULL); + + virCapabilitiesAddGuestDomain(guest, VIR_DOMAIN_VIRT_HYPERV, + NULL, NULL, 0, NULL); + + if (!(driver->xmlopt = chDomainXMLConfInit(driver))) { + printf("Unable to initialize driver XMLOPT\n"); + exit(EXIT_FAILURE); + } + + emulator = "/usr/bin/cloud-hypervisor"; + + /* Enable printing of XML to stdout (useful for debugging crashes) */ + if (dump_xml_env && STREQ(dump_xml_env, "YES")) + enable_xml_dump = true; + + /* Enable fuzzing of XML formatting */ + if (format_xml_env && STREQ(format_xml_env, "YES")) + enable_xml_format = true; + + initialized = true; + } + + convertProtoToCHXMLDomain(message, xml); + + if (enable_xml_dump) + printf("%s\n", xml.c_str()); + + fuzzXMLToCommandLine(driver, xml.c_str()); + + if (parse_pass % 1000 == 0) + printf("[FUZZ METRICS] Parse: %lu, Format: %lu, Cmdline: %lu, Success: %lu, Network: %lu, Network Success: %lu\n", + parse_pass, format_pass, command_line_pass, success, net, net_success); + +} diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build index 9579f56749..bb27c2843d 100644 --- a/tests/fuzz/meson.build +++ b/tests/fuzz/meson.build @@ -57,6 +57,23 @@ if conf.has('WITH_QEMU') ] endif +if conf.has('WITH_CH') + fuzzer_src = [ + 'ch_xml_domain_fuzz.cc', + 'proto_to_xml.cc', + ] + + ch_libs = [ + ch_driver_impl, + test_utils_lib, + libvirt_lib, + ] + + xml_fuzzers += [ + { 'name': 'ch_xml_domain_fuzz', 'src': [ fuzzer_src, xml_domain_proto_src ], 'libs': ch_libs, 'macro': '-DXML_DOMAIN', 'deps': [ fuzz_autogen_xml_domain_dep ] }, + ] +endif + foreach fuzzer: xml_fuzzers xml_domain_fuzz = executable(fuzzer['name'], fuzzer['src'], diff --git a/tests/fuzz/proto_to_xml.cc b/tests/fuzz/proto_to_xml.cc index 36ad1028b1..40858e4779 100644 --- a/tests/fuzz/proto_to_xml.cc +++ b/tests/fuzz/proto_to_xml.cc @@ -216,6 +216,24 @@ void convertProtoToQEMUXMLDomain(const libvirt::MainObj &message, } +void convertProtoToCHXMLDomain(const libvirt::MainObj &message, + std::string &xml) +{ + xml = "<domain type='kvm'>\n" + " <name>MyGuest</name>\n" + " <uuid>4dea22b3-1d52-d8f3-2516-782e98ab3fa0</uuid>\n" + " <os>\n" + " <type>hvm</type>\n" + " <kernel>hypervisor-fw</kernel>\n" + " </os>\n" + " <memory>4096</memory>\n"; + + convertProtoToXMLInternal(message, xml, true); + + xml += "</domain>\n"; +} + + void convertProtoToXML(const libvirt::MainObj &message, std::string &xml) { diff --git a/tests/fuzz/proto_to_xml.h b/tests/fuzz/proto_to_xml.h index a87547a319..5c76772c5f 100644 --- a/tests/fuzz/proto_to_xml.h +++ b/tests/fuzz/proto_to_xml.h @@ -27,5 +27,7 @@ void convertProtoToQEMUXMLDomain(const libvirt::MainObj &message, std::string arch, std::string &xml); +void convertProtoToCHXMLDomain(const libvirt::MainObj &message, + std::string &xml); void convertProtoToXML(const libvirt::MainObj &message, std::string &xml); -- 2.34.1

This patch adds the harness for the VMX driver to fuzz XML parsing and VMX config generation. VMX config generation is done with a hardcoded version of 13. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- tests/fuzz/meson.build | 16 +++ tests/fuzz/proto_to_xml.cc | 18 +++ tests/fuzz/proto_to_xml.h | 3 + tests/fuzz/vmx_xml_domain_fuzz.cc | 208 ++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 tests/fuzz/vmx_xml_domain_fuzz.cc diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build index bb27c2843d..88b5fe103c 100644 --- a/tests/fuzz/meson.build +++ b/tests/fuzz/meson.build @@ -74,6 +74,22 @@ if conf.has('WITH_CH') ] endif +if conf.has('WITH_VMX') + fuzzer_src = [ + 'vmx_xml_domain_fuzz.cc', + 'proto_to_xml.cc', + ] + + vmx_libs = [ + test_utils_lib, + libvirt_lib, + ] + + xml_fuzzers += [ + { 'name': 'vmx_xml_domain_fuzz', 'src': [ fuzzer_src, xml_domain_proto_src ], 'libs': vmx_libs, 'macro': '-DXML_DOMAIN', 'deps': [ fuzz_autogen_xml_domain_dep ] }, + ] +endif + foreach fuzzer: xml_fuzzers xml_domain_fuzz = executable(fuzzer['name'], fuzzer['src'], diff --git a/tests/fuzz/proto_to_xml.cc b/tests/fuzz/proto_to_xml.cc index 40858e4779..983256bcae 100644 --- a/tests/fuzz/proto_to_xml.cc +++ b/tests/fuzz/proto_to_xml.cc @@ -234,6 +234,24 @@ void convertProtoToCHXMLDomain(const libvirt::MainObj &message, } +void convertProtoToVMXXMLDomain(const libvirt::MainObj &message, + std::string arch, + std::string &xml) +{ + xml = "<domain type='vmware'>\n" + " <name>MyGuest</name>\n" + " <uuid>4dea22b3-1d52-d8f3-2516-782e98ab3fa0</uuid>\n" + " <os>\n" + " <type arch='" + arch + "'>hvm</type>\n" + " </os>\n" + " <memory>4096</memory>\n"; + + convertProtoToXMLInternal(message, xml, true); + + xml += "</domain>\n"; +} + + void convertProtoToXML(const libvirt::MainObj &message, std::string &xml) { diff --git a/tests/fuzz/proto_to_xml.h b/tests/fuzz/proto_to_xml.h index 5c76772c5f..89e6726611 100644 --- a/tests/fuzz/proto_to_xml.h +++ b/tests/fuzz/proto_to_xml.h @@ -29,5 +29,8 @@ void convertProtoToQEMUXMLDomain(const libvirt::MainObj &message, std::string &xml); void convertProtoToCHXMLDomain(const libvirt::MainObj &message, std::string &xml); +void convertProtoToVMXXMLDomain(const libvirt::MainObj &message, + std::string arch, + std::string &xml); void convertProtoToXML(const libvirt::MainObj &message, std::string &xml); diff --git a/tests/fuzz/vmx_xml_domain_fuzz.cc b/tests/fuzz/vmx_xml_domain_fuzz.cc new file mode 100644 index 0000000000..69859551bd --- /dev/null +++ b/tests/fuzz/vmx_xml_domain_fuzz.cc @@ -0,0 +1,208 @@ +/* + * vmx_xml_domain_fuzz.cc: VMX domain 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> + +#include <unistd.h> +#include <sys/types.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdarg.h> + +extern "C" { +#include "testutils.h" +#include "internal.h" +#include "viralloc.h" +#include "vmx/vmx.h" +} + +#include "port/protobuf.h" +#include "proto_to_xml.h" +#include "src/libfuzzer/libfuzzer_macro.h" + +uint64_t parse_pass = 0; +uint64_t format_pass = 0; +uint64_t command_line_pass = 0; +uint64_t success = 0; + +bool enable_xml_dump = false; +bool enable_xml_format = false; + +static void +fuzzXMLToCommandLine(virVMXContext *ctx, + virDomainXMLOption *xmlopt, + const char *xml_string) +{ + virDomainDef *def = NULL; + g_autofree char *formatted_xml = NULL; + g_autofree char *formatted_config = NULL; + + parse_pass++; + if (!(def = virDomainDefParseString(xml_string, xmlopt, NULL, + VIR_DOMAIN_DEF_PARSE_INACTIVE))) + goto cleanup; + + if (enable_xml_format) { + format_pass++; + if (!(formatted_xml = virDomainDefFormat(def, xmlopt, + VIR_DOMAIN_DEF_FORMAT_SECURE))) + goto cleanup; + } + + command_line_pass++; + + if (!(formatted_config = virVMXFormatConfig(ctx, xmlopt, def, 13))) { + goto cleanup; + } + + success++; + + cleanup: + + virDomainDefFree(def); +} + + +virCaps *fuzzCapsInit() { + virCapsGuest *guest = NULL; + virCaps *caps = NULL; + + caps = virCapabilitiesNew(VIR_ARCH_I686, true, true); + + if (caps == NULL) + return NULL; + + virCapabilitiesAddHostMigrateTransport(caps, "esx"); + + /* i686 guest */ + guest = virCapabilitiesAddGuest(caps, VIR_DOMAIN_OSTYPE_HVM, + VIR_ARCH_I686, + NULL, NULL, 0, NULL); + + virCapabilitiesAddGuestDomain(guest, VIR_DOMAIN_VIRT_VMWARE, + NULL, NULL, 0, NULL); + + /* x86_64 guest */ + guest = virCapabilitiesAddGuest(caps, VIR_DOMAIN_OSTYPE_HVM, + VIR_ARCH_X86_64, + NULL, NULL, 0, NULL); + + virCapabilitiesAddGuestDomain(guest, VIR_DOMAIN_VIRT_VMWARE, + NULL, NULL, 0, NULL); + + return caps; +} + + +static char * +fuzzFormatVMXFileName(const char *src G_GNUC_UNUSED, + void *opaque G_GNUC_UNUSED) +{ + return g_strdup("/vmfs/volumes/test1/test2"); +} + + +static int +fuzzAutodetectSCSIControllerModel(virDomainDiskDef *def G_GNUC_UNUSED, + int *model, void *opaque G_GNUC_UNUSED) +{ + *model = VIR_DOMAIN_CONTROLLER_MODEL_SCSI_LSILOGIC; + + return 0; +} + + +DEFINE_PROTO_FUZZER(const libvirt::MainObj &message) +{ + static virVMXContext ctx; + static virDomainXMLOption *xmlopt = NULL; + static virCaps *caps; + + static bool initialized = false; + + static const char *arch_env = g_getenv("LPM_FUZZ_ARCH"); + static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT"); + static const char *format_xml_env = g_getenv("LPM_XML_FORMAT_ENABLE"); + + static std::string arch = ""; + + std::string xml = ""; + + /* + * One-time setup of VMX driver. Re-running them in every + * iteration incurs a significant penalty to the speed of the fuzzer. + */ + if (!initialized) { + FUZZ_COMMON_INIT(); + + caps = fuzzCapsInit(); + + if (caps == NULL) { + exit(EXIT_FAILURE); + } + + if (arch_env) { + arch = arch_env; + } else { + arch = "x86_64"; + } + + if (!(arch == "x86_64" || arch == "i686")) { + printf("Unsupported architecture: %s\n", arch.c_str()); + exit(EXIT_FAILURE); + } + + if (!(xmlopt = virVMXDomainXMLConfInit(caps))) + exit(EXIT_FAILURE); + + ctx.opaque = NULL; + ctx.parseFileName = NULL; + ctx.formatFileName = fuzzFormatVMXFileName; + ctx.autodetectSCSIControllerModel = fuzzAutodetectSCSIControllerModel; + ctx.datacenterPath = NULL; + + /* Enable printing of XML to stdout (useful for debugging crashes) */ + if (dump_xml_env && STREQ(dump_xml_env, "YES")) + enable_xml_dump = true; + + /* Enable fuzzing of XML formatting */ + if (format_xml_env && STREQ(format_xml_env, "YES")) + enable_xml_format = true; + + initialized = true; + } + + convertProtoToVMXXMLDomain(message, arch, xml); + + if (enable_xml_dump) + printf("%s\n", xml.c_str()); + + fuzzXMLToCommandLine(&ctx, xmlopt, xml.c_str()); + + if (parse_pass % 1000 == 0) + printf("[FUZZ METRICS] Parse: %lu, Format: %lu, Cmdline: %lu, Success: %lu\n", + parse_pass, format_pass, command_line_pass, success); + +} -- 2.34.1

This patch adds the harness for the xen driver to fuzz XML parsing, domconfig generation and JSON conversion. libxlmock.so is required to make the libXL fuzzer work outside of xen hosts. However, LD_PRELOAD breaks the fuzzer when ASAN/UBSAN is present. This is because LD_PRELOAD gets propagated to llvm-symbolizer which is undesirable and causes various undefined symbol errors. We add a wrapper binary which execs the real llvm-symbolizer with the environment unset. However, LD_PRELOAD still breaks parallel fuzzing so it is disabled for the time being. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- tests/fuzz/libxl_xml_domain_fuzz.cc | 159 +++++++++++++++++++++++++++ tests/fuzz/llvm_symbolizer_wrapper.c | 11 ++ tests/fuzz/meson.build | 31 ++++++ tests/fuzz/proto_to_xml.cc | 18 +++ tests/fuzz/proto_to_xml.h | 3 + tests/fuzz/run_fuzz.in | 15 +++ 6 files changed, 237 insertions(+) create mode 100644 tests/fuzz/libxl_xml_domain_fuzz.cc create mode 100644 tests/fuzz/llvm_symbolizer_wrapper.c diff --git a/tests/fuzz/libxl_xml_domain_fuzz.cc b/tests/fuzz/libxl_xml_domain_fuzz.cc new file mode 100644 index 0000000000..a8fcb62d06 --- /dev/null +++ b/tests/fuzz/libxl_xml_domain_fuzz.cc @@ -0,0 +1,159 @@ +/* + * libxl_xml_domain_fuzz.cc: libXL domain 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> + +#include <unistd.h> +#include <sys/types.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdarg.h> + +extern "C" { +#include "testutils.h" +#include "internal.h" +#include "libxl/libxl_conf.h" +#include "testutilsxen.h" +} + +#include "port/protobuf.h" +#include "proto_to_xml.h" +#include "src/libfuzzer/libfuzzer_macro.h" + +uint64_t parse_pass = 0; +uint64_t format_pass = 0; +uint64_t config_pass = 0; +uint64_t json_pass = 0; +uint64_t command_line_pass = 0; +uint64_t success = 0; + +bool enable_xml_dump = false; +bool enable_xml_format = false; + +static void +fuzzXMLToCommandLine(libxlDriverPrivate *driver, + const char *xml_string) +{ + virDomainDef *def = NULL; + const char *formatted_xml = NULL; + g_autofree char *json_string = NULL; + libxl_domain_config config; + virPortAllocatorRange *gports = NULL; + bool config_init = false; + + g_autoptr(libxlDriverConfig) cfg = libxlDriverConfigGet(driver); + + parse_pass++; + if (!(def = virDomainDefParseString(xml_string, driver->xmlopt, NULL, + VIR_DOMAIN_DEF_PARSE_INACTIVE))) + goto cleanup; + + if (enable_xml_format) { + format_pass++; + if (!(formatted_xml = virDomainDefFormat(def, driver->xmlopt, + VIR_DOMAIN_DEF_FORMAT_SECURE))) + goto cleanup; + } + + libxl_domain_config_init(&config); + config_init = true; + + if (!(gports = virPortAllocatorRangeNew("vnc", 5900, 6000))) + goto cleanup; + + config_pass++; + if (libxlBuildDomainConfig(gports, def, cfg, &config) < 0) + goto cleanup; + + json_pass++; + if (!(json_string = libxl_domain_config_to_json(cfg->ctx, &config))) + goto cleanup; + + success++; + + cleanup: + + virPortAllocatorRangeFree(gports); + if (config_init) + libxl_domain_config_dispose(&config); + virDomainDefFree(def); +} + + +DEFINE_PROTO_FUZZER(const libvirt::MainObj &message) +{ + static libxlDriverPrivate *driver = NULL; + + static bool initialized = false; + + static const char *arch_env = g_getenv("LPM_FUZZ_ARCH"); + static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT"); + static const char *format_xml_env = g_getenv("LPM_XML_FORMAT_ENABLE"); + + static std::string arch = ""; + + std::string xml = ""; + + + /* + * One-time setup of libXL driver. Re-running them in every + * iteration incurs a significant penalty to the speed of the fuzzer. + */ + if (!initialized) { + FUZZ_COMMON_INIT(); + + /* NOTE: Driver initialization will fail without libxlmock */ + if ((driver = testXLInitDriver()) == NULL) + exit(EXIT_FAILURE); + + if (arch_env) { + arch = arch_env; + } else { + arch = "x86_64"; + } + + /* Enable printing of XML to stdout (useful for debugging crashes) */ + if (dump_xml_env && STREQ(dump_xml_env, "YES")) + enable_xml_dump = true; + + /* Enable fuzzing of XML formatting */ + if (format_xml_env && STREQ(format_xml_env, "YES")) + enable_xml_format = true; + + initialized = true; + } + + convertProtoTolibXLXMLDomain(message, arch, xml); + + if (enable_xml_dump) + printf("%s\n", xml.c_str()); + + fuzzXMLToCommandLine(driver, xml.c_str()); + + if (parse_pass % 1000 == 0) + printf("[FUZZ METRICS] Parse: %lu, Format: %lu, Config: %lu, JSON: %lu, Success: %lu\n", + parse_pass, format_pass, config_pass, json_pass, success); + +} diff --git a/tests/fuzz/llvm_symbolizer_wrapper.c b/tests/fuzz/llvm_symbolizer_wrapper.c new file mode 100644 index 0000000000..d0aeba61fe --- /dev/null +++ b/tests/fuzz/llvm_symbolizer_wrapper.c @@ -0,0 +1,11 @@ +#include <config.h> + +#include <stdlib.h> +#include <unistd.h> + +int main(int argc, char **argv) { + // Avoid unused error. + (void) argc; + + execve("/usr/bin/llvm-symbolizer", argv, NULL); +} diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build index 88b5fe103c..417b8dc1ef 100644 --- a/tests/fuzz/meson.build +++ b/tests/fuzz/meson.build @@ -90,6 +90,24 @@ if conf.has('WITH_VMX') ] endif +if conf.has('WITH_LIBXL') + fuzzer_src = [ + 'libxl_xml_domain_fuzz.cc', + 'proto_to_xml.cc', + ] + + libxl_libs = [ + test_xen_driver_lib, + test_utils_xen_lib, + test_utils_lib, + libvirt_lib, + ] + + xml_fuzzers += [ + { 'name': 'libxl_xml_domain_fuzz', 'src': [ fuzzer_src, xml_domain_proto_src ], 'libs': libxl_libs, 'macro': '-DXML_DOMAIN', 'deps': [ fuzz_autogen_xml_domain_dep, libxl_dep ] }, + ] +endif + foreach fuzzer: xml_fuzzers xml_domain_fuzz = executable(fuzzer['name'], fuzzer['src'], @@ -102,6 +120,19 @@ foreach fuzzer: xml_fuzzers available_fuzzers += '"' + fuzzer['name'] + '"' + ',' endforeach +# Wrapper binary which execs the real llvm-symbolizer but +# unsets the env vars to avoid propagating LD_PRELOAD to the +# real llvm-symbolizer and causing undefined symbol errors when ASAN/UBSAN +# is enabled. + +llvm_symbolizer_wrapper = executable( + 'llvm-symbolizer-wrapper', + 'llvm_symbolizer_wrapper.c', + dependencies: [ + tests_dep, + ], +) + run_conf = configuration_data({ 'abs_builddir': meson.project_build_root(), 'available_fuzzers': available_fuzzers, diff --git a/tests/fuzz/proto_to_xml.cc b/tests/fuzz/proto_to_xml.cc index 983256bcae..fde3394cc7 100644 --- a/tests/fuzz/proto_to_xml.cc +++ b/tests/fuzz/proto_to_xml.cc @@ -252,6 +252,24 @@ void convertProtoToVMXXMLDomain(const libvirt::MainObj &message, } +void convertProtoTolibXLXMLDomain(const libvirt::MainObj &message, + std::string arch, + std::string &xml) +{ + xml = "<domain type='xen'>\n" + " <name>MyGuest</name>\n" + " <uuid>4dea22b3-1d52-d8f3-2516-782e98ab3fa0</uuid>\n" + " <os>\n" + " <type arch='" + arch + "'>hvm</type>\n" + " </os>\n" + " <memory>4096</memory>\n"; + + convertProtoToXMLInternal(message, xml, true); + + xml += "</domain>\n"; +} + + void convertProtoToXML(const libvirt::MainObj &message, std::string &xml) { diff --git a/tests/fuzz/proto_to_xml.h b/tests/fuzz/proto_to_xml.h index 89e6726611..b4849890cc 100644 --- a/tests/fuzz/proto_to_xml.h +++ b/tests/fuzz/proto_to_xml.h @@ -32,5 +32,8 @@ void convertProtoToCHXMLDomain(const libvirt::MainObj &message, void convertProtoToVMXXMLDomain(const libvirt::MainObj &message, std::string arch, std::string &xml); +void convertProtoTolibXLXMLDomain(const libvirt::MainObj &message, + std::string arch, + std::string &xml); void convertProtoToXML(const libvirt::MainObj &message, std::string &xml); diff --git a/tests/fuzz/run_fuzz.in b/tests/fuzz/run_fuzz.in index 414b99b6cf..a2b0c66c32 100644 --- a/tests/fuzz/run_fuzz.in +++ b/tests/fuzz/run_fuzz.in @@ -118,6 +118,21 @@ if args.fuzzer == "qemu_xml_hotplug_fuzz": # so we have to disable LeakSanitizer env["ASAN_OPTIONS"] = "detect_leaks=0" +if args.fuzzer == "libxl_xml_domain_fuzz": + # To make LD_PRELOAD work without running into undefined symbol + # errors, we need to override ASAN_SYMBOLIZER_PATH with a wrapper + # that unsets LD_PRELOAD before running the real llvm-symbolizer. + + env["ASAN_SYMBOLIZER_PATH"] = llvm_symbolizer_path + env["LD_PRELOAD"] = f"{abs_builddir}/tests/libxlmock.so" + + # Note: Above workaround breaks multithreaded mode, + # so inform the user of the same. + + if args.jobs > 1: + print("libxl_xml_domain_fuzz cannot be used in parallel mode.") + sys.exit(1) + if args.libfuzzer_options: process_args.extend([x for x in args.libfuzzer_options.split(' ') if x != '']) -- 2.34.1

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

Document the fuzzers in two ways. 1. Explain the high level working of the fuzzers under docs/kbase. 2. Add README to explain general setup of the fuzzer and its usage. Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com> --- docs/kbase/index.rst | 3 + docs/kbase/internals/meson.build | 1 + docs/kbase/internals/xml-fuzzing.rst | 120 ++++++++++++++++++++++++ tests/fuzz/README.rst | 131 +++++++++++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 docs/kbase/internals/xml-fuzzing.rst create mode 100644 tests/fuzz/README.rst diff --git a/docs/kbase/index.rst b/docs/kbase/index.rst index e51b35cbfc..9cf6268800 100644 --- a/docs/kbase/index.rst +++ b/docs/kbase/index.rst @@ -116,3 +116,6 @@ Internals `QEMU monitor event handling <internals/qemu-event-handlers.html>`__ Brief outline how events emitted by qemu on the monitor are handlded. + +`XML Fuzzing <internals/xml-fuzzing.html>`__ + Working of the structure-aware XML fuzzers. diff --git a/docs/kbase/internals/meson.build b/docs/kbase/internals/meson.build index f1e9122f8f..86b6639419 100644 --- a/docs/kbase/internals/meson.build +++ b/docs/kbase/internals/meson.build @@ -9,6 +9,7 @@ docs_kbase_internals_files = [ 'qemu-migration', 'qemu-threads', 'rpc', + 'xml-fuzzing', ] diff --git a/docs/kbase/internals/xml-fuzzing.rst b/docs/kbase/internals/xml-fuzzing.rst new file mode 100644 index 0000000000..85f565fda5 --- /dev/null +++ b/docs/kbase/internals/xml-fuzzing.rst @@ -0,0 +1,120 @@ +=================== +Libvirt XML fuzzing +=================== + +XML fuzzing is done using libFuzzer and libprotobuf-mutator. XML fuzzing +cannot be done with normal fuzzing methods, as XML is a highly structured +format. Structure-aware fuzzing is implemented using libprotobuf-mutator which +mutates and fuzzes protobuf inputs. Protobufs are used as an intermediate +format and serialized to XML. + +Protobuf to XML representation +============================== + +A protobuf definition written to fuzz libvirt XML formats may resemble the +following. + +:: + + message MainObj { + message SomeTagMessage { + optional uint32 A_number = 1; + optional DummyString A_name = 2; + + enum typeEnum { + typeA = 0; + typeB = 1; + typeC = 2; + } + + optional typeEnum A_type = 3; + + message InnerTagMessage { + optional uint32 A_number = 1; + } + + repeated InnerTagMessage T_innertag = 4; + + message SecondInnerTagMessage { + optional uint32 V_value = 1; + } + optional SecondInnerTagMessage T_secondinner = 5; + } + + optional SomeTagMessage T_sometag = 1; + } + +* Fields starting with ``T_`` represent XML tags. Their types are protobuf messages + which may further contain other protobuf-defined XML tags or attributes. + +* Fields starting with ``A_`` represent XML attributes. Most of the time, + it uses one of the primitive datatypes (Eg: ``uint32``, ``bool``, ``enum``, etc. ) available in protobuf. + + * If the attribute can take multiple data types, it is encapsulated in a ``oneof`` statement. + The field name also has a prefix of ``A_OPTXX_`` where ``XX`` is a number between 0 to 99. + * If the attribute name contains special characters, the real name is stored in + ``libvirt::real_name`` which is extended by ``FieldOptions``. + * If an enum value contains special characters, the real value is stored in + ``libvirt::real_value`` which is extended by ``EnumValueOptions``. + +* Fields starting with ``V_`` represent raw text in XML. + + * If ``T_`` and ``V_`` fields are defined in the same message, ``V_`` fields + will be preferred only if it has presence, otherwise it will process the + rest of the ``T`` fields as usual. + * ``V_`` fields can take on the same datatypes as ``A_`` fields. + +* ``repeated`` is used to allow multiple XML tags of the same name. + +``A_`` fields must always precede ``V_`` and ``T_`` fields. Likewise, ``V_`` +fields must precede ``T_`` fields if any. + +On fuzzing the above protobuf definition, one of the possible protobuf to XML +serializations could be + +:: + + <sometag number='1' name='dummy' type='typeB'> + <innertag number='2'/> + <innertag number='3'/> + <secondinner>1241232</secondinner> + </sometag> + +Custom Protobuf Datatypes +------------------------- + +Sometimes, primitive data types or enums are not enough to encode the +desired attribute values, especially if they themselves are structured. In this +case, such fields are represented by a handwritten protobuf message defined in +``xml_domain_datatypes.proto``. To serialize these messages to XML attribute +values, custom handlers are defined in ``proto_custom_datatypes.cc``. + +This is useful for data types such as IP addresses, MAC addresses, target +device names, etc. + +Protobuf generation +=================== + +``proto`` files are automatically generated on compile-time using the script +``relaxng_to_proto.py``. The script parses relaxng schemas to generate a protobuf +file containing fields and messages representing all the defined XML tags and +attributes. + +The script tries to figure out the correct datatype of the XML attribute. +However, on its own it can only figure out the general datatype or enum values +of the attribute but not the constraints or regex patterns. Some override tables +are present to improve upon that. + +Fuzzer Harnesses +================ + +Driver-specific harnesses in general re-use the existing test driver setup +as well as other existing test utilities under ``tests/``. Harnesses are +available for the following drivers: + +* QEMU XML Domain +* QEMU XML Hotplug +* CH XML Domain +* VMX XML Domain +* libXL XML Domain +* NWFilter XML diff --git a/tests/fuzz/README.rst b/tests/fuzz/README.rst new file mode 100644 index 0000000000..d92cdc94d7 --- /dev/null +++ b/tests/fuzz/README.rst @@ -0,0 +1,131 @@ +======= +Fuzzing +======= + +The XML fuzzing project was built as part of Google Summer of Code 2024. +The fuzzing project aims to find edge-case XML configurations that may crash +libvirt during parsing. The libvirt domain XML format is a highly structured +grammar so normal methods of fuzzing will not work. We use a combination +of libFuzzer and libprotobuf-mutator to perform structure-aware fuzzing of +various libvirt XML formats. The XML is represented through an intermediate +protobuf that is mutated by libprotobuf-mutator. This protobuf is automatically +generated by a Python script ``relaxng_to_proto.py`` which parses relaxNG +schemas. + +Currently, we fuzz the following: + +* QEMU XML Domain (qemu_xml_domain_fuzz, qemu_xml_domain_fuzz_disk, qemu_xml_domain_fuzz_interface) +* QEMU XML Hotplug (qemu_xml_hotplug_fuzz) +* CH XML Domain (ch_xml_domain_fuzz) +* VMX XML Domain (vmx_xml_domain_fuzz) +* LibXL XML Domain (libxl_xml_domain_fuzz) +* NWFilter XML (xml_nwfilter_fuzz) + +libprotobuf-mutator +=================== + +libprotobuf-mutator is the crux of our fuzzing methodology that +allows us to perform grammar-aware fuzzing of the XML format in the first +place. However, its setup is a bit involved. The general build and install +instructions can be followed in +https://github.com/google/libprotobuf-mutator/blob/master/README.md +but we will have to tweak it depending on the distro. One of the biggest +problems is that most distros have very outdated versions of protobuf +which will cause various build and linkage issues with the mutator. + +- If you are on a rolling release distro, the system package can likely be + used as-is. However, you may need to pass ``-std=c++17`` in ``CXXFLAGS`` + and ``-Wl,--copy-dt-needed-entries`` in ``LDFLAGS``.\ +- For every other distro with old protobuf installations, you can supply + ``-DLIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON`` during libprotobuf-mutator + setup. After this, provide ``-Dexternal_protobuf_dir=<dir>`` to libvirt + meson setup pointing to the ``external.protobuf`` directory generated + during libprotobuf-mutator compilation. +- On some distros like Fedora which predominantly use PIC compiled + libraries, you may need to pass ``-fPIC`` in ``CFLAGS/CXXFLAGS`` or you + will encounter relocation errors during libvirt compilation. + +Setup +===== + +:: + + env CC=clang CXX=clang++ \ + meson setup build -Dsystem=true -Ddriver_qemu=enabled -Db_lundef=false \ + -Db_sanitize=address,undefined -Dfuzz=enabled -Dexternal_protobuf_dir=<dir> + +- This command line will introduce LLVM SanitizerCoverage across all + object files. +- libFuzzer is supported only on clang/clang++. +- To use an external protobuf dependency, use + ``-Dexternal_protobuf_dir=<dir>``. If your system has a new enough protobuf + dependency, you can ignore this. +- ``b_sanitize`` is not compulsory but it does improve the odds of the fuzzer + finding interesting test cases. It is recommended to pass + ``address,undefined`` to enable both ASAN and UBSan. Note that ASAN will + cut your performance by a factor of 2 on average. +- You can set ``b_sanitize`` to ``thread`` to enable TSAN which is useful for + fuzzing race conditions in the ``qemu_xml_hotplug_fuzz`` fuzzer especially. + +NOTE: This has only been tested on x86_64 and aarch64 Linux, but should work +identically on other architectures and possibly even other UNIX based OSes +(BSD, macOS, etc.). + +Usage +===== + +Run ``./tests/fuzz/run_fuzz <fuzzer>``. + +If the fuzzer finds a crashing test case, it will dump a separate file in your +working directory. Run +``./tests/fuzz/run_fuzz <fuzzer> --testcase <file_name>`` to reproduce the crash. +More options to configure the fuzzer can be found with the ``-h`` flag. To save/ +load a corpus, add ``--corpus <corpus_dir>``. + +To merge or minimize corpuses, run +:: + ./tests/fuzz/run_fuzz <fuzzer> --libfuzzer-options="-merge=1 <dest_corpus> <src_corpus>" + +Notable options are listed below. + +- ``--arch``: Set architecture of the domain XML to fuzz. +- ``-j, --jobs``: Run parallel fuzzing workers using either ``jobs`` or + ``fork`` based on ``--parallel-mode``. Eg: + ``./tests/fuzz/run_fuzz qemu_xml_domain_fuzz -j8 --parallel-mode fork``. +- ``--dump-xml``: Print all fuzzed XMLs (useful for debugging reproducers) +- ``--format-xml``: Exercise format function on XML domain fuzzers. +- ``--corpus``: Save or use corpus on-disk. +- ``--libfuzzer-options``: Pass additional libFuzzer flags as documented in + https://llvm.org/docs/LibFuzzer.html#options. + +Coverage Report +=============== + +- libvirt supports instrumenting builds with gcov for coverage data collection + using ``-Dtest_coverage=true``. +:: + + ./tests/fuzz/run_fuzz <fuzzer> --total_time=<duration> --corpus=<corpus_dir> + ./tests/fuzz/run_fuzz <fuzzer> --corpus=<corpus_dir> --libfuzzer-options="-runs=0" + find -name '*.gcda' -exec llvm-cov gcov {} \; # Run in build directory + gcovr --gcov-executable "llvm-cov gcov" --html-details coverage.html -r <source_directory> + +- Alternatively, we can use clang profile coverage instrumentation + enabled with ``-Dtest_coverage_clang=true``. +:: + + ./tests/fuzz/run_fuzz <fuzzer> --total_time=<duration> --corpus=<corpus_dir> + ./tests/fuzz/run_fuzz <fuzzer> --corpus=<corpus_dir> --llvm-profile-file=coverage.profraw + llvm-profdata merge coverage.profraw -output coverage.profdata + llvm-cov show --instr-profile coverage.profdata <objects> --sources <sources> --format html > coverage.html + +Tips +==== + +- libFuzzer will try to pass comparison checks using its internal TORC + (Table of Recent Comparisons), but this can get easily overwhelmed in the + case of libvirt due to its code being quite complex. You can alleviate + this to some extent by passing ``--use-value-profile`` to the fuzzer. +- If you want the fuzzer to proceed even after encountering a crash, + add ``-j<N> --parallel-mode=fork``. Do note that the memory usage will + increase exponentially with each parallel fuzzing worker. -- 2.34.1

On Mon, Aug 19, 2024 at 09:39:38PM +0530, Rayhan Faizel wrote:
This series introduces multiple fuzzers developed as part of Google Summer of Code 2024. We adopt a structure-aware fuzzing approach to fuzz libvirt XML formats. The fuzzing methodology makes use of libFuzzer and libprotobuf-mutator. The fuzzers work by mutating intermediate protobufs and converting them to XML.
The fuzzing method in use requires inclusion of C++ sources. However, C++ compilation will be done only if '-Dfuzz' is enabled. Otherwise, libvirt will compile normally as before. The fuzzing method works only on clang compilers which support libFuzzer.
Hmm, I wish you'd raised this issue on the list before investing all this work becasue IMHO the dependency on C++ is not something I would want to see in the libvirt project, even just for tests. It was a very delibrate decision that libvirt be a C project, not C++ project, and if we're going to extend libvirt to take code in any new language the choices that make sense looking to the future are Rust or Go, not C++. With regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

On Tue, Aug 20, 2024 at 03:03:47PM +0100, Daniel P. Berrangé wrote:
On Mon, Aug 19, 2024 at 09:39:38PM +0530, Rayhan Faizel wrote:
This series introduces multiple fuzzers developed as part of Google Summer of Code 2024. We adopt a structure-aware fuzzing approach to fuzz libvirt XML formats. The fuzzing methodology makes use of libFuzzer and libprotobuf-mutator. The fuzzers work by mutating intermediate protobufs and converting them to XML.
The fuzzing method in use requires inclusion of C++ sources. However, C++ compilation will be done only if '-Dfuzz' is enabled. Otherwise, libvirt will compile normally as before. The fuzzing method works only on clang compilers which support libFuzzer.
Hmm, I wish you'd raised this issue on the list before investing all this work becasue IMHO the dependency on C++ is not something I would want to see in the libvirt project, even just for tests. It was a very delibrate decision that libvirt be a C project, not C++ project, and if we're going to extend libvirt to take code in any new language the choices that make sense looking to the future are Rust or Go, not C++.
That was unfortunate, but since Rayhan had the first implementation done in a very short time we rather spent the rest of the time enhancing the fuzzing and it definitely bore fruit -- some of the found things are fixed, some are still waiting for a patch or two. The crucial part of this is the existing libprotobuf-mutator which is already in C++ and does provide very specific C++ APIs. Another approach (except writing our own mutator) would be to keep the code in a separate repository. I'm not completely sure whether we would still need the code modifications, I don't remember our discussions about whether the fuzzing compilation could work with all current libvirt code compiled as C and only the fuzzing parts compiled in C++.
With regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

On Tue, Aug 27, 2024 at 11:07 AM Martin Kletzander <mkletzan@redhat.com> wrote:
That was unfortunate, but since Rayhan had the first implementation done in a very short time we rather spent the rest of the time enhancing the fuzzing and it definitely bore fruit -- some of the found things are fixed, some are still waiting for a patch or two.
The crucial part of this is the existing libprotobuf-mutator which is already in C++ and does provide very specific C++ APIs.
Another approach (except writing our own mutator) would be to keep the code in a separate repository. I'm not completely sure whether we would still need the code modifications, I don't remember our discussions about whether the fuzzing compilation could work with all current libvirt code compiled as C and only the fuzzing parts compiled in C++.
All the existing code is indeed still compiled as C. Only the fuzzing executables (under tests/fuzz/) are compiled in C++ and linked to those C objects. We still do need some of the minor code modifications (in PATCH 1) because existing C headers are sometimes interpreted a bit differently from the fuzzer's PoV, even with C linkage. There are still some other code modifications in tests/ and src/ for a few other fuzzers (mostly hotplug and CH) to make fuzzing easier. I agree that we could keep it as a separate repo, perhaps a subproject. I have seen some projects keep their fuzzing code separate (mostly on oss-fuzz). -- Rayhan Faizel

On Tue, Aug 27, 2024 at 12:08:27PM +0300, Rayhan Faizel wrote:
On Tue, Aug 27, 2024 at 11:07 AM Martin Kletzander <mkletzan@redhat.com> wrote:
That was unfortunate, but since Rayhan had the first implementation done in a very short time we rather spent the rest of the time enhancing the fuzzing and it definitely bore fruit -- some of the found things are fixed, some are still waiting for a patch or two.
The crucial part of this is the existing libprotobuf-mutator which is already in C++ and does provide very specific C++ APIs.
Another approach (except writing our own mutator) would be to keep the code in a separate repository. I'm not completely sure whether we would still need the code modifications, I don't remember our discussions about whether the fuzzing compilation could work with all current libvirt code compiled as C and only the fuzzing parts compiled in C++.
All the existing code is indeed still compiled as C. Only the fuzzing executables (under tests/fuzz/) are compiled in C++ and linked to those C objects. We still do need some of the minor code modifications (in PATCH 1) because existing C headers are sometimes interpreted a bit differently from the fuzzer's PoV, even with C linkage.
Sorry, what I meant is whether it would be possible to keep the code as is, the keyword parameters are a bit of a problem, but writing a layer of C code to call it through from the C++ code feels weird. Of course attributes are also complicated to make work, but those changes in the C code are pretty okay I think.
There are still some other code modifications in tests/ and src/ for a few other fuzzers (mostly hotplug and CH) to make fuzzing easier.
I agree that we could keep it as a separate repo, perhaps a subproject. I have seen some projects keep their fuzzing code separate (mostly on oss-fuzz).
-- Rayhan Faizel

On Tue, Aug 27, 2024 at 1:51 PM Martin Kletzander <mkletzan@redhat.com> wrote:
On Tue, Aug 27, 2024 at 12:08:27PM +0300, Rayhan Faizel wrote:
All the existing code is indeed still compiled as C. Only the fuzzing executables (under tests/fuzz/) are compiled in C++ and linked to those C objects. We still do need some of the minor code modifications (in PATCH 1) because existing C headers are sometimes interpreted a bit differently from the fuzzer's PoV, even with C linkage.
Sorry, what I meant is whether it would be possible to keep the code as is, the keyword parameters are a bit of a problem, but writing a layer of C code to call it through from the C++ code feels weird. Of course attributes are also complicated to make work, but those changes in the C code are pretty okay I think.
Sorry, I am not sure I fully understand the first statement regarding writing a layer of C code. I had only replaced the keyword parameters with alternative names in PATCH 1.
There are still some other code modifications in tests/ and src/ for a few other fuzzers (mostly hotplug and CH) to make fuzzing easier.
I agree that we could keep it as a separate repo, perhaps a subproject. I have seen some projects keep their fuzzing code separate (mostly on oss-fuzz).
-- Rayhan Faizel
-- Rayhan Faizel

On Tue, Aug 27, 2024 at 02:15:10PM +0300, Rayhan Faizel wrote:
On Tue, Aug 27, 2024 at 1:51 PM Martin Kletzander <mkletzan@redhat.com> wrote:
On Tue, Aug 27, 2024 at 12:08:27PM +0300, Rayhan Faizel wrote:
All the existing code is indeed still compiled as C. Only the fuzzing executables (under tests/fuzz/) are compiled in C++ and linked to those C objects. We still do need some of the minor code modifications (in PATCH 1) because existing C headers are sometimes interpreted a bit differently from the fuzzer's PoV, even with C linkage.
Sorry, what I meant is whether it would be possible to keep the code as is, the keyword parameters are a bit of a problem, but writing a layer of C code to call it through from the C++ code feels weird. Of course attributes are also complicated to make work, but those changes in the C code are pretty okay I think.
Sorry, I am not sure I fully understand the first statement regarding writing a layer of C code. I had only replaced the keyword parameters with alternative names in PATCH 1.
My bad, I was trying to be brief and overdone it. What I meant is a function that would look like the following, but it would not be a very nice solution: int callableFromCPlusPlus(int a, int b) { return orig(a, b); } and call that one from C++. Now that I think about it, it could be even easier, and maybe more awkward, if you only changed the declarations in the header. But anyway, we're getting sidetracked, sorry for that.
There are still some other code modifications in tests/ and src/ for a few other fuzzers (mostly hotplug and CH) to make fuzzing easier.
I agree that we could keep it as a separate repo, perhaps a subproject. I have seen some projects keep their fuzzing code separate (mostly on oss-fuzz).
-- Rayhan Faizel
-- Rayhan Faizel

On Tue, Aug 27, 2024 at 10:06:58AM +0200, Martin Kletzander wrote:
On Tue, Aug 20, 2024 at 03:03:47PM +0100, Daniel P. Berrangé wrote:
On Mon, Aug 19, 2024 at 09:39:38PM +0530, Rayhan Faizel wrote:
This series introduces multiple fuzzers developed as part of Google Summer of Code 2024. We adopt a structure-aware fuzzing approach to fuzz libvirt XML formats. The fuzzing methodology makes use of libFuzzer and libprotobuf-mutator. The fuzzers work by mutating intermediate protobufs and converting them to XML.
The fuzzing method in use requires inclusion of C++ sources. However, C++ compilation will be done only if '-Dfuzz' is enabled. Otherwise, libvirt will compile normally as before. The fuzzing method works only on clang compilers which support libFuzzer.
Hmm, I wish you'd raised this issue on the list before investing all this work becasue IMHO the dependency on C++ is not something I would want to see in the libvirt project, even just for tests. It was a very delibrate decision that libvirt be a C project, not C++ project, and if we're going to extend libvirt to take code in any new language the choices that make sense looking to the future are Rust or Go, not C++.
That was unfortunate, but since Rayhan had the first implementation done in a very short time we rather spent the rest of the time enhancing the fuzzing and it definitely bore fruit -- some of the found things are fixed, some are still waiting for a patch or two.
Do you have pointers to the list of things that it found ?
The crucial part of this is the existing libprotobuf-mutator which is already in C++ and does provide very specific C++ APIs.
I'm struggling a little to understand exactly what kind of changes this code actually produces ? Are there examples of the mutated XML files showing these changes ?
Another approach (except writing our own mutator) would be to keep the code in a separate repository. I'm not completely sure whether we would still need the code modifications, I don't remember our discussions about whether the fuzzing compilation could work with all current libvirt code compiled as C and only the fuzzing parts compiled in C++.
If we could have it in a separate repo, and NOT have to change libvirt code to avoid C++ keywords/etc, then that could make it more palatable. Ultimately though the libvirt maintainers are still on the hook to maintain C++ code long term now, so a separate repo just stops the C++ stuff spreading :-( With regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|

On Mon, Sep 02, 2024 at 03:46:26PM +0100, Daniel P. Berrangé wrote:
On Tue, Aug 27, 2024 at 10:06:58AM +0200, Martin Kletzander wrote:
On Tue, Aug 20, 2024 at 03:03:47PM +0100, Daniel P. Berrangé wrote:
On Mon, Aug 19, 2024 at 09:39:38PM +0530, Rayhan Faizel wrote:
This series introduces multiple fuzzers developed as part of Google Summer of Code 2024. We adopt a structure-aware fuzzing approach to fuzz libvirt XML formats. The fuzzing methodology makes use of libFuzzer and libprotobuf-mutator. The fuzzers work by mutating intermediate protobufs and converting them to XML.
The fuzzing method in use requires inclusion of C++ sources. However, C++ compilation will be done only if '-Dfuzz' is enabled. Otherwise, libvirt will compile normally as before. The fuzzing method works only on clang compilers which support libFuzzer.
Hmm, I wish you'd raised this issue on the list before investing all this work becasue IMHO the dependency on C++ is not something I would want to see in the libvirt project, even just for tests. It was a very delibrate decision that libvirt be a C project, not C++ project, and if we're going to extend libvirt to take code in any new language the choices that make sense looking to the future are Rust or Go, not C++.
That was unfortunate, but since Rayhan had the first implementation done in a very short time we rather spent the rest of the time enhancing the fuzzing and it definitely bore fruit -- some of the found things are fixed, some are still waiting for a patch or two.
Do you have pointers to the list of things that it found ?
Oh, I thought the link to the write-up was somewhere in here, sorry. https://gitlab.com/Skryptonyte/libvirt-gsoc-finalreport
The crucial part of this is the existing libprotobuf-mutator which is already in C++ and does provide very specific C++ APIs.
I'm struggling a little to understand exactly what kind of changes this code actually produces ? Are there examples of the mutated XML files showing these changes ?
See the link above. Once The fuzzing is running (and it had pretty quick results for me) it is written in a way that the conflicting XMLs can be produced from the results.
Another approach (except writing our own mutator) would be to keep the code in a separate repository. I'm not completely sure whether we would still need the code modifications, I don't remember our discussions about whether the fuzzing compilation could work with all current libvirt code compiled as C and only the fuzzing parts compiled in C++.
If we could have it in a separate repo, and NOT have to change libvirt code to avoid C++ keywords/etc, then that could make it more palatable. Ultimately though the libvirt maintainers are still on the hook to maintain C++ code long term now, so a separate repo just stops the C++ stuff spreading :-(
It is definitely possible, the question is how long until that becomes stale/out of date.
With regards, Daniel -- |: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|
participants (3)
-
Daniel P. Berrangé
-
Martin Kletzander
-
Rayhan Faizel