diff --git a/lib/common/fuzzers/xml_fuzzer.c b/lib/common/fuzzers/xml_fuzzer.c new file mode 100644 index 00000000000..638d344231d --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer.c @@ -0,0 +1,165 @@ +/* + * Copyright 2026 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include // dirname() +#include // PATH_MAX +#include // bool, false, true +#include // uint8_t +#include // free(), getenv(), setenv() +#include // memcpy() +#include // readlink() + +#include // GString, g_string_sized_new() + +#include + +/* The input is treated as a CIB document and taken through the stages a real + * configuration goes through: parse, walk, serialize, validate against a + * schema, then upgrade to the newest schema. Each stage lives in a different + * source file, so one input covers the whole XML pipeline. + */ + +// Schema copy shipped beside the fuzzer binary +#define SCHEMA_SUBDIR "pacemaker-schemas" + +// Maximum element depth to walk (a deep document costs more to walk than parse) +#define MAX_DEPTH 8 + +static bool initialized = false; + +/*! + * \internal + * \brief Point \c PCMK_schema_directory at the schema copy beside this binary + * + * \note The schema cache reads .rng and upgrade .xsl files from disk, so the + * validate and upgrade stages return early unless it can find them. An + * existing value is left alone, so a caller can override the location. + */ +static void +set_schema_dir(void) +{ + char exe[PATH_MAX] = { '\0', }; + char *schema_dir = NULL; + ssize_t len = 0; + + if (getenv("PCMK_schema_directory") != NULL) { + return; + } + + len = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (len <= 0) { + return; + } + exe[len] = '\0'; + + schema_dir = pcmk__assert_asprintf("%s/%s", dirname(exe), SCHEMA_SUBDIR); + setenv("PCMK_schema_directory", schema_dir, 0); + free(schema_dir); +} + +/*! + * \internal + * \brief Recursively parse every attribute of every element + * + * \param[in] xml XML element to walk the children of + * \param[in] depth Current recursion depth + * + * \note The parsed values are discarded. What is under test is turning an + * arbitrary attribute string into a score, a number, a boolean, or a + * date/time, not whatever any given attribute happens to hold. + */ +static void +walk_children(const xmlNode *xml, int depth) +{ + if ((xml == NULL) || (depth >= MAX_DEPTH)) { + return; + } + + for (const xmlNode *child = pcmk__xe_first_child(xml, NULL, NULL, NULL); + child != NULL; child = pcmk__xe_next(child, NULL)) { + + for (const xmlAttr *attr = pcmk__xe_first_attr(child); attr != NULL; + attr = attr->next) { + + const char *name = (const char *) attr->name; + int score = 0; + long long ll = 0; + bool boolean = false; + crm_time_t *t = NULL; + + pcmk__xe_get_score(child, name, &score, 0); + pcmk__xe_get_ll(child, name, &ll); + pcmk__xe_get_bool(child, name, &boolean); + + if (pcmk__xe_get_datetime(child, name, &t) == pcmk_rc_ok) { + free(t); + } + } + + walk_children(child, depth + 1); + } +} + +int +LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + char *ns = NULL; + xmlNode *xml = NULL; + xmlNode *upgraded = NULL; + GString *buffer = NULL; + + // Have at least some data + if (size < 5) { + return -1; // Do not add input to testing corpus + } + + if (!initialized) { + set_schema_dir(); + pcmk__schema_init(); + initialized = true; + } + + // pcmk__assert_alloc() zeroes the memory, so the string is terminated + ns = pcmk__assert_alloc(size + 1, sizeof(char)); + memcpy(ns, data, size); + + xml = pcmk__xml_parse(ns); + if (xml == NULL) { + goto done; + } + + walk_children(xml, 0); + + buffer = g_string_sized_new(1024); + pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_open + |pcmk__xml_fmt_children|pcmk__xml_fmt_close + |pcmk__xml_fmt_text, buffer, 0); + + pcmk__validate_xml(xml, NULL, NULL); + + /* pcmk__update_schema() replaces the node it is given, so hand it a copy + * and free whatever comes back. Only a crash along the way is of interest + * here, so its return code is ignored. + */ + upgraded = pcmk__xml_copy(NULL, xml); + if (upgraded != NULL) { + pcmk__update_schema(&upgraded, NULL, true, false); + } + +done: + if (buffer != NULL) { + g_string_free(buffer, TRUE); + } + pcmk__xml_free(upgraded); + pcmk__xml_free(xml); + free(ns); + return 0; +} diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_acls.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_acls.xml new file mode 100644 index 00000000000..92f2df6921d --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_acls.xml @@ -0,0 +1 @@ + diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_bad_schema.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_bad_schema.xml new file mode 100644 index 00000000000..00eeb9a71fd --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_bad_schema.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_dup_id.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_dup_id.xml new file mode 100644 index 00000000000..b3daa70de2c --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_dup_id.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_entity.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_entity.xml new file mode 100644 index 00000000000..b5af74673e9 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_entity.xml @@ -0,0 +1 @@ +]>&e; \ No newline at end of file diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_minimal.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_minimal.xml new file mode 100644 index 00000000000..996add675ea --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_minimal.xml @@ -0,0 +1 @@ + diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_no_validate.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_no_validate.xml new file mode 100644 index 00000000000..5452e266627 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_no_validate.xml @@ -0,0 +1 @@ + diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_old_schema.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_old_schema.xml new file mode 100644 index 00000000000..055ee8d4960 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_old_schema.xml @@ -0,0 +1 @@ + diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_resource.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_resource.xml new file mode 100644 index 00000000000..4a00d8fb8f4 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_resource.xml @@ -0,0 +1 @@ + diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_rules.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_rules.xml new file mode 100644 index 00000000000..1b5e8ab40e6 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_rules.xml @@ -0,0 +1 @@ + diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_status.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_status.xml new file mode 100644 index 00000000000..fe1f650af41 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_status.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/common/fuzzers/xml_fuzzer_corpus/cib_truncated.xml b/lib/common/fuzzers/xml_fuzzer_corpus/cib_truncated.xml new file mode 100644 index 00000000000..1229c807240 --- /dev/null +++ b/lib/common/fuzzers/xml_fuzzer_corpus/cib_truncated.xml @@ -0,0 +1 @@ +