commit 2ec5436e90f16c2a59636119dfa46556fcae4c1c from: Sergey Bronnikov date: Fri Aug 09 07:39:53 2024 UTC libluamut: initial version The patch adds a shared library that implements a custom mutation and crossover functions for LibFuzzer. These functions allows to implement mutation and crossover logic with Lua programming language and thus replace the default mutation and crossover functions: `LLVMFuzzerCustomMutator` and `LLVMFuzzerCustomCrossover`. For implementing custom mutation function in Lua one need to create a Lua script with function `LLVMFuzzerCustomMutator` and set a path to the script in environment variable with name `LIBFUZZER_LUA_SCRIPT`. When this environment variable is not set default script name `libfuzzer_lua_script.lua` will be used. The same with custom crossover function - one need create a Lua script with defined Lua function `LLVMFuzzerCustomCrossover` and set a path to the script in environment variable `LIBFUZZER_LUA_SCRIPT`. Pay attention that both functions uses its own Lua state internally. Note, `libluamut` is unused now and building is disabled by default. Follows up #19 commit - 5ba54aa0b94bf4491a5e92872ff8e57458f38270 commit + 2ec5436e90f16c2a59636119dfa46556fcae4c1c blob - b2814cfdaa625db0fd5fab1dd629c8e5ac8df175 blob + b31d8b458fd75313e13770a17a3803989008f0d4 --- CMakeLists.txt +++ CMakeLists.txt @@ -80,4 +80,5 @@ endif() enable_testing() add_subdirectory(extra) +add_subdirectory(libluamut) add_subdirectory(tests) blob - /dev/null blob + cfd54a6ca3bd3bac958432ecfd56d6e2efad05f7 (mode 644) --- /dev/null +++ libluamut/CMakeLists.txt @@ -0,0 +1,24 @@ +set(CFLAGS -Wall -Wextra -Wpedantic -Wno-unused-parameter) + +if (ENABLE_COV) + set(CFLAGS ${CFLAGS} -fprofile-instr-generate -fprofile-arcs + -fcoverage-mapping -ftest-coverage) + set(LDFLAGS ${LDFLAGS} -fprofile-instr-generate -fprofile-arcs + -fcoverage-mapping -ftest-coverage) +endif (ENABLE_COV) + +set(LIB_LUA_MUTATE lua_mutate) +add_library(${LIB_LUA_MUTATE} STATIC mutate.c) +target_link_libraries(${LIB_LUA_MUTATE} PRIVATE ${LUA_LIBRARIES} ${LDFLAGS}) +target_include_directories(${LIB_LUA_MUTATE} PRIVATE ${LUA_INCLUDE_DIR}) +target_compile_options(${LIB_LUA_MUTATE} PRIVATE ${CFLAGS}) +add_dependencies(${LIB_LUA_MUTATE} ${LUA_TARGET}) + +set(LIB_LUA_CROSSOVER lua_crossover) +add_library(${LIB_LUA_CROSSOVER} STATIC crossover.c) +target_link_libraries(${LIB_LUA_CROSSOVER} PRIVATE ${LUA_LIBRARIES} ${LDFLAGS}) +target_include_directories(${LIB_LUA_CROSSOVER} PRIVATE ${LUA_INCLUDE_DIR}) +target_compile_options(${LIB_LUA_CROSSOVER} PRIVATE ${CFLAGS}) +add_dependencies(${LIB_LUA_CROSSOVER} ${LUA_TARGET}) + +add_subdirectory(tests) blob - /dev/null blob + 01f3787a24f393feb339642938921c3b4f70df19 (mode 644) --- /dev/null +++ libluamut/README.md @@ -0,0 +1,20 @@ +### libluamut + +is two shared libraries that allows using custom mutation and +crossover functions written in Lua programming language in +LibFuzzer. When defined these Lua functions will be executed +instead default LibFuzzer functions `LLVMFuzzerCustomMutator` and +`LLVMFuzzerCustomCrossover`. + +For implementing a custom mutation function in Lua one need to +create a Lua script with a function `LLVMFuzzerCustomMutator` and +set a path to the script in an environment variable with name +`LIBFUZZER_LUA_SCRIPT`. When this environment variable is not set +default script name `libfuzzer_lua_script.lua` will be used. +The same with custom crossover function - one need create +a Lua script with defined Lua function `LLVMFuzzerCustomCrossover` +and set a path to the script in environment variable +`LIBFUZZER_LUA_SCRIPT`. + +Pay attention that both functions uses its own Lua state +internally. blob - /dev/null blob + 70dd9736b29312418e0fe22da367df1c9c7ed7fa (mode 644) --- /dev/null +++ libluamut/crossover.c @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright 2022-2024, Sergey Bronnikov + */ + +#include +#include +#include +#include +#include + +#include "lua.h" +#include "lauxlib.h" +#include "lualib.h" + +static const char *script_default = "./libfuzzer_lua_script.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); + } + lua_pop(L, 1); + + return ret_size; +} + +/* + * The libFuzzer can specify a "Custom Crossover" function for combining two + * inputs from the corpus. This function is sometimes called by libFuzzer + * when mutating inputs. + * + * data1: location of first input + * size1: length of first input + * data1: location of second input + * size1: length of second input + * out: where to place the resulting, mutated input + * max_out_size: the maximum length of the input that can be placed in out + * seed: the seed that should be used to make mutations deterministic, when + * needed + * + * See libfuzzer's LLVMFuzzerCustomCrossOver API for more info. + * + * Can be NULL. + */ +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) ? 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 + 0ad05752d6a92356f69e75715e30c7fd9980b245 (mode 644) --- /dev/null +++ libluamut/mutate.c @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright 2022-2024, Sergey Bronnikov + */ + +#include +#include +#include +#include +#include + +#include "lua.h" +#include "lauxlib.h" +#include "lualib.h" + +static const char *script_default = "./libfuzzer_lua_script.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) ? 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); + + if (getenv("") && ret_size != 0) { + fprintf(stderr, "-------------------------"); + fprintf(stderr, "%s\n", Data); + } + + lua_close(L); + + return ret_size; +} blob - /dev/null blob + 5a91dca379f8a19f27707a89fd2572b1719b73d6 (mode 644) --- /dev/null +++ libluamut/tests/CMakeLists.txt @@ -0,0 +1,117 @@ +set(ENV_NAME_PATH "LIBFUZZER_LUA_SCRIPT") + +add_executable(mutator_basic_test mutator_basic_test.c) +target_include_directories(mutator_basic_test PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(mutator_basic_test PRIVATE ${LUA_LIBRARIES} + ${LDFLAGS} + ${LIB_LUA_MUTATE}) +target_compile_options(mutator_basic_test PRIVATE ${CFLAGS}) +add_test( + NAME libluamut_mutator_basic_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_basic_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_mutator_basic_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=${CMAKE_CURRENT_SOURCE_DIR}/script_basic.lua" + LABELS internal +) +add_test( + NAME libluamut_mutator_no_script_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_basic_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_mutator_no_script_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=unknown" + PASS_REGULAR_EXPRESSION "is not accessible" + LABELS internal +) + +add_executable(mutator_seed_test mutator_seed_test.c) +target_include_directories(mutator_seed_test PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(mutator_seed_test PRIVATE ${LUA_LIBRARIES} + ${LDFLAGS} + ${LIB_LUA_MUTATE}) +target_compile_options(mutator_seed_test PRIVATE ${CFLAGS}) +add_test( + NAME libluamut_mutator_seed_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_seed_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_mutator_seed_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=${CMAKE_CURRENT_SOURCE_DIR}/script_seed.lua" + LABELS internal +) + +add_executable(mutator_e2e_test mutator_e2e_test.c) +target_include_directories(mutator_e2e_test PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(mutator_e2e_test PRIVATE ${LUA_LIBRARIES} + ${LDFLAGS} -fsanitize=fuzzer + ${LIB_LUA_MUTATE}) +target_compile_options(mutator_e2e_test PRIVATE ${CFLAGS} -fsanitize=fuzzer) +add_test( + NAME libluamut_mutator_e2e_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mutator_e2e_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_mutator_e2e_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=${CMAKE_CURRENT_SOURCE_DIR}/script_e2e.lua" + PASS_REGULAR_EXPRESSION "BINGO: Found the target, exiting." + LABELS internal +) + +add_executable(crossover_basic_test crossover_basic_test.c) +target_include_directories(crossover_basic_test PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(crossover_basic_test PRIVATE + ${LUA_LIBRARIES} ${LDFLAGS} lua_crossover) +target_compile_options(crossover_basic_test PRIVATE ${CFLAGS}) +add_test( + NAME libluamut_crossover_basic_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_basic_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_crossover_basic_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=${CMAKE_CURRENT_SOURCE_DIR}/script_basic.lua" + LABELS internal +) +add_test( + NAME libluamut_crossover_no_script_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_basic_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_crossover_no_script_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=unknown" + PASS_REGULAR_EXPRESSION "is not accessible" + LABELS internal +) + +add_executable(crossover_seed_test crossover_seed_test.c) +target_include_directories(crossover_seed_test PRIVATE + ${LUA_INCLUDE_DIR}) +target_link_libraries(crossover_seed_test PRIVATE + ${LUA_LIBRARIES} ${LDFLAGS} lua_crossover) +target_compile_options(crossover_seed_test PRIVATE ${CFLAGS}) +add_test( + NAME libluamut_crossover_seed_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_seed_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_crossover_seed_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=${CMAKE_CURRENT_SOURCE_DIR}/script_seed.lua" + LABELS internal +) + +add_executable(crossover_e2e_test crossover_e2e_test.c) +target_include_directories(crossover_e2e_test PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(crossover_e2e_test PRIVATE + ${LUA_LIBRARIES} ${LDFLAGS} -fsanitize=fuzzer lua_crossover) +target_compile_options(crossover_e2e_test PRIVATE ${CFLAGS} -fsanitize=fuzzer) +add_test( + NAME libluamut_crossover_e2e_test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/crossover_e2e_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(libluamut_crossover_e2e_test PROPERTIES + ENVIRONMENT "${ENV_NAME_PATH}=${CMAKE_CURRENT_SOURCE_DIR}/script_e2e.lua" + PASS_REGULAR_EXPRESSION "BINGO: Found the target, exiting." + LABELS internal +) blob - /dev/null blob + 51dad72701a0b3308fd132554207c6beb376ca68 (mode 644) --- /dev/null +++ libluamut/tests/crossover_basic_test.c @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include +#include + +#ifndef lengthof +# define lengthof(array) (sizeof (array) / sizeof ((array)[0])) +#endif + +#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(void); + +static void +test_basic(void) +{ + uint8_t data[] = { 'L', 'U', 'A' }; + size_t size = lengthof(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(memcmp((char *)data, "LUA", size) == 0); +} + +int +main(void) +{ + test_basic(); +} blob - /dev/null blob + 3a13c165b5abfd90c17387732e19b9e45c883958 (mode 644) --- /dev/null +++ libluamut/tests/crossover_e2e_test.c @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include +#include + +int +LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + assert(Data); + char *buf = malloc(Size + 1); + assert(buf); + 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 + 134c44bbafd5d3b57f67828f028b1c6bf486d167 (mode 644) --- /dev/null +++ libluamut/tests/crossover_seed_test.c @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +#ifndef lengthof +# define lengthof(array) (sizeof (array) / sizeof ((array)[0])) +#endif + +#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(void) +{ + time_t t; + srand((unsigned) time(&t)); + + uint8_t data[] = { 'L', 'U', 'A' }; + size_t size = lengthof(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 + 559220aab3c00458a3c9348f868707ed34d5f3cc (mode 644) --- /dev/null +++ libluamut/tests/mutator_basic_test.c @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include + +#ifndef lengthof +# define lengthof(array) (sizeof (array) / sizeof ((array)[0])) +#endif + +#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(void) +{ + uint8_t data[] = { 'L', 'U', 'A' }; + size_t size = lengthof(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 + 3a13c165b5abfd90c17387732e19b9e45c883958 (mode 644) --- /dev/null +++ libluamut/tests/mutator_e2e_test.c @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include +#include + +int +LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) +{ + assert(Data); + char *buf = malloc(Size + 1); + assert(buf); + 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 + 0a1d31271efd3e6f6b236fca2ce3d44566c133aa (mode 644) --- /dev/null +++ libluamut/tests/mutator_seed_test.c @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include + +#ifndef lengthof +# define lengthof(array) (sizeof (array) / sizeof ((array)[0])) +#endif + +#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(void) +{ + time_t t; + srand((unsigned) time(&t)); + + uint8_t data[] = { 'L', 'U', 'A' }; + size_t size = lengthof(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 +++ libluamut/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 +++ libluamut/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 +++ libluamut/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