Commit Diff


commit - e52d181485a11b13f0951adb604e18be363ab8c9
commit + 847dd20811eb947fa67fb6e3c076f815f80d1a51
blob - b9e81c8dc7ffbf08ff895aea08a0228091f1acde
blob + 1c304d5b09927c1b1034ca87595766651c44ab11
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -12,3 +12,4 @@ and this project adheres to [Semantic Versioning](http
 - Integration with libFuzzer's `LLVMFuzzerTestOneInput()`.
 - Integration with libFuzzer's `LLVMFuzzerCustomMutator()`.
 - Integration with libFuzzer's `FuzzedDataProvider`.
+- libFuzzer custom mutator for Lua.
blob - /dev/null
blob + 08b64c368dcf8ed5dc24d8379bf0691a4ac8159c (mode 644)
--- /dev/null
+++ mutator/CMakeLists.txt
@@ -0,0 +1,10 @@
+if(ENABLE_TESTING)
+  add_subdirectory(tests)
+endif()
+
+install(
+  FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/mutator.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/crossover.c
+  DESTINATION ${CMAKE_LUADIR}/mutator/
+)
blob - /dev/null
blob + 13d11e8d1aab431f7b02fa1956204953ba3d46f1 (mode 644)
--- /dev/null
+++ mutator/crossover.c
@@ -0,0 +1,78 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+#include <assert.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+static const char *script_default = "./mutator.lua";
+
+static size_t
+luaL_custom_crossover(lua_State* L, const char *path, const char *func_name,
+					  const uint8_t *data1, size_t size1,
+					  const uint8_t *data2, size_t size2,
+					  size_t max_out_size, unsigned int seed)
+{
+	luaL_dofile(L, path);
+	lua_getglobal(L, func_name);
+	if (!lua_isfunction(L, -1)) {
+		luaL_error(L, "'%s' is not a function", func_name);
+	}
+	lua_pushlstring(L, (const char*)data1, size1);
+	lua_pushlstring(L, (const char*)data2, size2);
+	lua_pushinteger(L, max_out_size);
+	lua_pushinteger(L, seed);
+	const int num_args = 4;
+	const int num_return_values = 2;
+	lua_pcall(L, num_args, num_return_values, 0);
+
+	if (!lua_isnumber(L, -1)) {
+		luaL_error(L, "'%s' must return an integer value", func_name);
+	}
+	size_t ret_size = lua_tointeger(L, -1);
+	lua_pop(L, 1);
+
+	if (!lua_isstring(L, -1)) {
+		luaL_error(L, "'%s' must return a string value", func_name);
+	}
+	data1 = (uint8_t *)lua_tostring(L, -1); /* FIXME */
+	lua_pop(L, 1);
+
+	return ret_size;
+}
+
+size_t LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1,
+                                 const uint8_t *Data2, size_t Size2,
+                                 uint8_t *Out, size_t MaxOutSize,
+                                 unsigned int Seed)
+{
+	const char *script_env = "LIBFUZZER_LUA_SCRIPT";
+	const char *script_func = "LLVMFuzzerCustomCrossOver";
+	const char *script_path = getenv(script_env) ? : script_default;
+
+	if (access(script_path, F_OK) != 0) {
+		fprintf(stderr, "Script (%s) is not accessible.\n", script_path);
+		_exit(1);
+	}
+
+	lua_State* L = luaL_newstate();
+	if (!L) {
+		fprintf(stderr, "Unable to create Lua state.\n");
+		abort();
+	}
+	luaL_openlibs(L);
+	size_t size = luaL_custom_crossover(L, script_path, script_func,
+										Data1, Size1, Data2, Size2, MaxOutSize, Seed);
+	lua_close(L);
+
+	return size;
+}
blob - /dev/null
blob + 3dccc65c42f2839278079e4b7542d39a01d98704 (mode 644)
--- /dev/null
+++ mutator/mutator.c
@@ -0,0 +1,77 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+#include <assert.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+static const char *script_default = "./mutator.lua";
+
+static size_t
+luaL_custom_mutator(lua_State* L, const char *path, const char *func_name,
+					uint8_t *data, size_t size,
+					size_t max_size, unsigned int seed)
+{
+	luaL_dofile(L, path);
+	lua_getglobal(L, func_name);
+	if (!lua_isfunction(L, -1))
+		luaL_error(L, "'%s' is not a function", func_name);
+	lua_pushlstring(L, (const char*)data, size);
+	lua_pushinteger(L, max_size);
+	lua_pushinteger(L, seed);
+	/* do the call (3 arguments, 2 results) */
+	if (lua_pcall(L, 3, 2, 0) != 0)
+		luaL_error(L, "error running function '%s': %s",
+				   func_name, lua_tostring(L, -1));
+
+	if (!lua_isnumber(L, -1)) {
+		luaL_error(L, "'%s' must return a number", func_name);
+	}
+	size_t ret_size = lua_tonumber(L, -1) - 1;
+	lua_pop(L, 1);
+
+	if (!lua_isstring(L, -1)) {
+		luaL_error(L, "'%s' must return a string", func_name);
+	}
+	const char *res = lua_tolstring(L, -1, &ret_size);
+	lua_pop(L, 1);
+
+	*data = *res;
+
+	return ret_size;
+}
+
+size_t
+LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
+                        size_t MaxSize, unsigned int Seed)
+{
+	const char *script_env = "LIBFUZZER_LUA_SCRIPT";
+	const char *script_func = "LLVMFuzzerCustomMutator";
+	const char *script_path = getenv(script_env) ? : script_default;
+
+	if (access(script_path, F_OK) != 0) {
+		fprintf(stderr, "Script (%s) is not accessible.\n", script_path);
+		_exit(1);
+	}
+
+	lua_State* L = luaL_newstate();
+	if (!L) {
+		fprintf(stderr, "Unable to create Lua state.\n");
+		abort();
+	}
+	luaL_openlibs(L);
+	size_t ret_size = luaL_custom_mutator(L, script_path, script_func,
+										  Data, Size, MaxSize, Seed);
+	lua_close(L);
+
+	return ret_size;
+}
blob - /dev/null
blob + 3477cff2d884071d10e8811f359e9bc18e4e8b88 (mode 644)
--- /dev/null
+++ mutator/tests/CMakeLists.txt
@@ -0,0 +1,119 @@
+add_executable(mutator_basic_test
+               ${PROJECT_SOURCE_DIR}/mutator/mutator.c
+               ${CMAKE_CURRENT_SOURCE_DIR}/mutator_basic_test.c)
+target_include_directories(mutator_basic_test PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(mutator_basic_test PRIVATE ${LUA_LIBRARIES})
+target_compile_options(mutator_basic_test PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+add_executable(mutator_seed_test
+               ${PROJECT_SOURCE_DIR}/mutator/mutator.c
+               ${CMAKE_CURRENT_SOURCE_DIR}/mutator_seed_test.c)
+target_include_directories(mutator_seed_test PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(mutator_seed_test PRIVATE ${LUA_LIBRARIES})
+target_compile_options(mutator_seed_test PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+add_executable(mutator_e2e_test
+               ${PROJECT_SOURCE_DIR}/mutator/mutator.c
+               ${CMAKE_CURRENT_SOURCE_DIR}/mutator_e2e_test.c)
+target_include_directories(mutator_e2e_test PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(mutator_e2e_test PRIVATE ${LUA_LIBRARIES} -fsanitize=address,fuzzer)
+target_compile_options(mutator_e2e_test PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+add_executable(crossover_basic_test
+               ${PROJECT_SOURCE_DIR}/mutator/crossover.c
+               ${CMAKE_CURRENT_SOURCE_DIR}/crossover_basic_test.c)
+target_include_directories(crossover_basic_test PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(crossover_basic_test PRIVATE ${LUA_LIBRARIES})
+target_compile_options(crossover_basic_test PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+add_executable(crossover_seed_test
+               ${PROJECT_SOURCE_DIR}/mutator/crossover.c
+               ${CMAKE_CURRENT_SOURCE_DIR}/crossover_seed_test.c)
+target_include_directories(crossover_seed_test PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(crossover_seed_test PRIVATE ${LUA_LIBRARIES})
+target_compile_options(crossover_seed_test PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+add_executable(crossover_e2e_test
+               ${PROJECT_SOURCE_DIR}/mutator/crossover.c
+               ${CMAKE_CURRENT_SOURCE_DIR}/crossover_e2e_test.c)
+target_include_directories(crossover_e2e_test PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(crossover_e2e_test PRIVATE ${LUA_LIBRARIES} -fsanitize=address,fuzzer)
+target_compile_options(crossover_e2e_test PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+add_test(
+  NAME mutator_basic_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_basic_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(mutator_basic_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/script_basic.lua"
+)
+
+add_test(
+  NAME mutator_seed_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_seed_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(mutator_seed_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/script_seed.lua"
+)
+
+add_test(
+  NAME mutator_e2e_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_e2e_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(mutator_e2e_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/script_e2e.lua"
+  PASS_REGULAR_EXPRESSION "BINGO: Found the target, exiting."
+  DISABLED True
+)
+
+add_test(
+  NAME mutator_no_script_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_basic_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(mutator_no_script_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=unknown"
+  PASS_REGULAR_EXPRESSION "is not accessible"
+)
+
+add_test(
+  NAME crossover_basic_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_basic_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(crossover_basic_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/script_basic.lua"
+)
+
+add_test(
+  NAME crossover_seed_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_seed_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(crossover_seed_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/script_seed.lua"
+)
+
+add_test(
+  NAME crossover_e2e_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_e2e_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(crossover_e2e_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/script_e2e.lua"
+  PASS_REGULAR_EXPRESSION "BINGO: Found the target, exiting."
+  DISABLED True
+)
+
+add_test(
+  NAME crossover_no_script_test
+  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_basic_test
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(crossover_no_script_test PROPERTIES
+  ENVIRONMENT "LIBFUZZER_LUA_SCRIPT=unknown"
+  PASS_REGULAR_EXPRESSION "is not accessible"
+)
blob - /dev/null
blob + aa0cc6ce25dcaa6bc4869da6e2dae1f6a7bf8d85 (mode 644)
--- /dev/null
+++ mutator/tests/crossover_basic_test.c
@@ -0,0 +1,40 @@
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+size_t
+LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1,
+						  const uint8_t *Data2, size_t Size2,
+						  uint8_t *Out, size_t MaxOutSize,
+						  unsigned int Seed);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+static void
+test_basic()
+{
+    uint8_t data[] = { 'L', 'U', 'A' };
+	size_t size = COUNT_OF(data);
+	size_t max_size = size + 1;
+	size_t seed = 100;
+	size_t res = LLVMFuzzerCustomCrossOver(data, size, data, size,
+										   NULL, max_size, seed);
+	assert(res != 0);
+	/* assert(strcmp((char *)data, "luzer") == 0); */
+}
+
+int main(void)
+{
+	test_basic();
+}
blob - /dev/null
blob + 0c3246676c84583417b2e3939acf59e4fb22bfcd (mode 644)
--- /dev/null
+++ mutator/tests/crossover_e2e_test.c
@@ -0,0 +1,21 @@
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
+{
+	assert(Data);
+	char *buf = calloc(Size, sizeof(char *));
+	memcpy(buf, (char *)Data, Size);
+	buf[Size] = '\0';
+	if (strcmp((char *)buf, "A") == 0) {
+		fprintf(stderr, "BINGO: Found the target, exiting.\n");
+		_exit(1);
+	}
+	free(buf);
+	return 0;
+}
blob - /dev/null
blob + 677e86a9cd6f4c6209cb51de1720e0358ccb6f22 (mode 644)
--- /dev/null
+++ mutator/tests/crossover_seed_test.c
@@ -0,0 +1,42 @@
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+
+#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+size_t
+LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1,
+						  const uint8_t *Data2, size_t Size2,
+						  uint8_t *Out, size_t MaxOutSize,
+						  unsigned int Seed);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+static void
+test_seed()
+{
+	time_t t;
+	srand((unsigned) time(&t));
+
+    uint8_t data[] = { 'L', 'U', 'A' };
+	size_t size = COUNT_OF(data);
+	size_t max_size = size;
+	size_t seed = rand();
+	size_t res = LLVMFuzzerCustomCrossOver(data, size, data, size,
+										   NULL, max_size, seed);
+	assert(res != 0);
+}
+
+int
+main(void)
+{
+	test_seed();
+}
blob - /dev/null
blob + b94a884c7deacd4128b697d59d97c00990a67b62 (mode 644)
--- /dev/null
+++ mutator/tests/mutator_basic_test.c
@@ -0,0 +1,39 @@
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+size_t
+LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
+					    size_t MaxSize, unsigned int Seed);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+static void
+test_basic()
+{
+    uint8_t data[] = { 'L', 'U', 'A' };
+	size_t size = COUNT_OF(data);
+	size_t max_size = size + 1;
+	size_t seed = 0;
+	size_t res = LLVMFuzzerCustomMutator(data, size, max_size, seed);
+	assert(res != 0);
+	data[res] = '\0';
+	assert(strcmp((char *)data, "XUA") == 0);
+}
+
+int
+main(void)
+{
+	test_basic();
+}
blob - /dev/null
blob + 0c3246676c84583417b2e3939acf59e4fb22bfcd (mode 644)
--- /dev/null
+++ mutator/tests/mutator_e2e_test.c
@@ -0,0 +1,21 @@
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
+{
+	assert(Data);
+	char *buf = calloc(Size, sizeof(char *));
+	memcpy(buf, (char *)Data, Size);
+	buf[Size] = '\0';
+	if (strcmp((char *)buf, "A") == 0) {
+		fprintf(stderr, "BINGO: Found the target, exiting.\n");
+		_exit(1);
+	}
+	free(buf);
+	return 0;
+}
blob - /dev/null
blob + 90dc199490f4912d502f71f75baa1f3b21c88b8b (mode 644)
--- /dev/null
+++ mutator/tests/mutator_seed_test.c
@@ -0,0 +1,39 @@
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+
+#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+size_t
+LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
+						size_t MaxSize, unsigned int Seed);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+static void
+test_seed()
+{
+	time_t t;
+	srand((unsigned) time(&t));
+
+    uint8_t data[] = { 'L', 'U', 'A' };
+	size_t size = COUNT_OF(data);
+	size_t max_size = size;
+	size_t seed = rand();
+	size_t res = LLVMFuzzerCustomMutator(data, size, max_size, seed);
+	assert(res != 0);
+}
+
+int
+main(void)
+{
+	test_seed();
+}
blob - /dev/null
blob + 5534b285ce376d966905cea86d28413a412c74f7 (mode 644)
--- /dev/null
+++ mutator/tests/script_basic.lua
@@ -0,0 +1,35 @@
+function LLVMFuzzerCustomMutator(data, max_size, seed) -- luacheck: ignore
+    assert(type(data) == "string")
+    assert(data == "LUA")
+
+    assert(type(max_size) == "number")
+    assert(max_size == #data + 1)
+
+    assert(type(seed) == "number")
+    assert(seed ~= nil)
+    assert(seed == 0)
+
+    local b = {}
+    data:gsub(".", function(c) table.insert(b, c) end)
+    b[1] = "X"
+    local buf = table.concat(b, "")
+
+    return buf, #buf
+end
+
+function LLVMFuzzerCustomCrossOver(data1, data2, max_size, seed) -- luacheck: ignore
+    assert(type(data1) == "string")
+    assert(data1 == "LUA")
+
+    assert(type(data2) == "string")
+    assert(data2 == "LUA")
+
+    assert(type(max_size) == "number")
+
+    assert(type(seed) == "number")
+    assert(seed ~= nil)
+
+    local buf = "luzer"
+
+    return buf, #buf
+end
blob - /dev/null
blob + 21f0aa67c0286ba0fcf1d670bb720c25fd321322 (mode 644)
--- /dev/null
+++ mutator/tests/script_e2e.lua
@@ -0,0 +1,7 @@
+function LLVMFuzzerCustomMutator(data, max_size, seed) -- luacheck: ignore
+    return string.rep("A", #data), #data
+end
+
+function LLVMFuzzerCustomCrossOver(data1, data2, max_size, seed) -- luacheck: ignore
+    return "", 0
+end
blob - /dev/null
blob + b80ad15623c729591f0c9a5f34f758a18f870066 (mode 644)
--- /dev/null
+++ mutator/tests/script_seed.lua
@@ -0,0 +1,11 @@
+local set_seed = require("math").randomseed
+
+function LLVMFuzzerCustomMutator(data, max_size, seed) -- luacheck: ignore
+    set_seed(seed)
+    return data .. "xxx", 10
+end
+
+function LLVMFuzzerCustomCrossOver(data1, data2, max_size, seed) -- luacheck: ignore
+    set_seed(seed)
+    return data1 .. "xxx", 10
+end