Skip to content

Commit 2700f18

Browse files
committed
luzer: fix memory leaks
The patch fixes memory leaks in luzer that manifest themselves in different ways: 1. `LLVMFuzzerRunDriver()` returns due to error with dictionary. 2. Lua objects accumulate memory between `TestOneInput()` runs, but the cause is the same for everyone: Lua is a language with GC-based memory management, and after finishing the work, we did not free the memory occupied by Lua objects, so libFuzzer thinks there was a memory leak. Fixes #52 Fixes #65
1 parent 2a58b8a commit 2700f18

File tree

6 files changed

+122
-20
lines changed

6 files changed

+122
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5757
- A memory leak in a Lua-based implementation of `TestOneInput()`.
5858
- An initial buffer size in FuzzedDataProvider.
5959
- Search of the archiver tool (`ar`) in the OSS Fuzz environment.
60+
- Memory leak in FuzzedDataProvider (#52).
61+
- Segfault on parsing a broken dictionary (#65).

docs/usage.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,33 @@ and can be disabled by setting the enviroment variable
228228
`DISABLE_LUAJIT_METRICS`. Learn more about the enviroment variable
229229
in the section [LuaJIT Metrics](#luajit-metrics).
230230
231+
### Memory leaks
232+
233+
When a target application (or a fuzzer) consumes increasing
234+
amounts of RAM over time without releasing it, it can be a normal
235+
memory consumption or memory leak is occurring. The common causes
236+
of memory leak are: unreleased references, improper handling of
237+
native resources in Lua. If you are encountering a memory leak in
238+
a target application while using luzer, you may need to use memory
239+
debugging tools to identify the specific code segment that isn't
240+
freeing memory.
241+
242+
luzer can encounter false positive memory leaks during testing.
243+
When fuzzing native extensions, using FDP (FuzzingDataProvider), or
244+
using FFI memory leak detection (ASan) should be disabled to
245+
prevent false reports. Set flag `-detect_leaks=0` using enviroment
246+
variable [`ASAN_OPTIONS`][asan-flags] as it is recommended by
247+
AddressSanitizer:
248+
249+
```
250+
SUMMARY: AddressSanitizer: 96 byte(s) leaked in 6 allocation(s).
251+
INFO: to ignore leaks on libFuzzer side use -detect_leaks=0.
252+
```
253+
231254
[ffi-library-url]: https://luajit.org/ext_ffi.html
232255
[programming-in-lua-8]: https://www.lua.org/pil/8.html
233256
[programming-in-lua-24]: https://www.lua.org/pil/24.html
234257
[atheris-native-extensions]: https://github.com/google/atheris/blob/master/native_extension_fuzzing.md
235258
[atheris-native-extensions-video]: https://www.youtube.com/watch?v=oM-7lt43-GA
236259
[luacov-website]: https://lunarmodules.github.io/luacov/
260+
[asan-flags]: https://github.com/google/sanitizers/wiki/addresssanitizerflags

luzer/luzer.c

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,33 @@ free_argv(int argc, char **argv)
437437
free(argv);
438438
}
439439

440+
NO_SANITIZE static void
441+
shutdown_lua(void)
442+
{
443+
lua_State *L = get_global_lua_state();
444+
luaL_cleanup(L);
445+
lua_close(L);
446+
set_global_lua_state(NULL);
447+
}
448+
449+
int atexit_retcode;
450+
451+
NO_SANITIZE void
452+
atexit_handler(void) {
453+
_exit(atexit_retcode);
454+
}
455+
456+
NO_SANITIZE static void
457+
graceful_exit(int retcode, bool prevent_crash_report) {
458+
if (prevent_crash_report) {
459+
/* Disable libFuzzer's atexit(). */
460+
atexit_retcode = retcode;
461+
atexit(&atexit_handler);
462+
}
463+
shutdown_lua();
464+
exit(retcode);
465+
}
466+
440467
NO_SANITIZE static int
441468
luaL_fuzz(lua_State *L)
442469
{
@@ -536,14 +563,8 @@ luaL_fuzz(lua_State *L)
536563
jit_status = luajit_has_enabled_jit(L);
537564
#endif
538565
set_global_lua_state(L);
539-
int rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput);
540-
541-
free_argv(argc, argv);
542-
luaL_cleanup(L);
543-
544-
lua_pushnumber(L, rc);
545-
546-
return 1;
566+
graceful_exit(LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput), true);
567+
return 0;
547568
}
548569

