commit e52d181485a11b13f0951adb604e18be363ab8c9 from: Sergey Bronnikov via: Sergey Bronnikov date: Wed Feb 08 15:52:59 2023 UTC Initial version 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//lib//, +# 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 +#include +#include +#include +#include + +#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 +#include +#include +#include +#include + +#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 +#include +#include +#include +#include + +#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: :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: :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: :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: :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: :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: :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: :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: :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: :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: :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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 ""; + } + 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 ""; + } + 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 +#include + +#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 +#include +#include /* 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 ", + 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++", + }, +}