commit 847dd20811eb947fa67fb6e3c076f815f80d1a51 from: Sergey Bronnikov via: Sergey Bronnikov date: Wed Feb 08 15:52:59 2023 UTC mutator: add an initial version 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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include + +#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