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..844304d1 100644 --- a/README.md +++ b/README.md @@ -17,22 +17,26 @@ 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") @@ -56,7 +60,10 @@ end luzer.Fuzz(TestOneInput) ``` -3. Start the fuzzer using the fuzz target +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 +89,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 +110,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/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-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..e39875ef 100644 --- a/luzer/CMakeLists.txt +++ b/luzer/CMakeLists.txt @@ -44,13 +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 - ${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 @@ -82,6 +86,22 @@ target_link_libraries(custom_mutator PRIVATE luzer_impl ) +set(AFL_LUA afl-lua) +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) endif() @@ -113,3 +133,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..2ac275f7 --- /dev/null +++ b/luzer/afl-lua.c @@ -0,0 +1,161 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2020, Steven Johnstone + * Copyright (c) 2025, Sergey Bronnikov + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" + +#include "afl.h" +#include "tracer.h" +#include "tracer_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. + */ +static const char *SHM_ENV = "__AFL_SHM_ID"; +static const char *NOFORK = "AFL_NO_FORKSRV"; + +static const int afl_read_fd = FORKSRV_FD; +static const int afl_write_fd = afl_read_fd + 1; + +static void +fork_write(int pid) { + int buf_sz = 4; + if (buf_sz != write(afl_write_fd, &pid, buf_sz)) { + perror("write"); + abort(); + } +} + +static void +fork_read(void) { + void *buf; + int buf_sz = 4; + if (buf_sz != read(afl_read_fd, &buf, buf_sz)) { + perror("read"); + abort(); + } +} + +static int +fork_close(void) { + close(afl_read_fd); + close(afl_write_fd); + return 0; +} + +int +main(int argc, const char **argv) { + if (argc == 1) { + fprintf(stderr, "afl-lua: missed arguments\n"); + exit(EXIT_FAILURE); + } + + int rc = shm_init(SHM_ENV); + 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); + } + shm_deinit(); + 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); + /* + * 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; + } + fork_write(child); + int status = 0; + 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.c b/luzer/afl.c new file mode 100644 index 00000000..10feac48 --- /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 1; + return 0; +} diff --git a/luzer/afl.h b/luzer/afl.h new file mode 100644 index 00000000..b05a2329 --- /dev/null +++ b/luzer/afl.h @@ -0,0 +1,9 @@ +#ifndef LUZER_AFL_LUA_H_ +#define LUZER_AFL_LUA_H_ + +#define AFL_LUA_ENV "AFL_LUA_IS_RUNNING" +#define AFL_LUA_MAXINPUT 512 + +int is_afl_running(void); + +#endif // LUZER_AFL_LUA_H_ diff --git a/luzer/init.lua b/luzer/init.lua index df079e3f..4d75ca09 100644 --- a/luzer/init.lua +++ b/luzer/init.lua @@ -68,10 +68,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 not luzer_impl._afl_mode 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 734e02d8..a2be95e9 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" @@ -303,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, @@ -314,20 +316,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 +341,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); @@ -543,8 +536,17 @@ 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()) { + /* 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); + } free_argv(argc, argv); luaL_cleanup(L); @@ -583,6 +585,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); 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) diff --git a/luzer/tests/CMakeLists.txt b/luzer/tests/CMakeLists.txt index 3be95190..721fbba0 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,55 @@ 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" +) +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 + 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_