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 68b7039..a0dfb4e 100644 --- a/luzer/tests/test_luajit_friendly.lua +++ b/luzer/tests/test_luajit_friendly.lua @@ -1,20 +1,18 @@ 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) 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 @@ -24,7 +22,7 @@ local function TestOneInput(buf) end -- Needed for testing LuaJIT metric with parsed functions. - load(chunk) + fib(5) end local args = {