Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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/
4 changes: 3 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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/
6 changes: 4 additions & 2 deletions luzer-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -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 <estetus@gmail.com>",
license = "ISC",
Expand All @@ -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",
Expand Down
39 changes: 32 additions & 7 deletions luzer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}/"
)
161 changes: 161 additions & 0 deletions luzer/afl-lua.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2020, Steven Johnstone
* Copyright (c) 2025, Sergey Bronnikov
*/

#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#include <sys/shm.h>
#include <sys/wait.h>

#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;
}
Loading
Loading