From f80a574ffc6a7e98061616b583e205212390f100 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Sun, 5 Jul 2026 21:17:19 +0300 Subject: [PATCH 1/4] cmake: FORTIFY_SOURCE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: На NixOS clang wrapper через hardening flags добавляет -D_FORTIFY_SOURCE=2 после пользовательских флагов, а -O2 — до. Когда CMake переопределяет -O2 на -O0 (для ASan/UBSan тестовых целей), -D_FORTIFY_SOURCE=2 остаётся, и glibc 2.40+ выдаёт ошибку: _FORTIFY_SOURCE requires compiling with optimization (-O). Исправление (в двух файлах): 1. luzer/CMakeLists.txt:46-52 — Добавлен -U_FORTIFY_SOURCE в общие флаги компиляции (belt-and-suspenders). 2. luzer/tests/CMakeLists.txt:218-219, 350-351 — Заменены -O0 на -O1 для ASan/UBSan тестовых целей (luac_asan, luac_ubsan, testlib_asan, testlib_ubsan). -O1 — минимальный рекомендуемый уровень оптимизации для санитайзеров. (cherry picked from commit 563e0a64f0e985562d48687652781a275382cba8) --- luzer/CMakeLists.txt | 12 +++++------- luzer/tests/CMakeLists.txt | 8 ++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/luzer/CMakeLists.txt b/luzer/CMakeLists.txt index 1db3eee..45ba562 100644 --- a/luzer/CMakeLists.txt +++ b/luzer/CMakeLists.txt @@ -43,13 +43,11 @@ add_compile_options( -Wno-unused-parameter -Wpedantic ) -if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") - # It turns out that macOS set _FORTIFY_SOURCE internally, so we - # need to undefine it first, otherwise an error "'_FORTIFY_SOURCE' - # macro redefined" breaks a building. - add_compile_options(-U_FORTIFY_SOURCE) - add_compile_options(-D_FORTIFY_SOURCE=2) -endif() +# _FORTIFY_SOURCE requires optimization (-O). When building without +# optimization (Debug) or with -O0 (ASan/UBSan tests), glibc 2.40+ +# errors out. Since the toolchain may define _FORTIFY_SOURCE by +# default (e.g. NixOS), undefine it unconditionally. +add_compile_options(-U_FORTIFY_SOURCE) set(LUZER_SOURCES luzer.c compat.c diff --git a/luzer/tests/CMakeLists.txt b/luzer/tests/CMakeLists.txt index d02a1f4..6cdab3d 100644 --- a/luzer/tests/CMakeLists.txt +++ b/luzer/tests/CMakeLists.txt @@ -215,8 +215,8 @@ macro(generate_luac_lib name cflags) endmacro() generate_luac_lib(luac "") -generate_luac_lib(luac_asan "-fsanitize=address;-O0") -generate_luac_lib(luac_ubsan "-fsanitize=undefined;-O0") +generate_luac_lib(luac_asan "-fsanitize=address;-O1") +generate_luac_lib(luac_ubsan "-fsanitize=undefined;-O1") macro(generate_luac_test name env_vars pass_regex) add_test(NAME ${name} @@ -347,8 +347,8 @@ macro(generate_testlib name cflags) endmacro() generate_testlib(testlib "") -generate_testlib(testlib_asan "-fsanitize=address;-O0") -generate_testlib(testlib_ubsan "-fsanitize=undefined;-O0") +generate_testlib(testlib_asan "-fsanitize=address;-O1") +generate_testlib(testlib_ubsan "-fsanitize=undefined;-O1") macro(generate_ffi_test name env_vars pass_regex) add_test( From 1a9e9274f1dd2f13d7ab924298601f960c5bb66d Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 16 Jul 2026 19:45:10 +0300 Subject: [PATCH 2/4] Revert "cmake: FORTIFY_SOURCE" This reverts commit f80a574ffc6a7e98061616b583e205212390f100. --- luzer/CMakeLists.txt | 12 +++++++----- luzer/tests/CMakeLists.txt | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/luzer/CMakeLists.txt b/luzer/CMakeLists.txt index 45ba562..1db3eee 100644 --- a/luzer/CMakeLists.txt +++ b/luzer/CMakeLists.txt @@ -43,11 +43,13 @@ add_compile_options( -Wno-unused-parameter -Wpedantic ) -# _FORTIFY_SOURCE requires optimization (-O). When building without -# optimization (Debug) or with -O0 (ASan/UBSan tests), glibc 2.40+ -# errors out. Since the toolchain may define _FORTIFY_SOURCE by -# default (e.g. NixOS), undefine it unconditionally. -add_compile_options(-U_FORTIFY_SOURCE) +if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + # It turns out that macOS set _FORTIFY_SOURCE internally, so we + # need to undefine it first, otherwise an error "'_FORTIFY_SOURCE' + # macro redefined" breaks a building. + add_compile_options(-U_FORTIFY_SOURCE) + add_compile_options(-D_FORTIFY_SOURCE=2) +endif() set(LUZER_SOURCES luzer.c compat.c diff --git a/luzer/tests/CMakeLists.txt b/luzer/tests/CMakeLists.txt index 6cdab3d..d02a1f4 100644 --- a/luzer/tests/CMakeLists.txt +++ b/luzer/tests/CMakeLists.txt @@ -215,8 +215,8 @@ macro(generate_luac_lib name cflags) endmacro() generate_luac_lib(luac "") -generate_luac_lib(luac_asan "-fsanitize=address;-O1") -generate_luac_lib(luac_ubsan "-fsanitize=undefined;-O1") +generate_luac_lib(luac_asan "-fsanitize=address;-O0") +generate_luac_lib(luac_ubsan "-fsanitize=undefined;-O0") macro(generate_luac_test name env_vars pass_regex) add_test(NAME ${name} @@ -347,8 +347,8 @@ macro(generate_testlib name cflags) endmacro() generate_testlib(testlib "") -generate_testlib(testlib_asan "-fsanitize=address;-O1") -generate_testlib(testlib_ubsan "-fsanitize=undefined;-O1") +generate_testlib(testlib_asan "-fsanitize=address;-O0") +generate_testlib(testlib_ubsan "-fsanitize=undefined;-O0") macro(generate_ffi_test name env_vars pass_regex) add_test( From fdfce5523eb94367ee639709b105af9712774c07 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 16 Jul 2026 19:39:59 +0300 Subject: [PATCH 3/4] luzer: fix always truthy assertions --- luzer/tests/test_luajit_friendly.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luzer/tests/test_luajit_friendly.lua b/luzer/tests/test_luajit_friendly.lua index 68b7039..226fc17 100644 --- a/luzer/tests/test_luajit_friendly.lua +++ b/luzer/tests/test_luajit_friendly.lua @@ -14,7 +14,7 @@ local function TestOneInput(buf) local numbers = fdp:consume_numbers(0, 2*10^6, 10) for _, n in ipairs(numbers) do if n == 100500 then - assert("Bingo!") + error("Bingo!") end end From 9b0c252e3ed65a0d71eef3a083610e76ea17b96b Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Fri, 26 Jun 2026 14:02:37 +0300 Subject: [PATCH 4/4] luzer: print metrics only once The function metrics_print() is called twice when `-fork`/`-jobs` are used. The reason is that `metrics_print()` is called from `__attribute__((destructor))`, and the destructor is executed in each forked process. When libFuzzer is run with the `-fork=N` or `-jobs=N` flags, it forks child processes. Each child process inherits the loaded shared library and its destructors. Fixes #89 --- CHANGELOG.md | 1 + luzer/init.lua | 3 + luzer/luzer.c | 86 +++++++++++++++++++++++++++- luzer/tests/CMakeLists.txt | 28 +++++++++ luzer/tests/test_luajit_friendly.lua | 4 +- 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feba263..c3c53eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,3 +61,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - An error when Clang couldn't find the library. - A possible corruption on copying library (#91). - A crash due to incorrect use of signal handler (#40). +- Double printing metrics when using options -fork/-jobs (#89). diff --git a/luzer/init.lua b/luzer/init.lua index df079e3..4a3772f 100644 --- a/luzer/init.lua +++ b/luzer/init.lua @@ -69,6 +69,9 @@ local function Fuzz(test_one_input, custom_mutator, func_args) error("args is not a table") end local flags = build_flags(arg, luzer_args) + if flags.fork or flags.jobs then + luzer_impl._set_fork_mode() + end local test_path = arg[0] local lua_bin = progname(arg) local test_cmd = ("%s %s"):format(lua_bin, test_path) diff --git a/luzer/luzer.c b/luzer/luzer.c index 4470713..e840875 100644 --- a/luzer/luzer.c +++ b/luzer/luzer.c @@ -11,6 +11,7 @@ #ifdef LUA_HAS_JIT #include "luajit.h" #endif /* LUA_HAS_JIT */ +#include #include #include #include @@ -50,6 +51,9 @@ static int jit_status = 0; #endif /* LUA_HAS_JIT && LUAJIT_FRIENDLY_MODE */ int internal_hook_disabled = 0; +static bool fork_mode = false; +static bool fork_mode_main = false; +static bool metrics_printed = false; #define LUA_SETHOOK(lua_state, hook, mask, count) \ do { \ @@ -57,6 +61,33 @@ int internal_hook_disabled = 0; lua_sethook((lua_state), (hook), (mask), (count)); \ } while(0) +/** + * Checks whether the current call is from the original (parent) + * process or from a forked child process. On first call, stores + * the current process PID. On subsequent calls, compares the + * stored PID with the current PID to detect fork(). + * + * @return 1 - original (parent) process + * 0 - forked child process + */ +NO_SANITIZE static int +check_parent_or_child(void) +{ + static pid_t saved_pid = -1; + static bool first_call = true; + + if (first_call) { + saved_pid = getpid(); + first_call = false; + return 1; + } + + if (saved_pid == getpid()) + return 1; + else + return 0; +} + NO_SANITIZE static void set_global_lua_state(lua_State *L) { @@ -265,6 +296,14 @@ luaL_set_custom_mutator(lua_State *L) return 0; } +NO_SANITIZE static int +luaL_set_fork_mode(lua_State *L) +{ + fork_mode = true; + fork_mode_main = true; + return 0; +} + NO_SANITIZE static int luaL_test_one_input(lua_State *L) { @@ -287,7 +326,20 @@ luaL_test_one_input(lua_State *L) __attribute__((destructor)) static void teardown(void) { - metrics_print(); + /* + * In single-process mode, print metrics from the destructor. + * In fork/jobs mode, the main process prints metrics once: + * - from teardown() for -fork (exit() is called inside libFuzzer) + * - after LLVMFuzzerRunDriver() for -jobs (returns normally) + * Child processes (fork/exec) always skip printing. + */ + if (fork_mode_main) { + if (!metrics_printed) + metrics_print(); + } else if (!fork_mode) { + if (check_parent_or_child() == 1) + metrics_print(); + } } NO_SANITIZE int @@ -438,6 +490,17 @@ free_argv(int argc, char **argv) NO_SANITIZE static int luaL_fuzz(lua_State *L) { + /* Remember PID. */ + check_parent_or_child(); + + /* + * If LUZER_IN_FORK_MODE is set, this process is a child + * spawned by libFuzzer's -jobs mode. Mark fork_mode without + * setting fork_mode_main so children skip metrics printing. + */ + if (getenv("LUZER_IN_FORK_MODE")) + fork_mode = true; + const char *str = luaL_checkstring(L, -1); lua_pop(L, 1); char *argv_0 = strdup(str); @@ -534,8 +597,28 @@ luaL_fuzz(lua_State *L) jit_status = luajit_has_enabled_jit(L); #endif set_global_lua_state(L); + + /* + * Set an environment variable so child processes spawned + * via -jobs (which use exec, not fork) know they are + * running in fork/jobs mode and should skip printing. + */ + if (fork_mode) + setenv("LUZER_IN_FORK_MODE", "1", 1); + int rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput); + /* + * In -jobs mode, the main process returns here after all + * child jobs have completed. Print metrics once. + * In -fork mode, libFuzzer calls exit() inside FuzzWithFork, + * so we never reach this point; printing happens in teardown(). + */ + if (fork_mode_main) { + metrics_print(); + metrics_printed = true; + } + free_argv(argc, argv); luaL_cleanup(L); @@ -548,6 +631,7 @@ static const struct luaL_Reg Module[] = { { "Fuzz", luaL_fuzz }, { "FuzzedDataProvider", luaL_fuzzed_data_provider }, { "_set_custom_mutator", luaL_set_custom_mutator }, + { "_set_fork_mode", luaL_set_fork_mode }, { "_mutate", luaL_mutate }, { NULL, NULL } }; diff --git a/luzer/tests/CMakeLists.txt b/luzer/tests/CMakeLists.txt index d02a1f4..fbe05f4 100644 --- a/luzer/tests/CMakeLists.txt +++ b/luzer/tests/CMakeLists.txt @@ -89,6 +89,34 @@ set_tests_properties(luzer_options_jobs_test PROPERTIES PASS_REGULAR_EXPRESSION "Job 4 exited with exit code 0" ) +set(LUAJIT_METRICS_MESSAGE "Total number of recorded traces") +if (NOT LUAJIT_FRIENDLY_MODE) + set(LUAJIT_METRICS_MESSAGE "LuaJIT metrics disabled.") +endif() +add_test( + NAME luzer_options_jobs_metrics_test + COMMAND ${LUA_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_luajit_friendly.lua + -runs=100 -jobs=2 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(luzer_options_jobs_metrics_test PROPERTIES + ENVIRONMENT "DISABLE_LUAJIT_METRICS=1;LUA_CPATH=${LUA_CPATH};LUA_PATH=${LUA_PATH}" + FAIL_REGULAR_EXPRESSION "(${LUAJIT_METRICS_MESSAGE})(.*\\n.*\\1)+" +) + +add_test( + NAME luzer_options_fork_metrics_test + COMMAND ${LUA_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_luajit_friendly.lua + -runs=100 -fork=1 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +set_tests_properties(luzer_options_fork_metrics_test PROPERTIES + ENVIRONMENT "DISABLE_LUAJIT_METRICS=1;LUA_CPATH=${LUA_CPATH};LUA_PATH=${LUA_PATH}" + FAIL_REGULAR_EXPRESSION "(${LUAJIT_METRICS_MESSAGE})(.*\\n.*\\1)+" +) + add_test( NAME luzer_options_help_test COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_options_2.lua diff --git a/luzer/tests/test_luajit_friendly.lua b/luzer/tests/test_luajit_friendly.lua index 226fc17..a0dfb4e 100644 --- a/luzer/tests/test_luajit_friendly.lua +++ b/luzer/tests/test_luajit_friendly.lua @@ -1,13 +1,11 @@ local luzer = require("luzer") -local chunk = [[ local function fib(n) if n <= 1 then return n end return fib(n - 1) + fib(n - 2) end -]] local function TestOneInput(buf) local fdp = luzer.FuzzedDataProvider(buf) @@ -24,7 +22,7 @@ local function TestOneInput(buf) end -- Needed for testing LuaJIT metric with parsed functions. - load(chunk) + fib(5) end local args = {