549570
static const struct luaL_Reg Module[] = {

luzer/tests/CMakeLists.txt

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -371,18 +371,6 @@ if (LUA_HAS_JIT)
371371
"${TEST_ENV};FFI_LIB_NAME=testlib${CMAKE_SHARED_LIBRARY_SUFFIX}"
372372
"Done 10 runs in 0 second"
373373
)
374-
string(JOIN ";" TEST_ENVIRONMENT
375-
"${TEST_ENV}"
376-
LD_PRELOAD=${ASAN_DSO_PATH}
377-
FFI_LIB_NAME=testlib_asan${CMAKE_SHARED_LIBRARY_SUFFIX}
378-
)
379-
# XXX: Memory leak in FDP is expected on Linux, should be fixed
380-
# in [1].
381-
# 1. https://github.com/ligurio/luzer/issues/52
382-
set(PASS_PATTERN "LeakSanitizer: detected memory leaks")
383-
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
384-
set(PASS_PATTERN "Done 10 runs in 0 second")
385-
endif()
386374
generate_ffi_test(luzer_ffi_asan
387375
"${TEST_ENVIRONMENT}"
388376
${PASS_PATTERN}
@@ -391,6 +379,8 @@ if (LUA_HAS_JIT)
391379
"${TEST_ENV}"
392380
LD_PRELOAD=${UBSAN_DSO_PATH}
393381
FFI_LIB_NAME=testlib_ubsan${CMAKE_SHARED_LIBRARY_SUFFIX}
382+
"${TEST_ENV};LD_PRELOAD=${ASAN_DSO_PATH};FFI_LIB_NAME=testlib_asan.so"
383+
"Done 10 runs in [0-9] second"
394384
)
395385
generate_ffi_test(luzer_ffi_ubsan
396386
"${TEST_ENVIRONMENT}"
@@ -419,3 +409,37 @@ if (LUA_HAS_JIT)
419409
"runtime error: load of null pointer of type"
420410
)
421411
endif()
412+
413+
string(JOIN ";" TEST_ENV
414+
"LUA_CPATH=${LUA_CPATH}"
415+
"LUA_PATH=${LUA_PATH}"
416+
"LD_PRELOAD=${ASAN_DSO_PATH}"
417+
)
418+
add_test(
419+
NAME luzer_missed_dict_test
420+
COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_options_3.lua
421+
-dict=${CMAKE_CURRENT_SOURCE_DIR}/nonexistent.dict
422+
)
423+
list(APPEND FAIL_REGULAR_EXPRESSION
424+
"LeakSanitizer: detected memory leaks"
425+
"[0-9]+ byte(s) leaked in [0-9]+ allocation"
426+
)
427+
set_tests_properties(luzer_missed_dict_test PROPERTIES
428+
ENVIRONMENT "${TEST_ENV}"
429+
PASS_REGULAR_EXPRESSION "ParseDictionaryFile: file does not exist or is empty"
430+
FAIL_REGULAR_EXPRESSION "${FAIL_REGULAR_EXPRESSION}"
431+
)
432+
433+
add_test(
434+
NAME luzer_leak_memory_test
435+
COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/leak_memory.lua
436+
-rss_limit_mb=500
437+
)
438+
list(APPEND PASS_REGULAR_EXPRESSION
439+
"libFuzzer disabled leak detection after every mutation"
440+
"ERROR: libFuzzer: out-of-memory"
441+
)
442+
set_tests_properties(luzer_leak_memory_test PROPERTIES
443+
ENVIRONMENT "${TEST_ENV}"
444+
PASS_REGULAR_EXPRESSION "${PASS_REGULAR_EXPRESSION}"
445+
)

luzer/tests/leak_memory.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
local luzer = require("luzer")
2+
3+
local leaky_cache = {} -- luacheck: no unused
4+
5+
-- Function to create a large, nested table.
6+
local function make_big_object(size)
7+
if size <= 0 then
8+
return {}
9+
end
10+
-- Create nested tables recursively.
11+
return {
12+
make_big_object(size - 1)
13+
}
14+
end
15+
16+
local function TestOneInput(_buf)
17+
local new_object = make_big_object(2)
18+
table.insert(leaky_cache, new_object)
19+
end
20+
21+
luzer.Fuzz(TestOneInput)

luzer/tests/test_options_3.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
local luzer = require("luzer")
2+
3+
local function TestOneInput(buf)
4+
local fdp = luzer.FuzzedDataProvider(buf)
5+
local str = fdp:consume_string(100)
6+
local str_chars = {}
7+
str:gsub(".", function(c) table.insert(str_chars, c) end)
8+
end
9+
10+
luzer.Fuzz(TestOneInput)

0 commit comments

Comments
 (0)