From 489b9f4f048ffaae6eb352d19537d86127406434 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Sat, 18 Oct 2025 00:02:38 +0300 Subject: [PATCH 1/5] luzer: skip null-termination --- luzer/luzer.c | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/luzer/luzer.c b/luzer/luzer.c index 734e02d8..d6d91f9a 100644 --- a/luzer/luzer.c +++ b/luzer/luzer.c @@ -314,20 +314,12 @@ TestOneInput(const uint8_t* data, size_t size) { lua_State *L = get_global_lua_state(); - char *buf = malloc(size + 1 * sizeof(*buf)); - if (!buf) { - perror("malloc"); - exit(EXIT_FAILURE); - } - memcpy(buf, data, size); - buf[size] = '\0'; - #if defined(LUA_HAS_JIT) && defined(LUAJIT_FRIENDLY_MODE) metrics_enable_luajit_hooks(L); if (jit_status) { if (!luaJIT_setmode(L, 0, LUAJIT_MODE_ON)) luaL_error(L, "cannot turn a JIT compiler on"); - lua_pushlstring(L, buf, size); + lua_pushlstring(L, (const char *)data, size); /* Returned value is not handled. */ luaL_test_one_input(L); if (!luaJIT_setmode(L, 0, LUAJIT_MODE_OFF)) @@ -347,9 +339,8 @@ TestOneInput(const uint8_t* data, size_t size) { */ LUA_SETHOOK(L, debug_hook, LUA_MASKCALL | LUA_MASKLINE, 0); - lua_pushlstring(L, buf, size); + lua_pushlstring(L, (const char *)data, size); int rc = luaL_test_one_input(L); - free(buf); /* Disable debug hook. */ LUA_SETHOOK(L, debug_hook, 0, 0); From ccf129e458fe730d9ee511206ada221b7de9c510 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 3 May 2023 14:36:40 +0300 Subject: [PATCH 2/5] luzer: initial AFL support The patch adds an initial integration with AFL (American Fuzzy Lop). Previously, it was an independent project [1]. A Lua AFL integration using the debug hook functionality which fires as Lua traverses lines, https://gist.github.com/stevenjohnstone/2236f632bb58697311cd01ea1cafbbc6 TODO: - Fix warnings in macros.h 1. https://github.com/ligurio/afl-lua 2. https://team-atlanta.github.io/blog/post-crs-java-libafl-jazzer/ 3. https://github.com/Team-Atlanta/aixcc-afc-atlantis/tree/main/example-crs-webservice/crs-java/crs/fuzzers 4. https://barro.github.io/2018/01/taking-a-look-at-python-afl/ 5. https://lcamtuf.coredump.cx/afl/status_screen.txt 6. https://lcamtuf.coredump.cx/afl/ 7. https://lcamtuf.coredump.cx/afl/technical_details.txt 8. https://aflplus.plus/docs/technical_details/ --- .github/workflows/test.yaml | 2 +- CHANGELOG.md | 1 + LICENSE | 23 +++++ README.md | 43 ++++++-- luzer-scm-1.rockspec | 6 +- luzer/CMakeLists.txt | 14 +++ luzer/afl-lua.c | 196 ++++++++++++++++++++++++++++++++++++ luzer/afl.c | 16 +++ luzer/afl.h | 8 ++ luzer/init.lua | 13 ++- luzer/luzer.c | 33 +++++- luzer/tests/CMakeLists.txt | 51 ++++++++++ 12 files changed, 391 insertions(+), 15 deletions(-) create mode 100644 luzer/afl-lua.c create mode 100644 luzer/afl.c create mode 100644 luzer/afl.h diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0eacbbe0..bb71f51e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,7 +44,7 @@ jobs: run: sudo apt-get remove --purge man-db - name: Setup common packages - run: sudo apt install -y clang-15 libclang-common-15-dev ${{ matrix.PACKAGES }} + run: sudo apt install -y clang-15 libclang-common-15-dev ${{ matrix.PACKAGES }} afl++ - name: Running CMake run: > diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dcb13d5..e0e3c58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support Address and UndefinedBehaviour sanitizers. - Support LuaJIT metrics. - Support OSS Fuzz environment (#73). +- Initial integration with an AFL (American Fuzzy Lop). ### Changed diff --git a/LICENSE b/LICENSE index d78411fa..98d86c34 100644 --- a/LICENSE +++ b/LICENSE @@ -13,3 +13,26 @@ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The MIT License + +Copyright (c) 2020, Steven Johnstone +Copyright (c) 2025, Sergey Bronnikov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 52df8fa4..82641715 100644 --- a/README.md +++ b/README.md @@ -17,27 +17,32 @@ valuable for finding security exploits and vulnerabilities. `luzer` is a coverage-guided Lua fuzzing engine. It supports fuzzing of Lua code, but also C extensions written for Lua. Luzer is based off of -[libFuzzer][libfuzzer-url]. When fuzzing native code, `luzer` can be used in -combination with Address Sanitizer or Undefined Behavior Sanitizer to catch -extra bugs. +[libFuzzer][libfuzzer-url] and [AFL][AFL-url]. When fuzzing native code, +`luzer` can be used in combination with Address Sanitizer or Undefined Behavior +Sanitizer to catch extra bugs. ## Quickstart To use luzer in your own project follow these few simple steps: -1. Setup `luzer` module: +1. Setup `luzer` module and dependencies: ```sh $ luarocks --local install luzer $ eval $(luarocks path) +$ export PATH=$PATH:$(luarocks path --lr-bin). ``` -2. Create a fuzz target invoking your code: +For using AFL engine install `afl++` binary package: `sudo apt install -y +afl++`. + +2. Create a Lua file `example.lua` with a fuzz target invoking your code: ```lua local luzer = require("luzer") local function TestOneInput(buf) + local buf = buf or io.read("*a") local b = {} buf:gsub(".", function(c) table.insert(b, c) end) if b[1] == 'c' then @@ -56,7 +61,22 @@ end luzer.Fuzz(TestOneInput) ``` -3. Start the fuzzer using the fuzz target +Make sure Lua script has failed when string "crash" is passed to `stdin`: + +```sh +$ echo "crash" | luajit example.lua +lua: example.lua:8: assertion failed! +stack traceback: + [C]: in function 'assert' + example.lua:8: in function 'fuzz' + example.lua:14: in main chunk + [C]: in ? +``` + +3. Start the fuzzing test: + +Running a Lua runtime with created Lua file will start fuzzing using libFuzzer +engine: ``` $ luajit examples/example_basic.lua @@ -82,6 +102,14 @@ To gather baseline coverage, the fuzzing engine executes both the seed corpus and the generated corpus, to ensure that no errors occurred and to understand the code coverage the existing corpus already provides. +Alternatively, one can start fuzzing using AFL engine: + +```sh +$ mkdir -p {in,out} +$ echo -n "\0" > in/sample +$ __AFL_SHM_ID=$RANDOM afl-fuzz -D -i in/ -o out/ afl-lua examples/example_basic.lua +``` + See tests that uses luzer library in: - Tarantool Lua API tests, https://github.com/tarantool/tarantool/tree/master/test/fuzz/lua @@ -95,8 +123,9 @@ See [documentation](docs/index.md). ## License Copyright © 2022-2025 [Sergey Bronnikov][bronevichok-url]. - Distributed under the ISC License. +See full Copyright Notice in the LICENSE file. [libfuzzer-url]: https://llvm.org/docs/LibFuzzer.html +[AFL-url]: https://aflplus.plus/ [bronevichok-url]: https://bronevichok.ru/ diff --git a/luzer-scm-1.rockspec b/luzer-scm-1.rockspec index 30c36a45..5726db49 100644 --- a/luzer-scm-1.rockspec +++ b/luzer-scm-1.rockspec @@ -9,8 +9,9 @@ description = { summary = "A coverage-guided, native Lua fuzzer", detailed = [[ luzer is a coverage-guided Lua fuzzing engine. It supports fuzzing of Lua code, but also C extensions written for Lua. Luzer is based off -of libFuzzer. When fuzzing native code, luzer can be used in combination with -Address Sanitizer or Undefined Behavior Sanitizer to catch extra bugs. ]], +of libFuzzer and support integration with AFL. When fuzzing native code, +luzer can be used in combination with Address Sanitizer or Undefined Behavior +Sanitizer to catch extra bugs. ]], homepage = "https://github.com/ligurio/luzer", maintainer = "Sergey Bronnikov ", license = "ISC", @@ -26,6 +27,7 @@ build = { -- https://github.com/luarocks/luarocks/blob/7ed653f010671b3a7245be9adcc70068c049ef68/docs/config_file_format.md#config-file-format -- luacheck: pop variables = { + CMAKE_BINARY_DIR = "$(LUA_DIR)/bin", CMAKE_LUADIR = "$(LUADIR)", CMAKE_LIBDIR = "$(LIBDIR)", CMAKE_BUILD_TYPE = "RelWithDebInfo", diff --git a/luzer/CMakeLists.txt b/luzer/CMakeLists.txt index 110a51b9..226ba36c 100644 --- a/luzer/CMakeLists.txt +++ b/luzer/CMakeLists.txt @@ -50,6 +50,7 @@ set(LUZER_SOURCES luzer.c tracer.c counters.c metrics.c + afl.c ${CMAKE_CURRENT_BINARY_DIR}/config.c) add_library(luzer_impl SHARED ${LUZER_SOURCES}) @@ -82,6 +83,14 @@ target_link_libraries(custom_mutator PRIVATE luzer_impl ) +set(AFL_LUA afl-lua) +add_executable(${AFL_LUA} afl-lua.c) +target_include_directories(${AFL_LUA} PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(${AFL_LUA} PRIVATE ${LUA_LIBRARIES}) +target_compile_options(${AFL_LUA} PRIVATE + ${CFLAGS} +) + if(ENABLE_TESTING) add_subdirectory(tests) endif() @@ -113,3 +122,8 @@ install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/init.lua DESTINATION "${CMAKE_LUADIR}/${PROJECT_NAME}" ) + +install( + TARGETS ${AFL_LUA} + DESTINATION "${CMAKE_BINARY_DIR}/" +) diff --git a/luzer/afl-lua.c b/luzer/afl-lua.c new file mode 100644 index 00000000..6bde04f8 --- /dev/null +++ b/luzer/afl-lua.c @@ -0,0 +1,196 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2020, Steven Johnstone + * Copyright (c) 2025, Sergey Bronnikov + */ + +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" + +#include "afl.h" + +/* + * We will communicate with the AFL forkserver over two pipes with + * file descriptors equal to 198 and 199 (these values are + * hardcoded by AFL). AFL specifies that the 198 pipe is for + * reading data from the forkserver, and 199 is for writing to it. + */ +#define FORKSRV_FD 198 + +/* + * The presence of this string is enough to allow AFL fuzz to run + * without using the environment variable AFL_SKIP_BIN_CHECK. + */ +const char *SHM_ENV = "__AFL_SHM_ID"; +const char *NOFORK = "AFL_NO_FORKSRV"; + +const int afl_read_fd = FORKSRV_FD; +const int afl_write_fd = afl_read_fd + 1; + +static unsigned char *afl_shm; +static size_t afl_shm_size = 1 << 16; + +static int +shm_init(void) { + const char *shm = getenv(SHM_ENV); + if (!shm) { + fprintf(stderr, "afl-lua: env variable %s is not set\n", SHM_ENV); + return -1; + } + afl_shm = shmat(atoi(shm), NULL, 0); + if (afl_shm == (void*) -1) { + perror("shmat"); + fprintf(stderr, "afl-lua: shmat() has failed (%s)\n", strerror(errno)); + return -1; + } + return 0; +} + +static int +fork_write(int pid) { + int buf_sz = 4; + assert(buf_sz == write(afl_write_fd, &pid, buf_sz)); + return 0; +} + +static int +fork_read(void) { + void *buf; + int buf_sz = 4; + assert(buf_sz == read(afl_read_fd, &buf, buf_sz)); + return 0; +} + +static int +fork_close(void) { + close(afl_read_fd); + close(afl_write_fd); + return 0; +} + +/** + * From afl-python + * https://github.com/jwilk/python-afl/blob/8df6bfefac5de78761254bf5d7724e0a52d254f5/afl.pyx#L74-L87 + */ +#define LHASH_INIT 0x811C9DC5 +#define LHASH_MAGIC_MULT 0x01000193 +#define LHASH_NEXT(x) h = ((h ^ (unsigned char)(x)) * LHASH_MAGIC_MULT) + +static inline unsigned int +lhash(const char *key, size_t offset) { + const char *const last = &key[strlen(key) - 1]; + uint32_t h = LHASH_INIT; + while (key <= last) + LHASH_NEXT(*key++); + for (; offset != 0; offset >>= 8) + LHASH_NEXT(offset); + + return h; +} + +static unsigned int current_location; + +static void +debug_hook(lua_State *L, lua_Debug *ar) { + lua_getinfo(L, "Sl", ar); + if (ar && ar->source && ar->currentline) { + const unsigned int new_location = + lhash(ar->source, ar->currentline) % afl_shm_size; + afl_shm[current_location ^ new_location] += 1; + current_location = new_location / 2; + } +} + +int +main(int argc, const char **argv) { + if (argc == 1) { + fprintf(stderr, "afl-lua: missed arguments\n"); + exit(EXIT_FAILURE); + } + + int rc = shm_init(); + if (rc != 0) { + fprintf(stderr, "afl-lua: shm_init() failed\n"); + exit(EXIT_FAILURE); + } + + /* Let luzer library know we're in AFL mode. */ + setenv(AFL_LUA_ENV, "1", 0); + + const char *script_path = argv[1]; + if (access(script_path, F_OK) != 0) { + fprintf(stderr, "afl-lua: file (%s) does not exist\n", script_path); + exit(EXIT_FAILURE); + } + + lua_State *L = luaL_newstate(); + if (L == NULL) { + fprintf(stderr, "afl-lua: Lua initialization failed\n"); + exit(EXIT_FAILURE); + } + + luaL_openlibs(L); + lua_sethook(L, debug_hook, LUA_MASKLINE, 0); + + /* + * "NOFORK" is used to run AFL in persistent mode, which is + * an alternative to the default fork server, allowing the + * fuzzer to run the target program repeatedly in a single + * process without creating a new one each time. + */ + if (getenv(NOFORK)) { + rc = luaL_dofile(L, script_path); + if (rc != 0) { + const char *err_str = lua_tostring(L, 1); + fprintf(stderr, "afl-lua: %s\n", err_str); + lua_pop(L, 1); + exit(EXIT_FAILURE); + } + return EXIT_SUCCESS; + } + + /* Let AFL know we're here. */ + fork_write(0); + + while (1) { + fork_read(); + pid_t child = fork(); + if (child == 0) { + fork_close(); + /* Loads a script that executes `luzer.Fuzz()`. */ + rc = luaL_dofile(L, script_path); + if (rc != 0) { + const char *err_str = lua_tostring(L, 1); + fprintf(stderr, "afl-lua: %s\n", err_str); + lua_pop(L, 1); + abort(); + } + return EXIT_SUCCESS; + } + fork_write(child); + int status = 0; + rc = wait(&status); + if (rc == -1) { + perror("wait"); + abort(); + } + fork_write(status); + } + + return EXIT_SUCCESS; +} diff --git a/luzer/afl.c b/luzer/afl.c new file mode 100644 index 00000000..242b8055 --- /dev/null +++ b/luzer/afl.c @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2025, Sergey Bronnikov + */ + +#include +#include "afl.h" + +int +is_afl_running(void) +{ + if (getenv(AFL_LUA_ENV)) + return 0; + return -1; +} diff --git a/luzer/afl.h b/luzer/afl.h new file mode 100644 index 00000000..63fd74ad --- /dev/null +++ b/luzer/afl.h @@ -0,0 +1,8 @@ +#ifndef LUZER_AFL_LUA_H_ +#define LUZER_AFL_LUA_H_ + +#define AFL_LUA_ENV "AFL_LUA_IS_RUNNING" + +int is_afl_running(void); + +#endif // LUZER_AFL_LUA_H_ diff --git a/luzer/init.lua b/luzer/init.lua index df079e3f..f59671f9 100644 --- a/luzer/init.lua +++ b/luzer/init.lua @@ -68,10 +68,15 @@ local function Fuzz(test_one_input, custom_mutator, func_args) if type(luzer_args) ~= "table" then error("args is not a table") end - local flags = build_flags(arg, luzer_args) - local test_path = arg[0] - local lua_bin = progname(arg) - local test_cmd = ("%s %s"):format(lua_bin, test_path) + local flags = {} + local test_cmd = "" + -- XXX: Replace `os.getenv()` with `luzer_impl._afl_mode`. + if not os.getenv("AFL_LUA_IS_RUNNING") then + flags = build_flags(arg, luzer_args) + local test_path = arg[0] + local lua_bin = progname(arg) + test_cmd = ("%s %s"):format(lua_bin, test_path) + end luzer_impl.Fuzz(test_one_input, custom_mutator, flags, test_cmd) end diff --git a/luzer/luzer.c b/luzer/luzer.c index d6d91f9a..a491c0dd 100644 --- a/luzer/luzer.c +++ b/luzer/luzer.c @@ -30,6 +30,7 @@ #include "tracer.h" #include "config.h" #include "luzer.h" +#include "afl.h" #define TEST_ONE_INPUT_FUNC "luzer_test_one_input" #define CUSTOM_MUTATOR_FUNC "luzer_custom_mutator" @@ -534,8 +535,38 @@ luaL_fuzz(lua_State *L) jit_status = luajit_has_enabled_jit(L); set_global_lua_state(L); - int rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput); + int rc = 0; + if (is_afl_running() == 0) { + /** + * Enable debug hook. + * + * Hook is called when the Lua interpreter calls a function + * and when the interpreter is about to start the execution + * of a new line of code, or when it jumps back in the code + * (even to the same line). + * https://www.lua.org/pil/23.2.html + */ + LUA_SETHOOK(L, debug_hook, LUA_MASKCALL | LUA_MASKLINE, 0); + + /* char *data = calloc(BUFSIZ + 1, sizeof(char)); */ + /* 8192 */ + char buf[BUFSIZ]; + while(fgets(buf, sizeof(buf), stdin) != NULL ) { + /* data = realloc(data, strlen(data) + 1 + strlen(data)); */ + /* if(!buf) */ + /* return 0; */ + /* fprintf(stderr, "%s\n", buf); */ + } + lua_pushlstring(L, buf, BUFSIZ); + rc = luaL_test_one_input(L); + /* free(data); */ + + /* Disable debug hook. */ + LUA_SETHOOK(L, debug_hook, 0, 0); + return rc; + } + rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput); free_argv(argc, argv); luaL_cleanup(L); diff --git a/luzer/tests/CMakeLists.txt b/luzer/tests/CMakeLists.txt index 3be95190..b1103e1c 100644 --- a/luzer/tests/CMakeLists.txt +++ b/luzer/tests/CMakeLists.txt @@ -1,5 +1,7 @@ include(MakeLuaPath) +find_program(AFL_FUZZ_BIN "afl-fuzz") + make_lua_path(LUA_CPATH PATHS ${PROJECT_BINARY_DIR}/luzer/?.so @@ -10,6 +12,18 @@ make_lua_path(LUA_PATH ${PROJECT_SOURCE_DIR}/?/?.lua ${PROJECT_SOURCE_DIR}/?/init.lua ) +set(AFL_LUA_BIN $) + +set(AFL_IN_DIR ${CMAKE_CURRENT_BINARY_DIR}/afl_input_dir) +set(AFL_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/afl_output_dir) +set(AFL_FUZZ_AVAILABLE FALSE) +if(EXISTS ${AFL_FUZZ_BIN}) + set(AFL_FUZZ_AVAILABLE TRUE) + make_directory(${AFL_IN_DIR}) + make_directory(${AFL_OUT_DIR}) + file(WRITE ${AFL_IN_DIR}/sample "0") + file(WRITE ${AFL_OUT_DIR}/sample "0") +endif() add_test( NAME luzer_unit_test @@ -304,3 +318,40 @@ if (LUA_HAS_JIT) "runtime error: load of null pointer of type" ) endif() + +# AFL environment variables, +# see https://aflplus.plus/docs/env_variables/. +# TODO: AFL_NO_FORKSRV +string(JOIN ";" AFL_ENV_VARIABLES + # Enables exit soon after the first crash is found. + AFL_BENCH_UNTIL_CRASH=1 + AFL_DEBUG=1 + AFL_DEBUG_CHILD=1 + # Disables the /proc/sys/kernel/core_pattern check. + AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 + # Disables the TUI as it is not useful in testing. + AFL_NO_UI=1 + # Disables the check for CPU scaling policy. + AFL_SKIP_CPUFREQ=1 +) +set(TEST_ENV "") +list(APPEND TEST_ENV + "LUA_CPATH=${LUA_CPATH}" + "LUA_PATH=${LUA_PATH}" + ${AFL_ENV_VARIABLES} + "__AFL_SHM_ID=${RANDOM_SEED}" +) +add_test( + NAME luzer_e2e_test_afl + COMMAND ${AFL_FUZZ_BIN} -D -i ${AFL_IN_DIR} + -o ${AFL_OUT_DIR} ${AFL_LUA_BIN} + "${CMAKE_CURRENT_SOURCE_DIR}/test_e2e.lua" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(luzer_e2e_test_afl PROPERTIES + ENVIRONMENT "${TEST_ENV}" + PASS_REGULAR_EXPRESSION "assert has triggered" +) +if(${AFL_FUZZ_BIN} STREQUAL "AFL_FUZZ_BIN-NOTFOUND") + set_tests_properties(luzer_e2e_test_afl PROPERTIES DISABLED TRUE) +endif() From b5894dcc5b99ec1cc31a790b8154ccee44e94af7 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Sat, 18 Oct 2025 10:47:20 +0300 Subject: [PATCH 3/5] luzer: luzer_impl._afl_mode --- luzer/afl.c | 4 ++-- luzer/init.lua | 3 +-- luzer/luzer.c | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/luzer/afl.c b/luzer/afl.c index 242b8055..10feac48 100644 --- a/luzer/afl.c +++ b/luzer/afl.c @@ -11,6 +11,6 @@ int is_afl_running(void) { if (getenv(AFL_LUA_ENV)) - return 0; - return -1; + return 1; + return 0; } diff --git a/luzer/init.lua b/luzer/init.lua index f59671f9..4d75ca09 100644 --- a/luzer/init.lua +++ b/luzer/init.lua @@ -70,8 +70,7 @@ local function Fuzz(test_one_input, custom_mutator, func_args) end local flags = {} local test_cmd = "" - -- XXX: Replace `os.getenv()` with `luzer_impl._afl_mode`. - if not os.getenv("AFL_LUA_IS_RUNNING") then + if not luzer_impl._afl_mode then flags = build_flags(arg, luzer_args) local test_path = arg[0] local lua_bin = progname(arg) diff --git a/luzer/luzer.c b/luzer/luzer.c index a491c0dd..4e19deff 100644 --- a/luzer/luzer.c +++ b/luzer/luzer.c @@ -605,6 +605,10 @@ luaopen_luzer_impl(lua_State *L) lua_pushstring(L, LUA_RELEASE); lua_rawset(L, -3); + lua_pushliteral(L, "_afl_mode"); + lua_pushboolean(L, is_afl_running()); + lua_rawset(L, -3); + lua_pushliteral(L, "path"); luaL_path(L); lua_rawset(L, -3); From 15ae1fe7b55bef1911e50a87c015fd3a044a2b2f Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Mon, 20 Oct 2025 12:41:37 +0300 Subject: [PATCH 4/5] afl support [TO SQUASH] --- README.md | 17 +------ docs/api.md | 4 +- docs/usage.md | 14 ++++++ luzer/CMakeLists.txt | 29 ++++++++---- luzer/afl-lua.c | 97 ++++++++++++-------------------------- luzer/afl.h | 1 + luzer/luzer.c | 40 ++++------------ luzer/tests/CMakeLists.txt | 21 +++++++-- luzer/tracer.c | 20 ++++---- luzer/tracer.h | 2 + luzer/tracer_afl.c | 52 ++++++++++++++++++++ luzer/tracer_afl.h | 10 ++++ luzer/tracer_libfuzzer.c | 11 +++++ luzer/tracer_libfuzzer.h | 9 ++++ 14 files changed, 195 insertions(+), 132 deletions(-) create mode 100644 luzer/tracer_afl.c create mode 100644 luzer/tracer_afl.h create mode 100644 luzer/tracer_libfuzzer.c create mode 100644 luzer/tracer_libfuzzer.h diff --git a/README.md b/README.md index 82641715..844304d1 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ $ eval $(luarocks path) $ export PATH=$PATH:$(luarocks path --lr-bin). ``` -For using AFL engine install `afl++` binary package: `sudo apt install -y -afl++`. +For using AFL engine install `afl++` binary package: +`sudo apt install -y afl++`. 2. Create a Lua file `example.lua` with a fuzz target invoking your code: @@ -42,7 +42,6 @@ afl++`. local luzer = require("luzer") local function TestOneInput(buf) - local buf = buf or io.read("*a") local b = {} buf:gsub(".", function(c) table.insert(b, c) end) if b[1] == 'c' then @@ -61,18 +60,6 @@ end luzer.Fuzz(TestOneInput) ``` -Make sure Lua script has failed when string "crash" is passed to `stdin`: - -```sh -$ echo "crash" | luajit example.lua -lua: example.lua:8: assertion failed! -stack traceback: - [C]: in function 'assert' - example.lua:8: in function 'fuzz' - example.lua:14: in main chunk - [C]: in ? -``` - 3. Start the fuzzing test: Running a Lua runtime with created Lua file will start fuzzing using libFuzzer diff --git a/docs/api.md b/docs/api.md index fff7ad81..0098f2da 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,10 +14,12 @@ Function accepts following arguments: invoked with a single string container. - `custom_mutator` (optional) defines a custom mutator function (equivalent to `LLVMFuzzerCustomMutator`). Default is `nil`. + Note, the custom mutator is not supported when AFL engine is used. - `args` (optional) is a table with arguments: the process arguments to pass to the fuzzer. Field `corpus` specifies a path to a directory with seed corpus, see a list with other options in the [libFuzzer documentation][libfuzzer-options-url]. - Default is an empty table. + Default is an empty table. Note, arguments specified in the + `args` table are ignored when AFL engine is used. It may be desirable to reject some inputs, i.e. to not add them to the corpus. For example, when fuzzing an API consisting of parsing and other logic, one may diff --git a/docs/usage.md b/docs/usage.md index f92c9e3d..342d800c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,5 +1,17 @@ ## Usage +### Fuzzing engines + +The luzer library supports two engines: [libFuzzer][libfuzzer-url] +and [AFL][AFL-url]. A single [API](api.md) is used for both +engines. However, integration with AFL has some limitations: +function with custom mutator specified in the `luzer.Fuzz()` +is ignored and arguments specified in the `args` table and +command-line are ignored. + +Thanks to the tight integration and a single API, the same test +can be used with both engines without modifications. + ### Fuzzing targets In general, `luzer` has an ability to write fuzzing tests for Lua functions. @@ -234,3 +246,5 @@ in the section [LuaJIT Metrics](#luajit-metrics). [atheris-native-extensions]: https://github.com/google/atheris/blob/master/native_extension_fuzzing.md [atheris-native-extensions-video]: https://www.youtube.com/watch?v=oM-7lt43-GA [luacov-website]: https://lunarmodules.github.io/luacov/ +[libfuzzer-url]: https://llvm.org/docs/LibFuzzer.html +[AFL-url]: https://aflplus.plus/ diff --git a/luzer/CMakeLists.txt b/luzer/CMakeLists.txt index 226ba36c..e39875ef 100644 --- a/luzer/CMakeLists.txt +++ b/luzer/CMakeLists.txt @@ -44,14 +44,17 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_options(-D_FORTIFY_SOURCE=2) endif() -set(LUZER_SOURCES luzer.c - compat.c - fuzzed_data_provider.cc - tracer.c - counters.c - metrics.c - afl.c - ${CMAKE_CURRENT_BINARY_DIR}/config.c) +set(LUZER_SOURCES + ${CMAKE_CURRENT_BINARY_DIR}/config.c + afl.c + compat.c + counters.c + fuzzed_data_provider.cc + luzer.c + tracer_afl.c + tracer.c + tracer_libfuzzer.c +) add_library(luzer_impl SHARED ${LUZER_SOURCES}) target_include_directories(luzer_impl PRIVATE @@ -84,12 +87,20 @@ target_link_libraries(custom_mutator PRIVATE ) set(AFL_LUA afl-lua) -add_executable(${AFL_LUA} afl-lua.c) +add_executable(${AFL_LUA} + afl-lua.c + afl.c + tracer.c + tracer_afl.c +) target_include_directories(${AFL_LUA} PRIVATE ${LUA_INCLUDE_DIR}) target_link_libraries(${AFL_LUA} PRIVATE ${LUA_LIBRARIES}) target_compile_options(${AFL_LUA} PRIVATE ${CFLAGS} ) +target_compile_definitions(${AFL_LUA} PRIVATE + DISABLE_TRACE_LIBFUZZER=1 +) if(ENABLE_TESTING) add_subdirectory(tests) diff --git a/luzer/afl-lua.c b/luzer/afl-lua.c index 6bde04f8..2ac275f7 100644 --- a/luzer/afl-lua.c +++ b/luzer/afl-lua.c @@ -5,13 +5,10 @@ * Copyright (c) 2025, Sergey Bronnikov */ -#undef NDEBUG -#include #include #include #include #include -#include #include #include @@ -23,6 +20,8 @@ #include "lauxlib.h" #include "afl.h" +#include "tracer.h" +#include "tracer_afl.h" /* * We will communicate with the AFL forkserver over two pipes with @@ -36,44 +35,29 @@ * The presence of this string is enough to allow AFL fuzz to run * without using the environment variable AFL_SKIP_BIN_CHECK. */ -const char *SHM_ENV = "__AFL_SHM_ID"; -const char *NOFORK = "AFL_NO_FORKSRV"; +static const char *SHM_ENV = "__AFL_SHM_ID"; +static const char *NOFORK = "AFL_NO_FORKSRV"; -const int afl_read_fd = FORKSRV_FD; -const int afl_write_fd = afl_read_fd + 1; +static const int afl_read_fd = FORKSRV_FD; +static const int afl_write_fd = afl_read_fd + 1; -static unsigned char *afl_shm; -static size_t afl_shm_size = 1 << 16; - -static int -shm_init(void) { - const char *shm = getenv(SHM_ENV); - if (!shm) { - fprintf(stderr, "afl-lua: env variable %s is not set\n", SHM_ENV); - return -1; - } - afl_shm = shmat(atoi(shm), NULL, 0); - if (afl_shm == (void*) -1) { - perror("shmat"); - fprintf(stderr, "afl-lua: shmat() has failed (%s)\n", strerror(errno)); - return -1; - } - return 0; -} - -static int +static void fork_write(int pid) { int buf_sz = 4; - assert(buf_sz == write(afl_write_fd, &pid, buf_sz)); - return 0; + if (buf_sz != write(afl_write_fd, &pid, buf_sz)) { + perror("write"); + abort(); + } } -static int +static void fork_read(void) { void *buf; int buf_sz = 4; - assert(buf_sz == read(afl_read_fd, &buf, buf_sz)); - return 0; + if (buf_sz != read(afl_read_fd, &buf, buf_sz)) { + perror("read"); + abort(); + } } static int @@ -83,39 +67,6 @@ fork_close(void) { return 0; } -/** - * From afl-python - * https://github.com/jwilk/python-afl/blob/8df6bfefac5de78761254bf5d7724e0a52d254f5/afl.pyx#L74-L87 - */ -#define LHASH_INIT 0x811C9DC5 -#define LHASH_MAGIC_MULT 0x01000193 -#define LHASH_NEXT(x) h = ((h ^ (unsigned char)(x)) * LHASH_MAGIC_MULT) - -static inline unsigned int -lhash(const char *key, size_t offset) { - const char *const last = &key[strlen(key) - 1]; - uint32_t h = LHASH_INIT; - while (key <= last) - LHASH_NEXT(*key++); - for (; offset != 0; offset >>= 8) - LHASH_NEXT(offset); - - return h; -} - -static unsigned int current_location; - -static void -debug_hook(lua_State *L, lua_Debug *ar) { - lua_getinfo(L, "Sl", ar); - if (ar && ar->source && ar->currentline) { - const unsigned int new_location = - lhash(ar->source, ar->currentline) % afl_shm_size; - afl_shm[current_location ^ new_location] += 1; - current_location = new_location / 2; - } -} - int main(int argc, const char **argv) { if (argc == 1) { @@ -123,7 +74,7 @@ main(int argc, const char **argv) { exit(EXIT_FAILURE); } - int rc = shm_init(); + int rc = shm_init(SHM_ENV); if (rc != 0) { fprintf(stderr, "afl-lua: shm_init() failed\n"); exit(EXIT_FAILURE); @@ -161,6 +112,7 @@ main(int argc, const char **argv) { lua_pop(L, 1); exit(EXIT_FAILURE); } + shm_deinit(); return EXIT_SUCCESS; } @@ -178,6 +130,11 @@ main(int argc, const char **argv) { const char *err_str = lua_tostring(L, 1); fprintf(stderr, "afl-lua: %s\n", err_str); lua_pop(L, 1); + /* + * AFL detects a crash by recognizing that a + * program terminates due to a signal, such as + * SIGSEGV (segmentation fault) or SIGABRT (abort). + */ abort(); } return EXIT_SUCCESS; @@ -187,10 +144,18 @@ main(int argc, const char **argv) { rc = wait(&status); if (rc == -1) { perror("wait"); + /* + * AFL detects a crash by recognizing that a + * program terminates due to a signal, such as + * SIGSEGV (segmentation fault) or SIGABRT (abort). + */ abort(); } fork_write(status); } + lua_sethook(L, debug_hook, 0, 0); + lua_close(L); + shm_deinit(); return EXIT_SUCCESS; } diff --git a/luzer/afl.h b/luzer/afl.h index 63fd74ad..b05a2329 100644 --- a/luzer/afl.h +++ b/luzer/afl.h @@ -2,6 +2,7 @@ #define LUZER_AFL_LUA_H_ #define AFL_LUA_ENV "AFL_LUA_IS_RUNNING" +#define AFL_LUA_MAXINPUT 512 int is_afl_running(void); diff --git a/luzer/luzer.c b/luzer/luzer.c index 4e19deff..a2be95e9 100644 --- a/luzer/luzer.c +++ b/luzer/luzer.c @@ -304,6 +304,7 @@ teardown(void) NO_SANITIZE int TestOneInput(const uint8_t* data, size_t size) { + /* XXX: Use is_afl_running(). */ const counter_and_pc_table_range alloc = allocate_counters_and_pcs(); if (alloc.counters_start && alloc.counters_end) { __sanitizer_cov_8bit_counters_init(alloc.counters_start, @@ -536,37 +537,16 @@ luaL_fuzz(lua_State *L) jit_status = luajit_has_enabled_jit(L); set_global_lua_state(L); int rc = 0; - if (is_afl_running() == 0) { - /** - * Enable debug hook. - * - * Hook is called when the Lua interpreter calls a function - * and when the interpreter is about to start the execution - * of a new line of code, or when it jumps back in the code - * (even to the same line). - * https://www.lua.org/pil/23.2.html - */ - LUA_SETHOOK(L, debug_hook, LUA_MASKCALL | LUA_MASKLINE, 0); - - /* char *data = calloc(BUFSIZ + 1, sizeof(char)); */ - /* 8192 */ - char buf[BUFSIZ]; - while(fgets(buf, sizeof(buf), stdin) != NULL ) { - /* data = realloc(data, strlen(data) + 1 + strlen(data)); */ - /* if(!buf) */ - /* return 0; */ - /* fprintf(stderr, "%s\n", buf); */ - } - - lua_pushlstring(L, buf, BUFSIZ); - rc = luaL_test_one_input(L); - /* free(data); */ - - /* Disable debug hook. */ - LUA_SETHOOK(L, debug_hook, 0, 0); - return rc; + if (is_afl_running()) { + /* AFL writes generated data to stdin (standard input). */ + char buf[AFL_LUA_MAXINPUT]; + const char *res = fgets(buf, AFL_LUA_MAXINPUT, stdin); + if (!res) + return 0; + rc = TestOneInput((const uint8_t *)&buf, AFL_LUA_MAXINPUT); + } else { + rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput); } - rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput); free_argv(argc, argv); luaL_cleanup(L); diff --git a/luzer/tests/CMakeLists.txt b/luzer/tests/CMakeLists.txt index b1103e1c..721fbba0 100644 --- a/luzer/tests/CMakeLists.txt +++ b/luzer/tests/CMakeLists.txt @@ -344,14 +344,29 @@ list(APPEND TEST_ENV add_test( NAME luzer_e2e_test_afl COMMAND ${AFL_FUZZ_BIN} -D -i ${AFL_IN_DIR} - -o ${AFL_OUT_DIR} ${AFL_LUA_BIN} - "${CMAKE_CURRENT_SOURCE_DIR}/test_e2e.lua" + -o ${AFL_OUT_DIR} ${AFL_LUA_BIN} + "${CMAKE_CURRENT_SOURCE_DIR}/test_e2e.lua" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties(luzer_e2e_test_afl PROPERTIES ENVIRONMENT "${TEST_ENV}" PASS_REGULAR_EXPRESSION "assert has triggered" ) +add_test( + NAME luzer_e2e_test_afl_nofork + COMMAND ${AFL_FUZZ_BIN} -D -i ${AFL_IN_DIR} + -o ${AFL_OUT_DIR} ${AFL_LUA_BIN} + "${CMAKE_CURRENT_SOURCE_DIR}/test_e2e.lua" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(luzer_e2e_test_afl_nofork PROPERTIES + ENVIRONMENT "${TEST_ENV};NOFORK=1" + PASS_REGULAR_EXPRESSION "assert has triggered" +) if(${AFL_FUZZ_BIN} STREQUAL "AFL_FUZZ_BIN-NOTFOUND") - set_tests_properties(luzer_e2e_test_afl PROPERTIES DISABLED TRUE) + set_tests_properties( + luzer_e2e_test_afl + luzer_e2e_test_afl_nofork + PROPERTIES DISABLED TRUE + ) endif() diff --git a/luzer/tracer.c b/luzer/tracer.c index e72a0aa6..e8f68579 100644 --- a/luzer/tracer.c +++ b/luzer/tracer.c @@ -24,8 +24,12 @@ #include #include /* strlen */ -#include "counters.h" #include "macros.h" +#include "afl.h" +#ifndef DISABLE_TRACE_LIBFUZZER +#include "tracer_libfuzzer.h" +#endif /* DISABLE_TRACE_LIBFUZZER */ +#include "tracer_afl.h" /** * From afl-python @@ -35,12 +39,6 @@ #define LHASH_MAGIC_MULT 0x01000193 #define LHASH_NEXT(x) h = ((h ^ (unsigned char)(x)) * LHASH_MAGIC_MULT) -NO_SANITIZE void -_trace_branch(uint64_t idx) -{ - increment_counter(idx); -} - NO_SANITIZE static inline unsigned int lhash(const char *key, size_t offset) { @@ -68,6 +66,12 @@ debug_hook(lua_State *L, lua_Debug *ar) lua_getinfo(L, "Sln", ar); if (ar && ar->source && ar->currentline) { const unsigned int new_location = lhash(ar->source, ar->currentline); - _trace_branch(new_location); + if (is_afl_running()) { + trace_afl(new_location); + } else { +#ifndef DISABLE_TRACE_LIBFUZZER + trace_libfuzzer(new_location); +#endif /* DISABLE_TRACE_LIBFUZZER */ + } } } diff --git a/luzer/tracer.h b/luzer/tracer.h index 232d4a3b..9f7fa765 100644 --- a/luzer/tracer.h +++ b/luzer/tracer.h @@ -1,6 +1,8 @@ #ifndef LUZER_TRACER_H_ #define LUZER_TRACER_H_ +#include "lua.h" + void debug_hook(lua_State *L, lua_Debug *ar); #endif // LUZER_TRACER_H_ diff --git a/luzer/tracer_afl.c b/luzer/tracer_afl.c new file mode 100644 index 00000000..319e235f --- /dev/null +++ b/luzer/tracer_afl.c @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2020, Steven Johnstone + * Copyright (c) 2025, Sergey Bronnikov + */ + +#include +#include +#include +#include +#include + +#include "afl.h" +#include "macros.h" + +unsigned char *afl_shm; +size_t afl_shm_size = 1 << 16; + +static unsigned int current_location; + +NO_SANITIZE int +shm_init(const char *shm_env) { + const char *shm = getenv(shm_env); + if (!shm) { + fprintf(stderr, "afl-lua: env variable %s is not set\n", shm_env); + return -1; + } + afl_shm = shmat(atoi(shm), NULL, 0); + if (afl_shm == (void *) -1) { + perror("shmat"); + fprintf(stderr, "afl-lua: shmat() has failed (%s)\n", strerror(errno)); + return -1; + } + return 0; +} + +NO_SANITIZE int +shm_deinit(void) { + int rc = shmdt(afl_shm); + if (rc != 0) { + perror("shmdt"); + } + return rc; +} + +NO_SANITIZE void +trace_afl(const unsigned int new_location) { + const unsigned int new_loc = new_location % afl_shm_size; + afl_shm[current_location ^ new_loc] += 1; + current_location = new_loc / 2; +} diff --git a/luzer/tracer_afl.h b/luzer/tracer_afl.h new file mode 100644 index 00000000..f7f9ad87 --- /dev/null +++ b/luzer/tracer_afl.h @@ -0,0 +1,10 @@ +#ifndef LUZER_TRACER_AFL_H_ +#define LUZER_TRACER_AFL_H_ + +#include "macros.h" + +NO_SANITIZE int shm_init(const char *shm_env); +NO_SANITIZE int shm_deinit(void); +NO_SANITIZE void trace_afl(const unsigned int new_location); + +#endif // LUZER_TRACER_AFL_H_ diff --git a/luzer/tracer_libfuzzer.c b/luzer/tracer_libfuzzer.c new file mode 100644 index 00000000..9c96ca28 --- /dev/null +++ b/luzer/tracer_libfuzzer.c @@ -0,0 +1,11 @@ +#include +#include + +#include "counters.h" +#include "macros.h" + +NO_SANITIZE void +trace_libfuzzer(uint64_t idx) +{ + increment_counter(idx); +} diff --git a/luzer/tracer_libfuzzer.h b/luzer/tracer_libfuzzer.h new file mode 100644 index 00000000..96e8daa0 --- /dev/null +++ b/luzer/tracer_libfuzzer.h @@ -0,0 +1,9 @@ +#ifndef LUZER_TRACER_LIBFUZZER_H_ +#define LUZER_TRACER_LIBFUZZER_H_ + +#include +#include "macros.h" + +NO_SANITIZE void trace_libfuzzer(uint64_t idx); + +#endif // LUZER_TRACER_LIBFUZZER_H_ From 510cdd1423c786b5d23eb9845af45e775391861b Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Mon, 20 Oct 2025 13:55:41 +0300 Subject: [PATCH 5/5] luzer: comment-out warnings [FIXME] --- luzer/macros.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/luzer/macros.h b/luzer/macros.h index f8bae1e8..59266371 100644 --- a/luzer/macros.h +++ b/luzer/macros.h @@ -57,10 +57,10 @@ #if __has_feature(coverage_sanitizer) #define NO_SANITIZE_COVERAGE __attribute__((no_sanitize("coverage"))) #else // __has_feature(coverage_sanitizer) -#warning "compiler does not support 'coverage_sanitizer' feature" -#warning "it still may have instrumentation, but no way to exclude -#warning "certain functions found" -#warning "if you proceed, your coverage may be polluted or broken" +/* #warning "compiler does not support 'coverage_sanitizer' feature" */ +/* #warning "it still may have instrumentation, but no way to exclude */ +/* #warning "certain functions found" */ +/* #warning "if you proceed, your coverage may be polluted or broken" */ #define NO_SANITIZE_COVERAGE #endif // __has_feature(coverage_sanitizer)