From 27a1add4be1abfbf738882c0bb22573f71b04fac Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 15 Oct 2025 18:56:52 +0300 Subject: [PATCH 1/2] ci: disable processing triggers for man-db --- .github/workflows/test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ad59ac8..0eacbbe 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,6 +40,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Disable processing triggers for man-db + 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 }} From 7095cefba1f9b33edb79248321e5000a269e2b41 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 3 May 2023 14:36:40 +0300 Subject: [PATCH 2/2] 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 timeouted tests in CI - Add regression test with NOFORK - Share common `debug_hook()` - Fix passing a `buf` to `TestOneInput` - Clang is not required, build only `afl-lua` if Clang is not available. - Update documentation 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 The problem with Release build: LUA_CPATH="/home/sergeyb/sources/luzer/build/luzer/?.so;;" LUA_PATH="/home/sergeyb/sources/luzer/?/?.lua;/home/sergeyb/sources/luzer/?/init.lua;;" AFL_BENCH_UNTIL_CRASH=1 AFL_DEBUG=1 AFL_DEBUG_CHILD=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 AFL_NO_UI=1 AFL_SKIP_CPUFREQ=1 __AFL_SHM_ID=214691379 /bin/afl-fuzz "-D" "-i" "/home/sergeyb/sources/luzer/build/luzer/tests/afl_input_dir" "-o" "/home/sergeyb/sources/luzer/build/luzer/tests/afl_output_dir" "/home/sergeyb/sources/luzer/build/luzer/afl-lua" "/home/sergeyb/sources/luzer/luzer/tests/test_e2e.lua" --- .github/workflows/test.yaml | 2 +- CHANGELOG.md | 1 + CMakeLists.txt | 2 + LICENSE | 23 +++++ README.md | 43 ++++++-- luzer-scm-1.rockspec | 6 +- luzer/CMakeLists.txt | 14 +++ luzer/afl-lua.c | 189 ++++++++++++++++++++++++++++++++++++ luzer/afl.c | 16 +++ luzer/afl.h | 8 ++ luzer/init.lua | 12 ++- luzer/luzer.c | 33 ++++++- luzer/tests/CMakeLists.txt | 51 ++++++++++ 13 files changed, 385 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 0eacbbe..05e3543 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 afl++ ${{ matrix.PACKAGES }} - name: Running CMake run: > diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7cf4a..d07d529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support LuaJIT-friendly mode (#22). - Support LuaCov. - Support Address and UndefinedBehaviour sanitizers. +- Initial integration with an AFL (American Fuzzy Lop). ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index 247d0aa..9a3aa27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,8 @@ find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_VERSION}") +string(RANDOM LENGTH 9 ALPHABET 0123456789 RANDOM_SEED) + if(${LLVM_PACKAGE_VERSION} VERSION_LESS 5.0.0) message(FATAL_ERROR "LLVM 5.0.0 or newer is required") endif() diff --git a/LICENSE b/LICENSE index d78411f..98d86c3 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 75becb8..9d97f5d 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/ligurio/tarantool-lua-api-tests @@ -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 30c36a4..5726db4 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 25070b9..e3d9160 100644 --- a/luzer/CMakeLists.txt +++ b/luzer/CMakeLists.txt @@ -38,6 +38,7 @@ set(LUZER_SOURCES luzer.c fuzzed_data_provider.cc tracer.c counters.c + afl.c ${CMAKE_CURRENT_BINARY_DIR}/config.c) add_library(luzer_impl SHARED ${LUZER_SOURCES}) @@ -70,6 +71,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() @@ -91,3 +100,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 0000000..c0d25f8 --- /dev/null +++ b/luzer/afl-lua.c @@ -0,0 +1,189 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright © 2020, Steven Johnstone + * Copyright © 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, "%s is not set.\n", SHM_ENV); + return -1; + } + afl_shm = shmat(atoi(shm), NULL, 0); + if (afl_shm == (void*) -1) { + fprintf(stderr, "shmat() has failed (%s).\n", strerror(errno)); + return -1; + } + return 0; +} + +static int +fork_write(int pid) { + int buf_sz = 4; + (void)buf_sz; + assert(buf_sz == write(afl_write_fd, &pid, buf_sz)); + return 0; +} + +static int +fork_read(void) { + void *buf; + (void)buf; + int buf_sz = 4; + (void)buf_sz; + 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); + } + + 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); + + 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(); + 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("afl-lua"); + abort(); + } + fork_write(status); + } + + return EXIT_SUCCESS; +} diff --git a/luzer/afl.c b/luzer/afl.c new file mode 100644 index 0000000..c8c8526 --- /dev/null +++ b/luzer/afl.c @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright © 2020, Steven Johnstone + * Copyright © 2025, Sergey Bronnikov + */ + +#include + +int +is_afl_running(void) +{ + if (getenv("AFL_LUA_IS_RUNNING")) + return 0; + return -1; +} diff --git a/luzer/afl.h b/luzer/afl.h new file mode 100644 index 0000000..63fd74a --- /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 0908090..0b34c34 100644 --- a/luzer/init.lua +++ b/luzer/init.lua @@ -62,10 +62,14 @@ 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 = "" + if arg ~= nil 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 35f0c17..55eb000 100644 --- a/luzer/luzer.c +++ b/luzer/luzer.c @@ -29,6 +29,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 fe73481..9bd7941 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 @@ -256,3 +270,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()