Commit Diff


commit - /dev/null
commit + e52d181485a11b13f0951adb604e18be363ab8c9
blob - /dev/null
blob + 3a71546d1f7c2c33d99148523db474e789717f7a (mode 644)
--- /dev/null
+++ .github/workflows/check.yaml
@@ -0,0 +1,30 @@
+name: Static analysis
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  static-analysis:
+    if: |
+      github.event_name == 'push' ||
+      github.event_name == 'pull_request' &&
+      github.event.pull_request.head.repo.full_name != github.repository
+
+    runs-on: ubuntu-20.04
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Setup luarocks
+        run: sudo apt install -y luarocks
+
+      - name: Setup luacheck
+        run: luarocks --local install luacheck
+
+      - run: echo $(luarocks path --lr-bin) >> $GITHUB_PATH
+
+      - name: Run luacheck
+        run: luacheck .
+
+      - run: luarocks lint luzer-scm-1.rockspec
blob - /dev/null
blob + e4f6a7e2b507acb55d5d21d70e5454ed917663af (mode 644)
--- /dev/null
+++ .github/workflows/publish.yaml
@@ -0,0 +1,59 @@
+name: Publish
+
+on:
+  push:
+    branches: [master]
+    tags: ['*']
+
+jobs:
+  publish-scm-1:
+    if: github.ref == 'refs/heads/master'
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Setup luarocks
+        run: sudo apt install -y luarocks
+
+      - name: Setup cjson (required for upload)
+        run: luarocks install --local lua-cjson
+
+      - name: Upload rockspec scm-1
+        run: luarocks upload --force --api-key=${{ secrets.LUAROCKS_API_KEY }} luzer-scm-1.rockspec
+
+  publish-tag:
+    if: startsWith(github.ref, 'refs/tags/')
+    runs-on: ubuntu-latest
+    steps:
+      # https://github.com/luarocks/luarocks/wiki/Types-of-rocks
+      - uses: actions/checkout@v3
+
+      - name: Setup luarocks
+        run: sudo apt install -y luarocks
+
+      # Make a release.
+      - run: |
+          echo TAG=${GITHUB_REF##*/} >> $GITHUB_ENV
+          luarocks new_version --tag ${{ env.TAG }}
+          luarocks install luzer-${{ env.TAG }}-1.rockspec
+          luarocks pack luzer-${{ env.TAG }}-1.rockspec
+
+      - name: Upload .rockspec and .src.rock ${{ env.TAG }}
+        run: |
+            luarocks upload --api-key=${{ secrets.LUAROCKS_API_KEY }} luzer-${{ env.TAG }}-1.rockspec
+            luarocks upload --api-key=${{ secrets.LUAROCKS_API_KEY }} luzer-${{ env.TAG }}-1.src.rock
+
+  build-rock:
+    if: |
+      github.event_name == 'push' ||
+      github.event_name == 'pull_request' &&
+      github.event.pull_request.head.repo.full_name != github.repository
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v3
+
+      - run: sudo apt install -y luarocks lua5.2 liblua5.2-dev libclang-common-14-dev clang-14
+
+      - run: luarocks --local build luzer-scm-1.rockspec
+
+      - run: luarocks --local make
blob - /dev/null
blob + a29216567e10d17dc8b98ba28d41722ea451689c (mode 644)
--- /dev/null
+++ .github/workflows/test.yaml
@@ -0,0 +1,53 @@
+name: Testing
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  testing:
+    if: |
+      github.event_name == 'push' ||
+      github.event_name == 'pull_request' &&
+      github.event.pull_request.head.repo.full_name != github.repository
+    strategy:
+      matrix:
+        LIBLUA:
+          - "5.4"
+          - "5.3"
+          - "5.2"
+          - "5.1"
+      fail-fast: false
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Setup common packages
+        run: sudo apt install -y clang-14 libclang-common-14-dev
+
+      - name: Setup Lua 5.1 packages
+        run: sudo apt install -y lua5.1 liblua5.1-0-dev
+        if: ${{ matrix.LIBLUA == '5.1' }}
+
+      - name: Setup Lua 5.2 packages
+        run: sudo apt install -y lua5.2 liblua5.2-dev
+        if: ${{ matrix.LIBLUA == '5.2' }}
+
+      - name: Setup Lua 5.3 packages
+        run: sudo apt install -y lua5.3 liblua5.3-dev
+        if: ${{ matrix.LIBLUA == '5.3' }}
+
+      - name: Setup Lua 5.4 packages
+        run: sudo apt install -y lua5.4 liblua5.4-dev
+        if: ${{ matrix.LIBLUA == '5.4' }}
+
+      - name: Running CMake
+        run: cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_TESTING=ON -S . -B build
+
+      - name: Building
+        run: cmake --build build --parallel $(nproc)
+
+      - name: Testing
+        run: cmake --build build --target test
+        env:
+          CTEST_OUTPUT_ON_FAILURE: 1
blob - /dev/null
blob + 502322504fbd132167191eb3561230099309a556 (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1,7 @@
+build
+build.luarocks
+crash-*
+.ccls-cache
+.luarc.json
+.rocks
+tags
blob - /dev/null
blob + d996543956e1608c5c91cd113d67d3db29d5ca83 (mode 644)
--- /dev/null
+++ .luacheckrc
@@ -0,0 +1,27 @@
+files["mutator/mutator_example.lua"] = {
+    globals = {
+        "LLVMFuzzerCustomMutator",
+        "LLVMFuzzerMutate",
+    },
+}
+
+files["luzer/tests/*.lua"] = {
+    globals = {
+        "luzer_test_one_input",
+        "luzer_custom_mutator",
+    },
+}
+
+include_files = {
+    '.luacheckrc',
+    '*.rockspec',
+    '**/*.lua',
+}
+
+exclude_files = {
+    '.rocks',
+    'luzer-tests/',
+    'patches/',
+    'build/',
+    'trash/',
+}
blob - /dev/null
blob + b9e81c8dc7ffbf08ff895aea08a0228091f1acde (mode 644)
--- /dev/null
+++ CHANGELOG.md
@@ -0,0 +1,14 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Integration with libFuzzer's `LLVMFuzzerTestOneInput()`.
+- Integration with libFuzzer's `LLVMFuzzerCustomMutator()`.
+- Integration with libFuzzer's `FuzzedDataProvider`.
blob - /dev/null
blob + bb2884fff6f9a12fb76c1e1a9cada7fed3d7714f (mode 644)
--- /dev/null
+++ CMakeLists.txt
@@ -0,0 +1,67 @@
+cmake_minimum_required(VERSION 3.10.2)
+
+project(luzer
+  LANGUAGES C CXX
+  VERSION "1.0.0"
+)
+
+find_package(Lua 5.1 REQUIRED)
+find_package(LLVM REQUIRED CONFIG)
+find_library(LIBRT rt)
+
+set(LUA_NAME "lua${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}")
+find_program(LUA_EXECUTABLE "${LUA_NAME}")
+if(NOT EXISTS ${LUA_EXECUTABLE})
+  message(FATAL_ERROR "${LUA_NAME} is required")
+endif()
+
+message(STATUS "Found Lua ${LUA_VERSION_STRING}")
+message(STATUS "Found Lua interpreter ${LUA_EXECUTABLE}")
+message(STATUS "Found LLVM ${LLVM_VERSION}")
+
+if(${LLVM_PACKAGE_VERSION} VERSION_LESS 5.0.0)
+  message(FATAL_ERROR "LLVM 5.0.0 or newer is required")
+endif()
+
+if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
+   NOT CMAKE_C_COMPILER_ID STREQUAL "Clang")
+  message(FATAL_ERROR
+      "\n"
+      "Building is supported with Clang compiler only.\n"
+      " $ rm -rf build\n"
+      " $ cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -S . -B build\n"
+      " $ cmake --build build --parallel\n"
+      "\n")
+endif()
+
+if(ENABLE_TESTING AND NOT EXISTS ${LUA_EXECUTABLE})
+  message(WARNING "Lua executable is not found, testing is not available.")
+  unset(ENABLE_TESTING)
+else()
+  enable_testing()
+endif()
+
+add_subdirectory(mutator)
+add_subdirectory(luzer)
+
+## Install ####################################################################
+###############################################################################
+
+if (NOT CMAKE_LUADIR)
+  set(CMAKE_LUADIR "${CMAKE_PREFIX_PATH}")
+endif()
+
+if (NOT CMAKE_LIBDIR)
+  set(CMAKE_LIBDIR "${CMAKE_INCLUDE_PATH}")
+endif()
+
+install(
+  FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/README.md
+    ${CMAKE_CURRENT_SOURCE_DIR}/docs/api.md
+    ${CMAKE_CURRENT_SOURCE_DIR}/docs/grammar_based_fuzzing.md
+    ${CMAKE_CURRENT_SOURCE_DIR}/docs/index.md
+    ${CMAKE_CURRENT_SOURCE_DIR}/docs/test_management.md
+    ${CMAKE_CURRENT_SOURCE_DIR}/docs/usage.md
+  DESTINATION ${CMAKE_LUADIR}/doc
+)
blob - /dev/null
blob + 75bf65a6163e8f3eb55d5072ada1ddceabf328cf (mode 644)
--- /dev/null
+++ CMakePresets.json
@@ -0,0 +1,63 @@
+{
+  "version": 6,
+  "cmakeMinimumRequired": {
+    "major": 3,
+    "minor": 20,
+    "patch": 0
+  },
+  "configurePresets": [
+    {
+      "name": "default",
+      "displayName": "Default Config (Unix Makefile)",
+      "description": "Default build using Unix Makefile generator",
+      "binaryDir": "${sourceDir}/build",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CMAKE_C_COMPILER": "clang",
+        "CMAKE_CXX_COMPILER": "clang++",
+        "CMAKE_EXPORT_COMPILE_COMMANDS": {
+          "type": "BOOL",
+          "value": "ON"
+        },
+        "ENABLE_TESTING": {
+          "type": "BOOL",
+          "value": "ON"
+        }
+      }
+    }
+  ],
+  "buildPresets": [
+    {
+      "name": "default",
+      "configurePreset": "default",
+      "jobs": 10
+    }
+  ],
+  "testPresets": [
+    {
+      "name": "default",
+      "configurePreset": "default",
+      "output": {"outputOnFailure": true},
+      "execution": {"noTestsAction": "error", "stopOnFailure": true}
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        },
+        {
+          "type": "build",
+          "name": "default"
+        },
+        {
+          "type": "test",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
blob - /dev/null
blob + d70059b9ce65a47a49fd8161ff744b16d01722ef (mode 644)
--- /dev/null
+++ CONTRIBUTING.md
@@ -0,0 +1,12 @@
+## Hacking
+
+For developing `luzer` you need to install required packages. On Debian: `apt
+install -y liblua5.1-0-dev llvm-dev libclang-common-13-dev clang cmake`.
+
+```sh
+$ cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_TESTING=ON -S . -B build
+$ cmake --build build --parallel
+$ cmake --build build --target test
+```
+
+You are ready to make patches!
blob - /dev/null
blob + e52f0d131d5e2ddd9ab5647a45e7fa741272f35c (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2022-2023 Sergey Bronnikov
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+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.
blob - /dev/null
blob + a45083a78cd0f2a92a2d050ed1c058a14c25f93d (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,92 @@
+[![Static analysis](https://github.com/ligurio/luzer/actions/workflows/check.yaml/badge.svg)](https://github.com/ligurio/luzer/actions/workflows/check.yaml)
+[![Testing](https://github.com/ligurio/luzer/actions/workflows/test.yaml/badge.svg)](https://github.com/ligurio/luzer/actions/workflows/test.yaml)
+[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
+[![Luarocks](https://img.shields.io/luarocks/v/ligurio/luzer/scm-1)](https://luarocks.org/modules/ligurio/luzer)
+
+# luzer
+
+a coverage-guided, native Lua fuzzer.
+
+## Overview
+
+Fuzzing is a type of automated testing which continuously manipulates inputs to
+a program to find bugs. `luzer` uses coverage guidance to intelligently walk
+through the code being fuzzed to find and report failures to the user. Since it
+can reach edge cases which humans often miss, fuzz testing can be particularly
+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.
+
+## Quickstart
+
+To use luzer in your own project follow these few simple steps:
+
+1. Setup `luzer` module:
+
+```sh
+$ luarocks --local install luzer
+$ eval $(luarocks path)
+```
+
+2. Create a fuzz target invoking your code:
+
+```lua
+local luzer = require("luzer")
+
+local function TestOneInput(buf)
+    local b = {}
+    buf:gsub(".", function(c) table.insert(b, c) end)
+    if b[1] == 'c' then
+        if b[2] == 'r' then
+            if b[3] == 'a' then
+                if b[4] == 's' then
+                    if b[5] == 'h' then
+                        assert(nil)
+                    end
+                end
+            end
+        end
+    end
+end
+
+luzer.Fuzz(TestOneInput)
+```
+
+3. Start the fuzzer using the fuzz target
+
+```
+$ luajit examples/example_basic.lua
+INFO: Running with entropic power schedule (0xFF, 100).
+INFO: Seed: 1557779137
+INFO: Loaded 1 modules   (151 inline 8-bit counters): 151 [0x7f0640e706e3, 0x7f0640e7077a),
+INFO: Loaded 1 PC tables (151 PCs): 151 [0x7f0640e70780,0x7f0640e710f0),
+INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
+INFO: A corpus is not provided, starting from an empty corpus
+#2	INITED cov: 17 ft: 18 corp: 1/1b exec/s: 0 rss: 26Mb
+#32	NEW    cov: 17 ft: 24 corp: 2/4b lim: 4 exec/s: 0 rss: 26Mb L: 3/3 MS: 5 ShuffleBytes-ShuffleBytes-CopyPart-ChangeByte-CMP- DE: "\x00\x00"-
+...
+```
+
+While fuzzing is in progress, the fuzzing engine generates new inputs and runs
+them against the provided fuzz target. By default, it continues to run until a
+failing input is found, or the user cancels the process (e.g. with `Ctrl^C`).
+
+The first lines indicate that the "baseline coverage" is gathered before
+fuzzing begins.
+
+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.
+
+## License
+
+Copyright © 2022-2023 [Sergey Bronnikov][bronevichok-url].
+
+Distributed under the ISC License.
+
+[libfuzzer-url]: https://llvm.org/docs/LibFuzzer.html
+[bronevichok-url]: https://bronevichok.ru/
blob - /dev/null
blob + 785a9e297fabab0f521d678b5d673a9840829a24 (mode 644)
--- /dev/null
+++ luzer/CMakeLists.txt
@@ -0,0 +1,77 @@
+# Locate compiler-rt libraries.
+# Location is LLVM_LIBRARY_DIRS/clang/<version>/lib/<OS>/,
+# for example LLVM_LIBRARY_DIRS/clang/4.0.0/lib/darwin/.
+#
+# See https://llvm.org/docs/LibFuzzer.html#using-libfuzzer-as-a-library
+
+set(LLVM_BASE ${LLVM_LIBRARY_DIRS}/clang/${LLVM_PACKAGE_VERSION})
+string(TOLOWER ${CMAKE_HOST_SYSTEM_NAME} OS_NAME)
+set(LIBCLANG_RT ${LLVM_BASE}/lib/${OS_NAME}/libclang_rt.fuzzer_no_main-x86_64.a)
+if(EXISTS ${LIBCLANG_RT})
+  message(STATUS "Found libclang_rt ${LIBCLANG_RT}")
+else()
+  message(FATAL_ERROR "libclang_rt is not found")
+endif()
+
+configure_file(
+  ${CMAKE_CURRENT_SOURCE_DIR}/version.c
+  ${CMAKE_CURRENT_BINARY_DIR}/version.c
+  @ONLY
+)
+
+set(LUZER_SOURCES luzer.c
+                  fuzzed_data_provider.cc
+                  tracer.c
+                  counters.c
+                  ${CMAKE_CURRENT_BINARY_DIR}/version.c)
+
+add_library(${CMAKE_PROJECT_NAME} SHARED ${LUZER_SOURCES})
+target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
+	${LUA_INCLUDE_DIR}
+)
+target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE
+	${LUA_LIBRARIES}
+	${LIBRT}
+	${LIBCLANG_RT}
+	-fsanitize=fuzzer-no-link
+)
+target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
+	-D_FORTIFY_SOURCE=2
+	-fpie
+	-fPIC
+	-Wall
+	-Wextra
+	-Werror
+	-Wpedantic
+	-Wno-unused-parameter
+	-pedantic
+	-fsanitize=fuzzer-no-link
+)
+set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES PREFIX "")
+
+set(custom_mutator_lib_source ${CMAKE_CURRENT_SOURCE_DIR}/custom_mutator_lib.c)
+add_library(custom_mutator SHARED ${custom_mutator_lib_source})
+target_include_directories(custom_mutator PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(custom_mutator PRIVATE ${LUA_LIBRARIES})
+set_target_properties(custom_mutator PROPERTIES VERSION ${PROJECT_VERSION})
+set_target_properties(custom_mutator PROPERTIES SOVERSION 1)
+
+if(ENABLE_TESTING)
+  add_subdirectory(tests)
+endif()
+
+install(
+  TARGETS ${PROJECT_NAME}
+  LIBRARY
+  DESTINATION "${CMAKE_LIBDIR}/"
+  RENAME luzer.so
+)
+
+# See description of NAMELINK_SKIP in
+# https://cmake.org/cmake/help/latest/command/install.html
+install(
+  TARGETS custom_mutator
+  LIBRARY
+  NAMELINK_SKIP
+  DESTINATION "${CMAKE_LIBDIR}/"
+)
blob - /dev/null
blob + 68d1ebb363710e621de92a97a061474f6cc270ec (mode 644)
--- /dev/null
+++ luzer/counters.c
@@ -0,0 +1,142 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include "counters.h"
+#include "macros.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+void __sanitizer_cov_8bit_counters_init(uint8_t* start, uint8_t* stop);
+void __sanitizer_cov_pcs_init(uint8_t* pcs_beg, uint8_t* pcs_end);
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+static const int kDefaultNumCounters = 1 << 20;
+
+// Number of counters requested by Lua instrumentation.
+int counter_index = 0;
+// Number of counters given to Libfuzzer.
+int counter_index_registered = 0;
+// Maximum number of counters and pctable entries that may be reserved and also
+// the number that are allocated.
+int max_counters = 0;
+// Counter Allocations. These are allocated once, before __sanitize_... are
+// called and can only be deallocated by test_only_reset_counters.
+unsigned char* counters = NULL;
+struct PCTableEntry* pctable = NULL;
+
+NO_SANITIZE void
+test_only_reset_counters(void) {
+	if (counters) {
+		munmap(counters, max_counters);
+		counters = NULL;
+	}
+	if (pctable) {
+		munmap(pctable, max_counters);
+		pctable = NULL;
+	}
+	max_counters = 0;
+	counter_index = 0;
+	counter_index_registered = 0;
+}
+
+NO_SANITIZE int
+reserve_counters(int counters) {
+	int ret = counter_index;
+	counter_index += counters;
+	return ret;
+}
+
+NO_SANITIZE int
+reserve_counter(void)
+{
+	return counter_index++;
+}
+
+NO_SANITIZE void
+increment_counter(int counter_index)
+{
+	if (counters != NULL && pctable != NULL) {
+		// `counters` is an allocation of length `max_counters`. If we reserve more
+		// than the allocated number of counters, we'll wrap around and overload
+		// old counters, trading away fuzzing quality for limits on memory usage.
+		counters[counter_index % max_counters]++;
+	}
+}
+
+NO_SANITIZE void
+set_max_counters(int max)
+{
+	if (counters != NULL && pctable != NULL) {
+		fprintf(stderr, "Internal error: attempt to set max number of counters after "
+						"counters were passed to the sanitizer!\n");
+		_exit(1);
+	}
+	if (max < 1)
+		_exit(1);
+
+	max_counters = max;
+}
+
+NO_SANITIZE int
+get_max_counters(void)
+{
+	return max_counters;
+}
+
+NO_SANITIZE counter_and_pc_table_range
+allocate_counters_and_pcs(void) {
+	if (max_counters < 1) {
+		set_max_counters(kDefaultNumCounters);
+	}
+	if (counter_index < counter_index_registered) {
+		fprintf(stderr, "Internal error: The counter index is "
+						"greater than the number of counters registered.\n");
+		_exit(1);
+	}
+	// Allocate memory.
+	if (counters == NULL || pctable == NULL) {
+		// We mmap memory for pctable and counters, instead of std::vector, ensuring
+		// that there is no initialization. The untouched memory will only cost
+		// virtual memory, which is cheap.
+		counters = (unsigned char*)(
+			mmap(NULL, max_counters, PROT_READ | PROT_WRITE,
+				 MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
+			pctable = (struct PCTableEntry*)(
+					  mmap(NULL, max_counters * sizeof(struct PCTableEntry),
+						   PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
+		if (counters == MAP_FAILED || pctable == MAP_FAILED) {
+			fprintf(stderr, "Internal error: Failed to mmap counters.\n");
+			_exit(1);
+		}
+	}
+
+	const int next_index = MIN(counter_index, max_counters);
+	if (counter_index_registered >= next_index) {
+		// There are no counters to pass. Perhaps because we've reserved more than
+		// max_counters, or because no counters have been reserved since this was
+		// last called.
+		counter_index_registered = counter_index;
+		return (counter_and_pc_table_range){NULL, NULL, NULL, NULL};
+	} else {
+		counter_and_pc_table_range ranges = {
+			.counters_start = counters + counter_index_registered,
+			.counters_end = counters + next_index,
+			.pctable_start = (uint8_t*)(pctable + counter_index_registered),
+			.pctable_end = (uint8_t*)(pctable + next_index)
+		};
+		counter_index_registered = counter_index;
+		return ranges;
+	}
+}
blob - /dev/null
blob + 4bfa9f469bfa0035448118474ecbb0303f8f4e57 (mode 644)
--- /dev/null
+++ luzer/counters.h
@@ -0,0 +1,47 @@
+#ifndef LUZER_COUNTERS_H_
+#define LUZER_COUNTERS_H_
+
+struct PCTableEntry {
+	void* pc;
+	long flags;
+};
+
+// Sets the global number of counters.
+// Must not be called after InitializeCountersWithLLVM is called.
+void set_max_counters(int max);
+
+// Returns the maximum number of allocatable luzer counters. If more than this
+// many counters are reserved, luzer reuses counters, lowering fuzz quality.
+int get_max_counters(void);
+
+// Returns a new counter index.
+int reserve_counter(void);
+// Reserves a number of counters with contiguous indices, and returns the first
+// index.
+int reserve_counters(int counters);
+
+// Increments a counter at the given index. If more than the maximum number of
+// counters has been reserved, reuse counters.
+void increment_counter(int counter_index);
+
+typedef struct counter_and_pc_table_range {
+	unsigned char* counters_start;
+	unsigned char* counters_end;
+	unsigned char* pctable_start;
+	unsigned char* pctable_end;
+} counter_and_pc_table_range;
+
+// Returns pointers to a range of memory for counters and another for pctable.
+// The intent is for this memory to be handed to Libfuzzer. It will only be
+// deallocated by test_only_reset_counters. The size of the ranges is proportional
+// to the number of counters reserved, unless no new counters were reserved or
+// more than max_counters were already reserved, in which case returns nullptrs.
+counter_and_pc_table_range allocate_counters_and_pcs(void);
+
+// Resets counters' state to defaults. This is not safe for use with the actual
+// fuzzer as, once fuzzing begins, the fuzzer is given access to the counters'
+// memory. Unless you swapped out the fuzzer and know it will not access the
+// previous counters and pctable entries again, you'll probably segfault.
+void test_only_reset_counters(void);
+
+#endif  // LUZER_COUNTERS_H_
blob - /dev/null
blob + 4895b6396567a8b59f833420b99efef74192f70f (mode 644)
--- /dev/null
+++ luzer/custom_mutator_lib.c
@@ -0,0 +1,42 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <lua.h>
+#include <lauxlib.h>
+
+#include "luzer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+int luaL_error(lua_State *L, const char *fmt, ...);
+size_t lua_objlen(lua_State *L, int index);
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+size_t
+LLVMFuzzerCustomMutator(uint8_t* data, size_t size,
+						size_t max_size, unsigned int seed)
+{
+	lua_State *L = get_global_lua_state();
+	lua_pushlstring(L, (char *)data, size);
+	lua_pushinteger(L, max_size);
+	lua_pushinteger(L, seed);
+	luaL_mutate(L);
+
+	size_t sz = lua_objlen(L, -1);
+	if (sz > max_size)
+		luaL_error(L, "The size of mutated data cannot be larger than a max_size.");
+	const char *buf = lua_tostring(L, -1);
+	free(data);
+	data = (uint8_t *)buf;
+
+	return sz;
+}
blob - /dev/null
blob + 93424cfee62cdbe00123e97f5bcd3b9c62450e3f (mode 644)
--- /dev/null
+++ luzer/fuzzed_data_provider.cc
@@ -0,0 +1,286 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+#include <float.h>
+#include <fuzzer/FuzzedDataProvider.h>
+
+#include "fuzzed_data_provider.h"
+#include "macros.h"
+
+/**
+ * Unique name for userdata metatables.
+ */
+#define FDP_LUA_UDATA_NAME	"fdp"
+
+/*
+ * A convenience wrapper turning the raw fuzzer input bytes into Lua primitive
+ * types. The methods behave similarly to math.random(), with all returned
+ * values depending deterministically on the fuzzer input for the current run.
+ */
+
+typedef struct {
+	FuzzedDataProvider *fdp;
+} lua_userdata_t;
+
+/* Consumes a string from the fuzzer input. */
+static int
+luaL_consume_string(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	size_t max_length = luaL_checkinteger(L, 2);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_string(max_length)");
+
+	std::string str = lfdp->fdp->ConsumeRandomLengthString(max_length);
+	const char *cstr = str.c_str();
+	lua_pushlstring(L, cstr, str.length());
+
+	return 1;
+}
+
+/* Consumes a table with specified number of strings from the fuzzer input. */
+static int
+luaL_consume_strings(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_strings(count, max_length)");
+	size_t count = luaL_checkinteger(L, 2);
+	size_t max_length = luaL_checkinteger(L, 3);
+
+	std::string str;
+	const char *cstr;
+
+	lua_newtable(L);
+	for (int i = 1; i <= (int)count; i++) {
+		str = lfdp->fdp->ConsumeRandomLengthString(max_length);
+		cstr = str.c_str();
+		lua_pushnumber(L, i);
+		lua_pushlstring(L, cstr, str.length());
+		lua_settable(L, -3);
+	}
+
+	return 1;
+}
+
+/* Consumes a boolean from the fuzzer input. */
+static int
+luaL_consume_boolean(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_boolean()");
+
+	bool b = lfdp->fdp->ConsumeBool();
+	lua_pushboolean(L, (int)b);
+
+	return 1;
+}
+
+/* Consumes a table with specified number of booleans from the fuzzer input. */
+static int
+luaL_consume_booleans(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_booleans(count)");
+	int count = luaL_checkinteger(L, 2);
+
+	lua_newtable(L);
+	for (int i = 1; i <= (int)count; i++) {
+		bool b = lfdp->fdp->ConsumeBool();
+		lua_pushnumber(L, i);
+		lua_pushboolean(L, (int)b);
+		lua_settable(L, -3);
+	}
+
+	return 1;
+}
+
+/* Consumes a float from the fuzzer input. */
+static int
+luaL_consume_number(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_number(min, max)");
+	double min = luaL_checknumber(L, 2);
+	double max = luaL_checknumber(L, 3);
+	if (min > max)
+		luaL_error(L, "min must be less than or equal to max");
+
+	auto number = lfdp->fdp->ConsumeFloatingPointInRange(min, max);
+	lua_pushnumber(L, number);
+
+	return 1;
+}
+
+/* Consumes a table with specified number of numbers from the fuzzer input. */
+static int
+luaL_consume_numbers(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_numbers(count, min, max)");
+	int count = luaL_checkinteger(L, 2);
+	double min = luaL_checkinteger(L, 3);
+	double max = luaL_checkinteger(L, 4);
+	if (min > max)
+		luaL_error(L, "min must be less than or equal to max");
+
+	lua_newtable(L);
+	for (int i = 1; i <= count; i++) {
+		auto number = lfdp->fdp->ConsumeFloatingPointInRange(min, max);
+		lua_pushnumber(L, i);
+		lua_pushnumber(L, number);
+		lua_settable(L, -3);
+	}
+
+	return 1;
+}
+
+/* Consumes an arbitrary int or an int between min and max from the fuzzer
+   input. */
+static int
+luaL_consume_integer(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_integer(min, max)");
+	int min = luaL_checkinteger(L, 2);
+	int max = luaL_checkinteger(L, 3);
+	if (min > max)
+		luaL_error(L, "min must be less than or equal to max");
+
+	auto number = lfdp->fdp->ConsumeIntegralInRange(min, max);
+	lua_pushnumber(L, number);
+
+	return 1;
+}
+
+/* Consumes an int array from the fuzzer input. */
+static int
+luaL_consume_integers(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_integers(count, min, max)");
+	int count = luaL_checkinteger(L, 2);
+	int min = luaL_checkinteger(L, 3);
+	int max = luaL_checkinteger(L, 4);
+	if (min > max)
+		luaL_error(L, "min must be less than or equal to max");
+
+	lua_newtable(L);
+	for (int i = 1; i <= (int)count; i++) {
+		auto number = lfdp->fdp->ConsumeIntegralInRange(min, max);
+		lua_pushnumber(L, i);
+		lua_pushinteger(L, number);
+		lua_settable(L, -3);
+	}
+
+	return 1;
+}
+
+static int
+luaL_consume_probability(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:consume_probability()");
+
+	auto probability = lfdp->fdp->ConsumeFloatingPointInRange(0.0, 1.0);
+	lua_pushnumber(L, probability);
+
+	return 1;
+}
+
+/* Returns the number of unconsumed bytes in the fuzzer input. */
+static int
+luaL_remaining_bytes(lua_State *L)
+{
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	if (!lfdp)
+		luaL_error(L, "Usage: <FuzzedDataProvider>:remaining_bytes()");
+
+	size_t sz = lfdp->fdp->remaining_bytes();
+	lua_pushnumber(L, sz);
+
+	return 1;
+}
+
+static int close(lua_State *L) {
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t *)luaL_checkudata(L, 1, FDP_LUA_UDATA_NAME);
+	delete lfdp->fdp;
+
+	return 0;
+}
+
+static int tostring(lua_State *L) {
+	lua_pushstring(L, "FuzzedDataProvider");
+	return 1;
+}
+
+const luaL_Reg methods[] =
+{
+	{ "consume_string", luaL_consume_string },
+	{ "consume_strings", luaL_consume_strings },
+	{ "consume_boolean", luaL_consume_boolean },
+	{ "consume_booleans", luaL_consume_booleans },
+	{ "consume_number", luaL_consume_number },
+	{ "consume_numbers", luaL_consume_numbers },
+	{ "consume_integer", luaL_consume_integer },
+	{ "consume_integers", luaL_consume_integers },
+	{ "consume_probability", luaL_consume_probability },
+	{ "remaining_bytes", luaL_remaining_bytes },
+	{ "__gc", close },
+	{ "__tostring", tostring },
+	{ NULL, NULL }
+};
+
+int
+luaL_fuzzed_data_provider(lua_State *L)
+{
+	int index = lua_gettop(L);
+	if (index != 1)
+		luaL_error(L, "Usage: luzer.FuzzedDataProvider(string)");
+
+	const char *data = luaL_checkstring(L, 1);
+	size_t size = strlen(data);
+
+	luaL_newmetatable(L, FDP_LUA_UDATA_NAME);
+	lua_pushvalue(L, -1);
+	lua_setfield(L, -2, "__index");
+#if LUA_VERSION_NUM == 501
+	luaL_register(L, NULL, methods);
+#else
+	luaL_setfuncs(L, methods, 0);
+#endif
+
+	lua_userdata_t *lfdp;
+	lfdp = (lua_userdata_t*)lua_newuserdata(L, sizeof(*lfdp));
+	FuzzedDataProvider *fdp = new FuzzedDataProvider((const unsigned char *)data, size);
+	lfdp->fdp = fdp;
+
+	luaL_getmetatable(L, FDP_LUA_UDATA_NAME);
+	lua_setmetatable(L, -2);
+
+	return 1;
+}
blob - /dev/null
blob + 74a999d20d31db793b7a6d70f0d84d4f5280b351 (mode 644)
--- /dev/null
+++ luzer/fuzzed_data_provider.h
@@ -0,0 +1,12 @@
+#ifndef LUZER_FUZZED_DATA_PROVIDER_H_
+#define LUZER_FUZZED_DATA_PROVIDER_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+	int luaL_fuzzed_data_provider(lua_State *L);
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif  // LUZER_FUZZED_DATA_PROVIDER_H_
blob - /dev/null
blob + d776a6c05ebf95e627c8ad87bc39b0401b517a70 (mode 644)
--- /dev/null
+++ luzer/luzer.c
@@ -0,0 +1,459 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+#define _GNU_SOURCE
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <signal.h>
+#include <string.h>
+#include <dlfcn.h>
+#include <libgen.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <linux/limits.h>
+
+#include "fuzzed_data_provider.h"
+#include "counters.h"
+#include "macros.h"
+#include "tracer.h"
+#include "version.h"
+#include "luzer.h"
+
+#define TEST_ONE_INPUT_FUNC "luzer_test_one_input"
+#define CUSTOM_MUTATOR_FUNC "luzer_custom_mutator"
+#define CUSTOM_MUTATOR_LIB "libcustom_mutator.so.1"
+#define DEBUG_HOOK_FUNC "luzer_custom_hook"
+
+static lua_State *LL;
+
+static void
+set_global_lua_state(lua_State *L)
+{
+	LL = L;
+}
+
+lua_State *
+get_global_lua_state(void)
+{
+	if (!LL)
+		luaL_error(LL, "Lua state is not initialized.");
+
+	return LL;
+}
+
+#if LUA_VERSION_NUM < 502
+static int
+luaL_traceback(lua_State *L) {
+	lua_getfield(L, LUA_GLOBALSINDEX, "debug");
+	if (!lua_istable(L, -1)) {
+		lua_pop(L, 1);
+		return 1;
+	}
+	lua_getfield(L, -1, "traceback");
+	if (!lua_isfunction(L, -1)) {
+		lua_pop(L, 2);
+		return 1;
+	}
+	lua_pushvalue(L, 1);
+	lua_pushinteger(L, 2);
+	lua_call(L, 2, 1);
+	fprintf(stderr, "%s\n", lua_tostring(L, -1));
+	return 1;
+}
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+typedef int (*UserCb)(const uint8_t* Data, size_t Size);
+int LLVMFuzzerRunDriver(int* argc, char*** argv,
+                        int (*UserCb)(const uint8_t* Data, size_t Size));
+void __sanitizer_cov_8bit_counters_init(uint8_t* start, uint8_t* stop);
+
+// [pcs_beg, pcs_end) is an array of ptr-sized integers representing
+// pairs [PC, PCFlags] for every instrumented block in the current DSO.
+// Capture this array in order to read the PCs and their Flags.
+// The number of PCs and PCFlags for a given DSO is the same as the number
+// of 8-bit counters (-fsanitize-coverage=inline-8bit-counters), or
+// boolean flags (-fsanitize-coverage=inline=bool-flags), or trace_pc_guard
+// callbacks (-fsanitize-coverage=trace-pc-guard).
+// A PCFlags describes the basic block:
+//  * bit0: 1 if the block is the function entry block, 0 otherwise.
+void __sanitizer_cov_pcs_init(uint8_t* pcs_beg, uint8_t* pcs_end);
+
+/**
+ * Sets the callback to be called right before death on error.
+ * Passing 0 will unset the callback. Called in libfuzzer_driver.cpp.
+ */
+NO_SANITIZE void
+__sanitizer_set_death_callback(void (*callback)(void))
+{
+	/* cleanup(); */
+}
+
+/**
+ * Suppress libFuzzer warnings about missing sanitizer methods in non-sanitizer
+ * builds.
+ */
+NO_SANITIZE int
+__sanitizer_acquire_crash_state(void)
+{
+	return 1;
+}
+
+/**
+ * Print the stack trace leading to this call. Useful for debugging user code.
+ * See:
+ * - https://github.com/keplerproject/lua-compat-5.2/blob/master/c-api/compat-5.2.c#L229
+ * - http://www.lua.org/manual/5.2/manual.html#luaL_traceback
+ */
+NO_SANITIZE void
+__sanitizer_print_stack_trace(void)
+{
+	lua_State *L = get_global_lua_state();
+#if LUA_VERSION_NUM < 502
+	luaL_traceback(L);
+#else
+	luaL_traceback(L, L, "traceback", 3);
+#endif
+}
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+NO_SANITIZE const char *
+GetLibFuzzerSymbolsLocation(void) {
+	Dl_info dl_info;
+	if (!dladdr((void*)&LLVMFuzzerRunDriver, &dl_info)) {
+		return "<Not a shared object>";
+	}
+	return (dl_info.dli_fname);
+}
+
+NO_SANITIZE const char *
+GetCoverageSymbolsLocation(void) {
+	Dl_info dl_info;
+	if (!dladdr((void*)&__sanitizer_cov_8bit_counters_init, &dl_info)) {
+		return "<Not a shared object>";
+	}
+	return (dl_info.dli_fname);
+}
+
+void
+init(void)
+{
+	if (!&LLVMFuzzerRunDriver) {
+		printf("LLVMFuzzerRunDriver symbol not found. This means "
+        "you had an old version of Clang installed when you built luzer.");
+        /* TODO: exit */
+        assert(NULL);
+	}
+
+	if (strcmp(GetCoverageSymbolsLocation(), GetLibFuzzerSymbolsLocation()) != 0) {
+        fprintf(stderr,
+        "WARNING: Coverage symbols are being provided by a library other than "
+        "libFuzzer. This will result in a broken Lua code coverage and "
+        "severely impacted native extension code coverage. Symbols are coming "
+        "from this library: %s"
+        "\nYou can likely resolve this issue by linking libFuzzer into "
+        "Lua directly, and using `atheris_no_libfuzzer` instead of "
+        "`atheris`. See documentation for details.", GetCoverageSymbolsLocation());
+	}
+}
+
+static void
+sig_handler(int sig)
+{
+	switch (sig) {
+	case SIGINT:
+		exit(0);
+		break;
+	case SIGSEGV:
+		__sanitizer_print_stack_trace();
+		break;
+	}
+}
+
+NO_SANITIZE int
+luaL_mutate(lua_State *L)
+{
+	int index = lua_gettop(L);
+	if (index != 4) {
+		luaL_error(L, "required arguments: data, size, max_size, seed");
+	}
+	lua_getglobal(L, CUSTOM_MUTATOR_FUNC);
+	if (lua_isfunction(L, -1) != 1) {
+		luaL_error(L, "no luzer_custom_mutator is defined");
+	}
+	lua_insert(L, 5);
+	lua_call(L, 6, 1);
+
+	if (lua_isstring(L, -1) != 1) {
+		luaL_error(L, "_mutate() must return a string");
+	}
+
+	return 1;
+}
+
+NO_SANITIZE static int
+luaL_set_custom_mutator(lua_State *L)
+{
+	if (lua_isfunction(L, -1) != 1)
+		luaL_error(L, "custom_mutator is not a Lua function.");
+	lua_setglobal(L, CUSTOM_MUTATOR_FUNC);
+
+	return 0;
+}
+
+NO_SANITIZE static int
+luaL_test_one_input(lua_State *L)
+{
+	lua_getglobal(L, TEST_ONE_INPUT_FUNC);
+	if (lua_isfunction(L, -1) != 1) {
+		lua_settop(L, 0);
+		luaL_error(L, "no luzer_test_one_input is defined");
+	}
+	lua_insert(L, -2);
+	lua_call(L, 1, 1);
+
+	int rc = 0;
+	if (lua_isnumber(L, 1) == 1)
+		rc = lua_tonumber(L, 1);
+	lua_settop(L, 0);
+
+	return rc;
+}
+
+NO_SANITIZE int
+TestOneInput(const uint8_t* data, size_t size) {
+	const counter_and_pc_table_range alloc = allocate_counters_and_pcs();
+	if (alloc.counters_start && alloc.counters_end) {
+		__sanitizer_cov_8bit_counters_init(alloc.counters_start,
+										   alloc.counters_end);
+	}
+	if (alloc.pctable_start && alloc.pctable_end) {
+		__sanitizer_cov_pcs_init(alloc.pctable_start, alloc.pctable_end);
+	}
+
+	lua_State *L = get_global_lua_state();
+	char *buf = calloc(size + 1, sizeof(char));
+	memcpy(buf, data, size);
+	buf[size] = '\0';
+	lua_pushlstring(L, buf, size);
+	int rc = luaL_test_one_input(L);
+	free(buf);
+
+	return rc;
+}
+
+NO_SANITIZE static int
+luaL_cleanup(lua_State *L)
+{
+	lua_sethook(L, debug_hook, 0, 0);
+	lua_pushnil(L);
+	lua_setglobal(L, TEST_ONE_INPUT_FUNC);
+	lua_pushnil(L);
+	lua_setglobal(L, DEBUG_HOOK_FUNC);
+	lua_pushnil(L);
+	lua_setglobal(L, CUSTOM_MUTATOR_FUNC);
+	return 0;
+}
+
+NO_SANITIZE static int
+search_module_path(char *so_path, size_t len) {
+	char *lua_cpath = getenv("LUA_CPATH");
+	if (!lua_cpath)
+		lua_cpath = "./";
+	int rc = -1;
+	char *cpath = NULL;
+	while ((cpath = strsep(&lua_cpath, ";")) != NULL) {
+		const char *dir = dirname(cpath);
+		snprintf(so_path, len, "%s/%s", dir, CUSTOM_MUTATOR_LIB);
+		if (access(so_path, F_OK) == 0) {
+			rc = 0;
+			break;
+		}
+	}
+
+	return rc;
+}
+
+/**
+ * We couldn't define custom mutator function in a compile-time,
+ * so we define it in runtime - when user has specified a Lua
+ * function with custom mutator. LibFuzzer uses custom mutator
+ * defined by user when a function LLVMFuzzerCustomMutator has been defined.
+ * We define that function in a shared library and preload it when
+ * user defines a Lua function with custom mutator.
+ * LLVMFuzzerCustomMutator executes a Lua function, mutates portion of data
+ * and returns it back to LibFuzzer. Shared library is located
+ * at the same directory where the main shared library with luzer's
+ * implementation is placed. To search it's location we search
+ * shared library CUSTOM_MUTATOR_LIB in directories listed in
+ * environment variable LUA_CPATH.
+ */
+NO_SANITIZE static int
+load_custom_mutator_lib(void) {
+	char *so_path = calloc(PATH_MAX, sizeof(char));
+	int rc = search_module_path(so_path, PATH_MAX);
+	if (rc) {
+		free(so_path);
+		DEBUG_PRINT("search_module_path");
+		return -1;
+	}
+	void *custom_mutator_lib = dlopen(so_path, RTLD_LAZY);
+	free(so_path);
+	if (!custom_mutator_lib) {
+		DEBUG_PRINT("dlopen");
+		return -1;
+	}
+	void *custom_mutator = dlsym(custom_mutator_lib, "LLVMFuzzerCustomMutator");
+	if (!custom_mutator) {
+		DEBUG_PRINT("dlsym");
+		return -1;
+	}
+	rc = dlclose(custom_mutator_lib);
+	if (rc) {
+		DEBUG_PRINT("dlclose");
+		return -1;
+	}
+	return 0;
+}
+
+NO_SANITIZE static int
+luaL_fuzz(lua_State *L)
+{
+	if (lua_istable(L, -1) == 0) {
+		luaL_error(L, "opts is not a table");
+	}
+	lua_pushnil(L);
+
+	/* Processing a table with options. */
+	int argc = 0;
+	char **argv = malloc(1 * sizeof(char*));
+	if (!argv)
+		luaL_error(L, "not enough memory");
+	const char *corpus_path = NULL;
+	while (lua_next(L, -2) != 0) {
+		char **argvp = realloc(argv, sizeof(char*) * (argc + 1));
+		if (argvp == NULL) {
+			free(argv);
+			luaL_error(L, "not enough memory");
+		}
+		const char *key = lua_tostring(L, -2);
+		const char *value = lua_tostring(L, -1);
+		if (strcmp(key, "corpus") != 0) {
+			size_t arg_len = strlen(key) + strlen(value) + 3;
+			char *arg = calloc(arg_len, sizeof(char));
+			if (!arg)
+				luaL_error(L, "not enough memory");
+			snprintf(arg, arg_len, "-%s=%s", key, value);
+			argvp[argc] = arg;
+			argc++;
+		} else {
+			corpus_path = strdup(value);
+		}
+		lua_pop(L, 1);
+		argv = argvp;
+	}
+	if (corpus_path) {
+		argv[argc] = (char*)corpus_path;
+		argc++;
+	}
+	if (argc == 0) {
+		argv[argc] = "";
+		argc++;
+	}
+	argv[argc] = NULL;
+	lua_pop(L, 1);
+
+#ifdef DEBUG
+	char **p = argv;
+	while(*p++) {
+		if (*p)
+			DEBUG_PRINT("libFuzzer arg - '%s'\n", *p);
+	}
+#endif /* DEBUG */
+
+	/* Processing a function with custom mutator. */
+	if (!lua_isnil(L, -1) && (lua_isfunction(L, -1) == 1)) {
+			if (load_custom_mutator_lib())
+				luaL_error(L, "function LLVMFuzzerCustomMutator is not available");
+			luaL_set_custom_mutator(L);
+	} else {
+		lua_pop(L, 1);
+	}
+
+	/* Processing a function LLVMFuzzerTestOneInput. */
+	if (lua_isfunction(L, -1) != 1) {
+		luaL_error(L, "test_one_input is not a Lua function");
+	}
+	lua_setglobal(L, TEST_ONE_INPUT_FUNC);
+
+	/**
+	 * Hook is called when the Lua interpreter calls a function and when the
+	 * interpreter is about to start the execution of a new line of code, or
+	 * when it jumps back in the code (even to the same line).
+	 * https://www.lua.org/pil/23.2.html
+	 */
+	lua_sethook(L, debug_hook, LUA_MASKCALL | LUA_MASKLINE, 0);
+	lua_pushboolean(L, 1);
+
+	struct sigaction act;
+	act.sa_handler = sig_handler;
+	sigaction(SIGINT, &act, NULL);
+	sigaction(SIGSEGV, &act, NULL);
+
+	lua_getglobal(L, TEST_ONE_INPUT_FUNC);
+	if (lua_isfunction(L, -1) != 1) {
+		luaL_error(L, "test_one_input is not defined");
+	}
+	lua_pop(L, -1);
+
+	set_global_lua_state(L);
+	int rc = LLVMFuzzerRunDriver(&argc, &argv, &TestOneInput);
+	luaL_cleanup(L);
+
+	lua_pushnumber(L, rc);
+
+	return 1;
+}
+
+static const struct luaL_Reg Module[] = {
+	{ "Fuzz", luaL_fuzz },
+	{ "FuzzedDataProvider", luaL_fuzzed_data_provider },
+	{ "_set_custom_mutator", luaL_set_custom_mutator },
+	{ "_mutate", luaL_mutate },
+	{ NULL, NULL }
+};
+
+int luaopen_luzer(lua_State *L)
+{
+	init();
+
+#if LUA_VERSION_NUM == 501
+	luaL_register(L, "luzer", Module);
+#else
+	luaL_newlib(L, Module);
+#endif
+	lua_pushliteral(L, "_VERSION");
+	lua_pushstring(L, luzer_version_string());
+	lua_rawset(L, -3);
+
+	lua_pushliteral(L, "_LLVM_VERSION");
+	lua_pushstring(L, llvm_version_string());
+	lua_rawset(L, -3);
+
+	lua_pushliteral(L, "_LUA_VERSION");
+	lua_pushstring(L, LUA_RELEASE);
+	lua_rawset(L, -3);
+
+	return 1;
+}
blob - /dev/null
blob + 234669009442b8c3ab58624ebec62d59a2b84cdd (mode 644)
--- /dev/null
+++ luzer/luzer.h
@@ -0,0 +1,13 @@
+#ifndef LUZER_MACROS_H_
+#define LUZER_MACROS_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+lua_State *get_global_lua_state();
+int luaL_mutate(lua_State *L);
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif  // LUZER_MACROS_H_
blob - /dev/null
blob + 984ed6829d6733ae2bf7b3b9d942c865339bfbc9 (mode 644)
--- /dev/null
+++ luzer/macros.h
@@ -0,0 +1,41 @@
+#ifndef LUZER_MACROS_H_
+#define LUZER_MACROS_H_
+
+#include <assert.h>
+#include <stdbool.h>
+
+#ifdef DEBUG
+#define DEBUG_PRINT(...) do{ fprintf( stderr, __VA_ARGS__ ); } while( false )
+#else
+#define DEBUG_PRINT(...) do{ } while ( false )
+#endif /* DEBUG */
+
+#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
+#define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
+
+/**
+ * If control flow reaches the point of the unreachable(), the program is
+ * undefined. It is useful in situations where the compiler cannot deduce
+ * the unreachability of the code.
+ */
+#if __has_builtin(__builtin_unreachable) || defined(__GNUC__)
+#  define unreachable() (assert(0), __builtin_unreachable())
+#else
+#  define unreachable() (assert(0))
+#endif
+
+#define NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
+
+#ifdef __has_attribute
+#if __has_attribute(no_sanitize)
+#define NO_SANITIZE_MEMORY __attribute__((no_sanitize("memory")))
+#else
+#define NO_SANITIZE_MEMORY
+#endif  // __has_attribute(no_sanitize)
+#else
+#define NO_SANITIZE_MEMORY
+#endif  // __has_attribute
+
+#define NO_SANITIZE NO_SANITIZE_ADDRESS NO_SANITIZE_MEMORY
+
+#endif  // LUZER_MACROS_H_
blob - /dev/null
blob + f6587371ddb17cb67ea6740fce29f8dd5494d7cb (mode 644)
--- /dev/null
+++ luzer/tests/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(LUA_CPATH "\;${PROJECT_BINARY_DIR}/luzer/?.so\;")
+
+add_test(
+  NAME luzer_unit_test
+  COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_unit.lua
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(luzer_unit_test PROPERTIES
+  ENVIRONMENT "LUA_CPATH='${LUA_CPATH}'"
+)
+
+add_test(
+  NAME luzer_e2e_test
+  COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_e2e.lua
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(luzer_e2e_test PROPERTIES
+  ENVIRONMENT "LUA_CPATH='${LUA_CPATH}'"
+  PASS_REGULAR_EXPRESSION "test_e2e.lua:7: assert has triggered"
+)
+
+add_test(
+  NAME luzer_options_test
+  COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_options.lua
+  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+set_tests_properties(luzer_options_test PROPERTIES
+  ENVIRONMENT "LUA_CPATH='${LUA_CPATH}'"
+  PASS_REGULAR_EXPRESSION "ERROR: The required directory \"undefined\" does not exist"
+)
blob - /dev/null
blob + db7e5fdc3775e4b03159084ccb4860c634ea6eb1 (mode 644)
--- /dev/null
+++ luzer/tests/test_e2e.lua
@@ -0,0 +1,15 @@
+local luzer = require("luzer")
+
+local function TestOneInput(buf)
+    local fdp = luzer.FuzzedDataProvider(buf)
+    local str = fdp:consume_string(1)
+    if str == "c" then
+		assert(nil, "assert has triggered")
+    end
+    return
+end
+
+local opts = {
+    max_len = 4096,
+}
+luzer.Fuzz(TestOneInput, nil, opts)
blob - /dev/null
blob + c6b81656386b76e4e03bdd3539f93333b807d054 (mode 644)
--- /dev/null
+++ luzer/tests/test_options.lua
@@ -0,0 +1,10 @@
+local luzer = require("luzer")
+
+local args = {
+    max_len = 1024,
+    print_pcs = 1,
+    corpus = "undefined",
+    max_total_time = 60,
+    print_final_stats = 1,
+}
+luzer.Fuzz(function() end, nil, args)
blob - /dev/null
blob + 53ca0019ccba07a3bc6ec9483868090c029a6d77 (mode 644)
--- /dev/null
+++ luzer/tests/test_unit.lua
@@ -0,0 +1,203 @@
+local luzer = require("luzer")
+
+local function trace(_, line)
+    local s = debug.getinfo(2).short_src
+    print(s .. ":" .. line)
+end
+
+debug.sethook(trace, "l")
+
+-- luzer._VERSION
+assert(type(luzer._VERSION) == "string")
+assert(type(luzer._LLVM_VERSION) == "string")
+assert(type(luzer._LUA_VERSION) == "string")
+
+local ok
+local err
+local fdp
+local res
+
+-- luzer.FuzzedDataProvider()
+assert(type(luzer.FuzzedDataProvider) == "function")
+ok, err = pcall(luzer.FuzzedDataProvider)
+assert(ok == false)
+assert(err ~= nil)
+fdp = luzer.FuzzedDataProvider(string.rep('A', 1024))
+assert(type(fdp) == "userdata")
+
+-- luzer.FuzzedDataProvider.remaining_bytes()
+fdp = luzer.FuzzedDataProvider("A")
+assert(type(fdp.remaining_bytes) == "function")
+res = fdp:remaining_bytes()
+assert(type(res) == "number")
+assert(res == 1)
+fdp = luzer.FuzzedDataProvider("ABC")
+res = fdp:remaining_bytes()
+assert(type(res) == "number")
+assert(res == 3)
+
+-- luzer.FuzzedDataProvider.consume_string()
+fdp = luzer.FuzzedDataProvider("ABCD")
+assert(type(fdp.consume_string) == "function")
+
+assert(fdp:remaining_bytes() == 4)
+res = fdp:consume_string(2)
+assert(type(res) == "string")
+assert(res == "AB")
+assert(fdp:remaining_bytes() == 2)
+res = fdp:consume_string(2)
+assert(type(res) == "string")
+assert(res == "CD")
+res = fdp:consume_string(2)
+assert(fdp:remaining_bytes() == 0)
+assert(type(res) == "string")
+assert(res == "")
+
+ok = pcall(fdp.consume_string)
+assert(ok == false)
+assert(err ~= nil)
+
+-- luzer.FuzzedDataProvider.consume_strings()
+fdp = luzer.FuzzedDataProvider("ABCDEF")
+assert(type(fdp.consume_strings) == "function")
+
+res = fdp:consume_strings(2, 3)
+assert(type(res) == "table")
+assert(#res == 2, #res)
+assert(fdp:remaining_bytes() == 0)
+
+ok = pcall(fdp.consume_strings)
+assert(ok == false)
+assert(err ~= nil)
+
+-- luzer.FuzzedDataProvider.consume_boolean()
+fdp = luzer.FuzzedDataProvider("AB")
+assert(type(fdp.consume_boolean) == "function")
+
+assert(fdp:remaining_bytes() == 2)
+res = fdp:consume_boolean()
+assert(type(res) == "boolean")
+assert(fdp:remaining_bytes() == 1)
+res = fdp:consume_boolean()
+assert(type(res) == "boolean")
+assert(fdp:remaining_bytes() == 0)
+res = fdp:consume_boolean()
+assert(type(res) == "boolean")
+assert(res == false)
+assert(fdp:remaining_bytes() == 0)
+res = fdp:consume_boolean()
+assert(type(res) == "boolean")
+assert(res == false)
+
+-- luzer.FuzzedDataProvider.consume_booleans()
+fdp = luzer.FuzzedDataProvider("AB")
+assert(type(fdp.consume_booleans) == "function")
+
+res = fdp:consume_booleans(2)
+assert(type(res) == "table")
+assert(type(res[1]) == "boolean")
+assert(type(res[2]) == "boolean")
+assert(fdp:remaining_bytes() == 0)
+res = fdp:consume_booleans(2)
+assert(type(res) == "table")
+assert(res[1] == false)
+assert(res[2] == false)
+
+ok = pcall(fdp.consume_booleans)
+assert(ok == false)
+
+-- luzer.FuzzedDataProvider.consume_number()
+fdp = luzer.FuzzedDataProvider("AB")
+assert(type(fdp.consume_number) == "function")
+
+res = fdp:consume_number(1, 10)
+assert(type(res) == "number")
+assert(res >= 1)
+assert(res <= 10, res)
+
+ok, err = pcall(fdp.consume_number)
+assert(ok == false)
+assert(err ~= nil)
+
+-- luzer.FuzzedDataProvider.consume_numbers()
+fdp = luzer.FuzzedDataProvider("ABCDEF")
+assert(type(fdp.consume_numbers) == "function")
+
+res = fdp:consume_numbers(2, 1, 3)
+assert(type(res) == "table")
+assert(type(res[1]) == "number")
+assert(type(res[2]) == "number")
+assert(res[3] == nil, res[3])
+
+ok, err = pcall(fdp.consume_numbers, fdp)
+assert(ok == false)
+assert(err ~= nil)
+
+-- luzer.FuzzedDataProvider.consume_integer()
+fdp = luzer.FuzzedDataProvider("AB")
+assert(type(fdp.consume_integer) == "function")
+
+res = fdp:consume_integer(10, 20)
+assert(type(res) == "number")
+assert(res >= 10)
+assert(res <= 20)
+
+ok, err = pcall(fdp.consume_integer)
+assert(ok == false)
+assert(err ~= nil)
+
+-- luzer.FuzzedDataProvider.consume_integers()
+fdp = luzer.FuzzedDataProvider("AB")
+assert(type(fdp.consume_integers) == "function")
+
+local min = 1
+local max = 6
+res = fdp:consume_integers(1, min, max)
+assert(type(res) == "table")
+assert(type(res[1]) == "number")
+assert(res[1] <= max, res[1])
+assert(res[1] >= min, res[1])
+assert(res[2] == nil)
+
+ok, err = pcall(fdp.consume_integers)
+assert(ok == false)
+assert(err ~= nil)
+
+-- luzer.FuzzedDataProvider.consume_probability()
+fdp = luzer.FuzzedDataProvider("AB")
+assert(type(fdp.consume_probability) == "function")
+
+local p1 = fdp:consume_probability()
+local p2 = fdp:consume_probability()
+assert(type(p1) == "number")
+assert(type(p2) == "number")
+assert(p1 >= 0 and p2 >= 0)
+assert(p1 <= 1 and p2 <= 1)
+assert(p1 ~= p2)
+
+local function custom_mutator(data, size, max_size, seed)
+    assert(type(data) == "string")
+    assert(type(size) == "number")
+    assert(size == #data)
+    assert(type(max_size) == "number")
+    assert(max_size == size)
+    assert(type(seed) == "number")
+    return data
+end
+
+-- luzer._set_custom_mutator()
+assert(luzer_custom_mutator == nil)
+luzer._set_custom_mutator(custom_mutator)
+assert(luzer_custom_mutator ~= nil)
+assert(type(luzer_custom_mutator) == "function")
+local buf = "data"
+assert(luzer_custom_mutator(buf, #buf, #buf, math.random(1, 10)) == buf)
+luzer_custom_mutator = nil -- Clean up.
+
+-- luzer._mutate()
+luzer._set_custom_mutator(custom_mutator)
+assert(luzer_custom_mutator ~= nil)
+-- luzer._mutate(buf, #buf, #buf, math.random(1, 10)) -- TODO
+luzer_custom_mutator = nil -- Clean up.
+
+print("Success!")
blob - /dev/null
blob + 793c5d733ef0a06be24d96eae9e628f590742ee3 (mode 644)
--- /dev/null
+++ luzer/tracer.c
@@ -0,0 +1,71 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright 2022-2023, Sergey Bronnikov
+ */
+
+/**
+ * SanitizerCoverage
+ * https://clang.llvm.org/docs/SanitizerCoverage.html
+ *
+ * SanCov: Above and Below the Sanitizer Interface
+ * https://calabi-yau.space/blog/sanitizer-coverage-interface.html
+ *
+ * Jazzer:
+ * jazzer/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp
+ * jazzer/src/main/native/com/code_intelligence/jazzer/jazzer_preload.c
+ *
+ * Atheris:
+ * atheris/src/native/core.cc
+ * atheris/src/native/counters.cc
+ */
+
+#include <lua.h>
+#include <stdint.h>
+#include <string.h> /* strlen */
+
+#include "counters.h"
+#include "macros.h"
+
+/**
+ * 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)
+
+NO_SANITIZE void
+_trace_branch(uint64_t idx)
+{
+	increment_counter(idx);
+}
+
+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;
+}
+
+/**
+ * luzer gathers coverage using a debug hook, and patches coroutine
+ * library to set it on created threads when under standard Lua, where each
+ * coroutine has its own hook. If a coroutine is created using Lua C API
+ * or before the monkey-patching, this wrapper should be applied to the
+ * main function of the coroutine. Under LuaJIT this function is redundant,
+ * as there is only one, global debug hook.
+ *
+ * https://github.com/lunarmodules/luacov/blob/master/src/luacov/runner.lua#L102-L117
+ * https://github.com/lunarmodules/luacov/blob/78f3d5058c65f9712e6c50a0072ad8160db4d00e/src/luacov/runner.lua#L439-L450
+ */
+void debug_hook(lua_State *L, lua_Debug *ar)
+{
+	lua_getinfo(L, "Sln", ar);
+	if (ar && ar->source && ar->currentline) {
+		const unsigned int new_location = lhash(ar->source, ar->currentline);
+		_trace_branch(new_location);
+	}
+}
blob - /dev/null
blob + 232d4a3b20e14ecab3a4d55c388835d037ed36f8 (mode 644)
--- /dev/null
+++ luzer/tracer.h
@@ -0,0 +1,6 @@
+#ifndef LUZER_TRACER_H_
+#define LUZER_TRACER_H_
+
+void debug_hook(lua_State *L, lua_Debug *ar);
+
+#endif  // LUZER_TRACER_H_
blob - /dev/null
blob + dc593020abaf976a4eed9982743a9b901a2bfb35 (mode 644)
--- /dev/null
+++ luzer/version.c
@@ -0,0 +1,7 @@
+const char *llvm_version_string(void) {
+	return "@LLVM_VERSION@";
+}
+
+const char *luzer_version_string(void) {
+	return "@CMAKE_PROJECT_VERSION@";
+}
blob - /dev/null
blob + 5023ec08bd491d659aa495c4531f8596419eb9b0 (mode 644)
--- /dev/null
+++ luzer/version.h
@@ -0,0 +1,7 @@
+#ifndef LUZER_VERSION_H_
+#define LUZER_VERSION_H_
+
+const char *llvm_version_string(void);
+const char *luzer_version_string(void);
+
+#endif /* LUZER_VERSION_H_ */
blob - /dev/null
blob + 6caabeca3f436beb8c1ffe5fb8e5df74e8c0b854 (mode 644)
--- /dev/null
+++ luzer-scm-1.rockspec
@@ -0,0 +1,33 @@
+package = "luzer"
+version = "scm-1"
+source = {
+    url = "git+https://github.com/ligurio/luzer",
+    branch = "master",
+}
+
+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. ]],
+    homepage = "https://github.com/ligurio/luzer",
+    maintainer = "Sergey Bronnikov <estetus@gmail.com>",
+    license = "ISC",
+}
+
+dependencies = {
+    "lua >= 5.1",
+}
+
+build = {
+    type = "cmake",
+    -- https://github.com/luarocks/luarocks/wiki/Config-file-format#variables
+    variables = {
+        CMAKE_LUADIR = "$(LUADIR)",
+        CMAKE_LIBDIR = "$(LIBDIR)",
+        CMAKE_BUILD_TYPE = "RelWithDebInfo",
+        CMAKE_C_COMPILER = "clang",
+        CMAKE_CXX_COMPILER = "clang++",
+    },
+}