Skip to content
Draft
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 @@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v3

- 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 afl++ ${{ matrix.PACKAGES }}

- name: Running CMake
run: >
Expand Down
1 change: 1 addition & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ include_files = {
exclude_files = {
".rocks",
"build/",
"luzer/afl_mutator.lua",
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support LuaJIT-friendly mode (#22).
- Support LuaCov.
- Support Address and UndefinedBehaviour sanitizers.
- Initial integration with an AFL (American Fuzzy Lop).

### Changed

Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ find_package(LLVM REQUIRED CONFIG)

message(STATUS "Found LLVM ${LLVM_VERSION}")

string(RANDOM LENGTH 9 ALPHABET 0123456789 RANDOM_SEED)

if(${LLVM_PACKAGE_VERSION} VERSION_LESS 5.0.0)
message(FATAL_ERROR "LLVM 5.0.0 or newer is required")
endif()
Expand Down
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,32 @@ 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 with a fuzz target invoking your code:

```lua
local luzer = require("luzer")

local function TestOneInput(buf)
local buf = buf or io.read("*a")
local b = {}
buf:gsub(".", function(c) table.insert(b, c) end)
if b[1] == 'c' then
Expand All @@ -56,7 +61,22 @@ end
luzer.Fuzz(TestOneInput)
```

3. Start the fuzzer using the fuzz target
Make sure Lua script has failed when string "crash" is passed to `stdin`:

```sh
$ echo "crash" | luajit example.lua
lua: example.lua:8: assertion failed!
stack traceback:
[C]: in function 'assert'
example.lua:8: in function 'fuzz'
example.lua:14: in main chunk
[C]: in ?
```

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 +102,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/corpus
$ __AFL_SHM_ID=$RANDOM afl-fuzz -D -i in/ -o out/ luzer examples/example_basic.lua
```

See tests that uses luzer library in:

- Tarantool Lua API tests, https://github.com/ligurio/tarantool-lua-api-tests
Expand All @@ -99,4 +127,5 @@ Copyright © 2022-2025 [Sergey Bronnikov][bronevichok-url].
Distributed under the ISC License.

[libfuzzer-url]: https://llvm.org/docs/LibFuzzer.html
[AFL-url]: https://aflplus.plus/
[bronevichok-url]: https://bronevichok.ru/
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 <[email protected]>",
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
21 changes: 21 additions & 0 deletions luzer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ set(LUZER_SOURCES luzer.c
fuzzed_data_provider.cc
tracer.c
counters.c
afl.c
${CMAKE_CURRENT_BINARY_DIR}/config.c)

add_library(luzer_impl SHARED ${LUZER_SOURCES})
Expand Down Expand Up @@ -70,6 +71,21 @@ target_link_libraries(custom_mutator PRIVATE
luzer_impl
)

set(AFL_LUA afl-lua)
add_executable(${AFL_LUA} afl-lua.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}
)

# TODO: install binary
set(AFL_MUTATOR_NAME luamutator)
add_library(${AFL_MUTATOR_NAME} SHARED afl_mutator.c)
target_include_directories(${AFL_MUTATOR_NAME} PRIVATE ${LUA_INCLUDE_DIR})
target_link_libraries(${AFL_MUTATOR_NAME} PRIVATE ${LUA_LIBRARIES})
target_compile_options(${AFL_MUTATOR_NAME} PUBLIC -Wall -Wextra -Wno-unused-parameter)

if(ENABLE_TESTING)
add_subdirectory(tests)
endif()
Expand All @@ -91,3 +107,8 @@ install(
FILES ${CMAKE_CURRENT_SOURCE_DIR}/init.lua
DESTINATION "${CMAKE_LUADIR}/${PROJECT_NAME}"
)

install(
TARGETS ${AFL_LUA}
DESTINATION "${CMAKE_BINARY_DIR}/"
)
188 changes: 188 additions & 0 deletions luzer/afl-lua.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright © 2020, Steven Johnstone
* 2022-2025, Sergey Bronnikov
*/

#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.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"

/*
* 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.
*/
const char *SHM_ENV = "__AFL_SHM_ID";
const char *NOFORK = "AFL_NO_FORKSRV";

const int afl_read_fd = FORKSRV_FD;
const int afl_write_fd = afl_read_fd + 1;

static unsigned char *afl_shm;
static size_t afl_shm_size = 1 << 16;

static int
shm_init(void) {
const char *shm = getenv(SHM_ENV);
if (!shm) {
fprintf(stderr, "%s is not set.\n", SHM_ENV);
return -1;
}
afl_shm = shmat(atoi(shm), NULL, 0);
if (afl_shm == (void*) -1) {
fprintf(stderr, "shmat() has failed (%s).\n", strerror(errno));
return -1;
}
return 0;
}

static int
fork_write(int pid) {
int buf_sz = 4;
(void)buf_sz;
assert(buf_sz == write(afl_write_fd, &pid, buf_sz));
return 0;
}

static int
fork_read(void) {
void *buf;
(void)buf;
int buf_sz = 4;
(void)buf_sz;
assert(buf_sz == read(afl_read_fd, &buf, buf_sz));
return 0;
}

static int
fork_close(void) {
close(afl_read_fd);
close(afl_write_fd);
return 0;
}

/**
* From afl-python
* https://github.com/jwilk/python-afl/blob/8df6bfefac5de78761254bf5d7724e0a52d254f5/afl.pyx#L74-L87
*/
#define LHASH_INIT 0x811C9DC5
#define LHASH_MAGIC_MULT 0x01000193
#define LHASH_NEXT(x) h = ((h ^ (unsigned char)(x)) * LHASH_MAGIC_MULT)

static inline unsigned int
lhash(const char *key, size_t offset) {
const char *const last = &key[strlen(key) - 1];
uint32_t h = LHASH_INIT;
while (key <= last)
LHASH_NEXT(*key++);
for (; offset != 0; offset >>= 8)
LHASH_NEXT(offset);

return h;
}

static unsigned int current_location;

static void
debug_hook(lua_State *L, lua_Debug *ar) {
lua_getinfo(L, "Sl", ar);
if (ar && ar->source && ar->currentline) {
const unsigned int new_location =
lhash(ar->source, ar->currentline) % afl_shm_size;
afl_shm[current_location ^ new_location] += 1;
current_location = new_location / 2;
}
}

int
main(int argc, const char **argv) {
if (argc == 1) {
fprintf(stderr, "afl-lua: missed arguments.\n");
exit(EXIT_FAILURE);
}

int rc = shm_init();
if (rc != 0) {
fprintf(stderr, "afl-lua: shm_init() failed.\n");
exit(EXIT_FAILURE);
}

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);

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);
}
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();
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);
abort();
}
return EXIT_SUCCESS;
}
fork_write(child);
int status = 0;
rc = wait(&status);
if (rc == -1) {
perror("afl-lua");
abort();
}
fork_write(status);
}

return EXIT_SUCCESS;
}
25 changes: 25 additions & 0 deletions luzer/afl.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright © 2020, Steven Johnstone
* 2022-2024, Sergey Bronnikov
*/

#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#include "lua.h"
#include "lauxlib.h"

int
is_afl_running(void)
{
if (getenv("AFL_LUA_IS_RUNNING"))
return 0;
return -1;
}
8 changes: 8 additions & 0 deletions luzer/afl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef LUZER_AFL_LUA_H_
#define LUZER_AFL_LUA_H_

#define AFL_LUA_ENV "AFL_LUA_IS_RUNNING"

int is_afl_running(void);

#endif // LUZER_AFL_LUA_H_
Loading
Loading