I decided to fuzz nokogiri again, because despite not finding any bugs previously, I have now written up a custom mutator with which to play with…
My custom mutator repeats a string many times. This can be used to find Denial Of Service vulnerabilities in software.
To enable the use of my custom mutator with nokogiri, I had to apply the next patch:
diff --git a/gumbo-parser/Makefile b/gumbo-parser/Makefile
index f29a38a3..b5ba4a9d 100644
--- a/gumbo-parser/Makefile
+++ b/gumbo-parser/Makefile
@@ -6,10 +6,10 @@ gtest_lib := googletest/make/gtest_main.a
# make SANITIZEFLAGS='-fsanitize=undefined -fsanitize=address'
SANITIZEFLAGS :=
-CPPFLAGS := -Isrc
-CFLAGS := -std=c99 -Os -Wall
+CPPFLAGS := -Isrc /usr/include/python3.10 -L/usr/lib/python3.10/config-3.10-x86_64-linux-gnu/ -lpython3.10 # -I/usr/include/python3.6m -lpython3.6m
+CFLAGS := -std=c99 -Os -Wall -Isrc /usr/include/python3.10 -L/usr/lib/python3.10/config-3.10-x86_64-linux-gnu/ -lpython3.10
CXXFLAGS := -isystem googletest/include -std=c++11 -Os -Wall
-LDFLAGS := -pthread
+LDFLAGS := -pthread -Isrc /usr/include/python3.10 -L/usr/lib/python3.10/config-3.10-x86_64-linux-gnu/ -lpython3.10
all: check
@@ -61,6 +61,8 @@ build/run_tests: $(gumbo_objs) $(test_objs) $(gtest_lib)
check: build/run_tests
./build/run_tests
+# -I/usr/include/python3.6m -lpython3.6m
+
coverage:
$(RM) build/{src,test}/*.gcda
$(RM) build/*.info
diff --git a/gumbo-parser/fuzzer/build.sh b/gumbo-parser/fuzzer/build.sh
index 849cd12f..373c3b81 100755
--- a/gumbo-parser/fuzzer/build.sh
+++ b/gumbo-parser/fuzzer/build.sh
@@ -28,8 +28,8 @@ srcdir=src-${SANITIZER}
CC="$($LLVM_CONFIG --bindir)/clang"
CXX="$($LLVM_CONFIG --bindir)/clang++"
-CXXFLAGS="-fsanitize=fuzzer-no-link"
-CFLAGS="-fsanitize=fuzzer-no-link"
+CXXFLAGS="-fsanitize=fuzzer-no-link -Isrc -I/usr/include/python3.10 -L/usr/lib/python3.10/config-3.10-x86_64-linux-gnu/ -lpython3.10"
+CFLAGS="-fsanitize=fuzzer-no-link -Isrc -I/usr/include/python3.10 -L/usr/lib/python3.10/config-3.10-x86_64-linux-gnu/ -lpython3.10"
ENGINE_LINK="$(find $($LLVM_CONFIG --libdir) -name libclang_rt.fuzzer-x86_64.a | head -1)"
if [[ "${SANITIZER}" = "ubsan" ]] ; then
diff --git a/gumbo-parser/fuzzer/parse_fuzzer.cc b/gumbo-parser/fuzzer/parse_fuzzer.cc
index 234d7b72..5d18de56 100644
--- a/gumbo-parser/fuzzer/parse_fuzzer.cc
+++ b/gumbo-parser/fuzzer/parse_fuzzer.cc
@@ -36,6 +36,247 @@ int SanityCheckPointers(const char* input, size_t input_length, const GumboNode*
return 0;
}
+
+
+
+// This is to use our custom python mutator type thing...
+
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <Python.h>
+
+static void LLVMFuzzerFinalizePythonModule();
+static void LLVMFuzzerInitPythonModule();
+
+static PyObject* py_module = NULL;
+
+class LLVMFuzzerPyContext {
+ public:
+ LLVMFuzzerPyContext() {
+ if (!py_module) {
+ LLVMFuzzerInitPythonModule();
+ }
+ }
+ ~LLVMFuzzerPyContext() {
+ if (py_module) {
+ LLVMFuzzerFinalizePythonModule();
+ }
+ }
+};
+
+// This takes care of (de)initializing things properly
+LLVMFuzzerPyContext init;
+
+static void py_fatal_error() {
+ fprintf(stderr, "The libFuzzer Python layer encountered a critical error.\n");
+ fprintf(stderr, "Please fix the messages above and then restart fuzzing.\n");
+ exit(1);
+}
+
+enum {
+ /* 00 */ PY_FUNC_CUSTOM_MUTATOR,
+ /* 01 */ PY_FUNC_CUSTOM_CROSSOVER,
+ PY_FUNC_COUNT
+};
+
+static PyObject* py_functions[PY_FUNC_COUNT];
+
+// Forward-declare the libFuzzer's mutator callback.
+extern "C" size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize);
+
+// This function unwraps the Python arguments passed, which must be
+//
+// 1) A bytearray containing the data to be mutated
+// 2) An int containing the maximum size of the new mutation
+//
+// The function will modify the bytearray in-place (and resize it accordingly)
+// if necessary. It returns None.
+PyObject* LLVMFuzzerMutatePyCallback(PyObject* data, PyObject* args) {
+ PyObject* py_value;
+
+ // Get MaxSize first, so we know how much memory we need to allocate
+ py_value = PyTuple_GetItem(args, 1);
+ if (!py_value) {
+ fprintf(stderr, "Error: Missing MaxSize argument to native callback.\n");
+ py_fatal_error();
+ }
+ size_t MaxSize = PyLong_AsSize_t(py_value);
+ if (MaxSize == (size_t)-1 && PyErr_Occurred()) {
+ PyErr_Print();
+ fprintf(stderr, "Error: Failed to convert MaxSize argument to size_t.\n");
+ py_fatal_error();
+ }
+
+ // Now get the ByteArray with our data and resize it appropriately
+ py_value = PyTuple_GetItem(args, 0);
+ size_t Size = (size_t)PyByteArray_Size(py_value);
+ if (PyByteArray_Resize(py_value, MaxSize) < 0) {
+ fprintf(stderr, "Error: Failed to resize ByteArray to MaxSize.\n");
+ py_fatal_error();
+ }
+
+ // Call libFuzzer's native mutator
+ size_t RetLen =
+ LLVMFuzzerMutate((uint8_t *)PyByteArray_AsString(py_value), Size, MaxSize);
+
+ if (PyByteArray_Resize(py_value, RetLen) < 0) {
+ fprintf(stderr, "Error: Failed to resize ByteArray to RetLen.\n");
+ py_fatal_error();
+ }
+
+ Py_RETURN_NONE;
+}
+
+static PyMethodDef LLVMFuzzerMutatePyMethodDef = {
+ "LLVMFuzzerMutate",
+ LLVMFuzzerMutatePyCallback,
+ METH_VARARGS | METH_STATIC,
+ NULL
+};
+
+static void LLVMFuzzerInitPythonModule() {
+ Py_Initialize();
+ char* module_name = getenv("LIBFUZZER_PYTHON_MODULE");
+
+ if (module_name) {
+ PyObject* py_name = PyUnicode_FromString(module_name);
+
+ py_module = PyImport_Import(py_name);
+ Py_DECREF(py_name);
+
+ if (py_module != NULL) {
+ py_functions[PY_FUNC_CUSTOM_MUTATOR] =
+ PyObject_GetAttrString(py_module, "custom_mutator");
+ py_functions[PY_FUNC_CUSTOM_CROSSOVER] =
+ PyObject_GetAttrString(py_module, "custom_crossover");
+
+ if (!py_functions[PY_FUNC_CUSTOM_MUTATOR]
+ || !PyCallable_Check(py_functions[PY_FUNC_CUSTOM_MUTATOR])) {
+ if (PyErr_Occurred())
+ PyErr_Print();
+ fprintf(stderr, "Error: Cannot find/call custom mutator function in"
+ " external Python module.\n");
+ py_fatal_error();
+ }
+
+ if (!py_functions[PY_FUNC_CUSTOM_CROSSOVER]
+ || !PyCallable_Check(py_functions[PY_FUNC_CUSTOM_CROSSOVER])) {
+ if (PyErr_Occurred())
+ PyErr_Print();
+ fprintf(stderr, "Warning: Python module does not implement crossover"
+ " API, standard crossover will be used.\n");
+ py_functions[PY_FUNC_CUSTOM_CROSSOVER] = NULL;
+ }
+ } else {
+ if (PyErr_Occurred())
+ PyErr_Print();
+ fprintf(stderr, "Error: Failed to load external Python module \"%s\"\n",
+ module_name);
+ py_fatal_error();
+ }
+ } else {
+ fprintf(stderr, "Warning: No Python module specified, using the default libfuzzer mutator (for now).\n");
+ // py_fatal_error();
+ }
+
+
+}
+
+static void LLVMFuzzerFinalizePythonModule() {
+ if (py_module != NULL) {
+ uint32_t i;
+ for (i = 0; i < PY_FUNC_COUNT; ++i)
+ Py_XDECREF(py_functions[i]);
+ Py_DECREF(py_module);
+ }
+ Py_Finalize();
+}
+
+extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
+ size_t MaxSize, unsigned int Seed) {
+ // First check if the custom python mutator is specified:
+ if (!py_module) { // No custom python mutator, so therefore just mutate regularly. (LLVMFuzzerMutate is the default mutator.)
+ return LLVMFuzzerMutate(Data, Size, MaxSize);
+ }
+ PyObject* py_args = PyTuple_New(4);
+
+ // Convert Data and Size to a ByteArray
+ PyObject* py_value = PyByteArray_FromStringAndSize((const char*)Data, Size);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ fprintf(stderr, "Error: Failed to convert buffer.\n");
+ py_fatal_error();
+ }
+ PyTuple_SetItem(py_args, 0, py_value);
+
+ // Convert MaxSize to a PyLong
+ py_value = PyLong_FromSize_t(MaxSize);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ fprintf(stderr, "Error: Failed to convert maximum size.\n");
+ py_fatal_error();
+ }
+ PyTuple_SetItem(py_args, 1, py_value);
+
+ // Convert Seed to a PyLong
+ py_value = PyLong_FromUnsignedLong((unsigned long)Seed);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ fprintf(stderr, "Error: Failed to convert seed.\n");
+ py_fatal_error();
+ }
+ PyTuple_SetItem(py_args, 2, py_value);
+
+ PyObject* py_callback = PyCFunction_New(&LLVMFuzzerMutatePyMethodDef, NULL);
+ if (!py_callback) {
+ fprintf(stderr, "Failed to create native callback\n");
+ py_fatal_error();
+ }
+
+ // Pass the native callback
+ PyTuple_SetItem(py_args, 3, py_callback);
+
+ py_value = PyObject_CallObject(py_functions[PY_FUNC_CUSTOM_MUTATOR], py_args);
+
+ Py_DECREF(py_args);
+ Py_DECREF(py_callback);
+
+ if (py_value != NULL) {
+ ssize_t ReturnedSize = PyByteArray_Size(py_value);
+ if (ReturnedSize > MaxSize) {
+ fprintf(stderr, "Warning: Python module returned buffer that exceeds "
+ "the maximum size. Returning a truncated buffer.\n");
+ ReturnedSize = MaxSize;
+ }
+ memcpy(Data, PyByteArray_AsString(py_value), ReturnedSize);
+ Py_DECREF(py_value);
+ // return ReturnedSize; // Instead of returning the python custom mutator, we should also try to use the original custom mutator too (maybe).
+ if (getenv("FUZZ_ONLY_CUSTOM")) { // Only fuzz with the custom mutator
+ return ReturnedSize;
+ }
+
+
+ return LLVMFuzzerMutate(Data, ReturnedSize, MaxSize);
+
+ } else {
+ if (PyErr_Occurred())
+ PyErr_Print();
+ fprintf(stderr, "Error: Call failed\n");
+ py_fatal_error();
+ }
+ return 0;
+}
+
+
+
+
+
+
+
+
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
/* arbitrary upper size limit to avoid "out-of-memory in parse_fuzzer" reports */
if (size < 10 | size > 25000) {
after doing that and compiling the fuzzers, they seem to work fine… except hold on, there is a fucking hardcoded limit to the fuzz stuff:
/* arbitrary upper size limit to avoid "out-of-memory in parse_fuzzer" reports */
if (size < 10 | size > 25000) {
return 0;
}
We need to get rid of that.