Skip to content

Commit e62118b

Browse files
committed
Support integration with luzer
The patch enables using luzer for fuzzing Lua projects in OSS-Fuzz. Usage: sudo python infra/helper.py build_fuzzers lua-example sudo python infra/helper.py check_build lua-example fuzz_basic sudo python infra/helper.py run_fuzzer lua-example fuzz_basic Closes #13782
1 parent 3775fff commit e62118b

File tree

11 files changed

+348
-0
lines changed

11 files changed

+348
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
layout: default
3+
title: Integrating a Lua project
4+
parent: Setting up a new project
5+
grand_parent: Getting started
6+
nav_order: 4
7+
permalink: /getting-started/new-project-guide/lua-lang/
8+
---
9+
10+
# Integrating a Lua project
11+
{: .no_toc}
12+
13+
- TOC
14+
{:toc}
15+
---
16+
17+
The process of integrating a project written in Lua with OSS-Fuzz is very
18+
similar to the general [Setting up a new project]({{ site.baseurl
19+
}}/getting-started/new-project-guide/) process. The key specifics of
20+
integrating a Lua project are outlined below.
21+
22+
## luzer
23+
24+
Lua fuzzing in OSS-Fuzz is powered by
25+
[luzer](https://github.com/ligurio/luzer), which is installed during the build
26+
step. As luzer operates directly on the Lua source code level, it can be
27+
applied to any project written in a language that can be transpiled into Lua
28+
such as [MoonScript](https://moonscript.org/),
29+
[TypeScriptToLua](https://typescripttolua.github.io/),
30+
[Fennel](https://fennel-lang.org/), and [Urn](https://urn-lang.com/). More
31+
information on how luzer fuzz targets looks like can be found in its [README's
32+
Quickstart section](https://github.com/ligurio/luzer#quickstart).
33+
34+
## Project files
35+
36+
### Example project
37+
38+
We recommend viewing
39+
[lua-example](https://github.com/google/oss-fuzz/tree/master/projects/lua-example)
40+
as an example of a simple Lua fuzzing project. This example also demonstrates
41+
how to use luzer's fuzzed data provider.
42+
43+
### project.yaml
44+
45+
The `language` attribute must be specified as follows:
46+
47+
```yaml
48+
language: lua
49+
```
50+
51+
The only supported fuzzing engine is libFuzzer (`libfuzzer`). The supported
52+
sanitizers are AddressSanitizer (`address`) and
53+
UndefinedBehaviorSanitizer (`undefined`). These must be explicitly specified.
54+
(`none`) is the default sanitizer for Lua projects, so setting it up in
55+
`project.yaml` is optional.
56+
57+
```yaml
58+
fuzzing_engines:
59+
- libfuzzer
60+
sanitizers:
61+
- none
62+
```
63+
64+
### Dockerfile
65+
66+
The Dockerfile should start by `FROM gcr.io/oss-fuzz-base/base-builder`.
67+
68+
The OSS-Fuzz base Docker images come without any pre-installed components
69+
required for Lua fuzzing. Apart from that, you should usually need to install
70+
Lua runtime, luzer module, clone the project, set a `WORKDIR`, and copy any
71+
necessary files, or install any project-specific dependencies here as you normally would.
72+
73+
### Fuzzers
74+
75+
In the simplest case, every fuzzer consists of a single Lua file that defines
76+
a function `TestOneInput` and executes a function named `luzer.Fuzz()`.
77+
An example fuzz target could thus be a file `fuzz_basic.lua` with contents:
78+
79+
```lua
80+
local parser = require("src.luacheck.parser")
81+
local decoder = require("luacheck.decoder")
82+
local luzer = require("luzer")
83+
84+
local function TestOneInput(buf)
85+
parser.parse(decoder.decode(buf))
86+
end
87+
88+
local args = {
89+
print_final_stats = 1,
90+
}
91+
luzer.Fuzz(TestOneInput, nil, args)
92+
```
93+
94+
### build.sh
95+
96+
The OSS-Fuzz base docker image for Lua comes with the [`compile_lua_fuzzer`
97+
script](https://github.com/google/oss-fuzz/blob/master/infra/base-images/base-builder/compile_lua_fuzzer)
98+
preinstalled. In `build.sh`, you should install dependencies for your project,
99+
and if necessary compile the code into Lua. Then, you can use the script to
100+
build the fuzzers. The script ensures that
101+
[luzer](https://luarocks.org/modules/ligurio/luzer) is installed so that its
102+
CLI can be used to execute your fuzz tests. It also generates a wrapper script
103+
that can be used as a drop-in replacement for libFuzzer. This means that the
104+
generated script accepts the same command line flags for libFuzzer. Under the
105+
hood these flags are simply forwarded to the libFuzzer native addon used by
106+
luzer.
107+
108+
A usage example from the lua-example project is
109+
110+
```shell
111+
compile_lua_fuzzer lua lua-example fuzz_basic.lua
112+
```
113+
114+
Arguments are:
115+
116+
* a Lua runtime name
117+
* relative path of the project in the $SRC directory
118+
* relative path to the fuzz test inside the project
119+
120+
The [lua-example](https://github.com/google/oss-fuzz/blob/master/projects/lua-example/build.sh)
121+
project contains an example of a `build.sh` for a Lua projects.
122+
123+
## FuzzedDataProvider
124+
125+
luzer provides a `FuzzedDataProvider` that can simplify the task of creating a
126+
fuzz target by translating the raw input bytes received from the fuzzer into
127+
useful primitive Lua types. Its functionality is similar to
128+
`FuzzedDataProviders` available in other languages, such as
129+
[Java](https://codeintelligencetesting.github.io/jazzer-docs/jazzer-api/com/code_intelligence/jazzer/api/FuzzedDataProvider.html) and
130+
[C++](https://github.com/google/fuzzing/blob/master/docs/split-inputs.md).
131+
132+
A fuzz target using the `FuzzedDataProvider` would look as follows:
133+
134+
```lua
135+
local luzer = require("luzer")
136+
137+
local function TestOneInput(buf)
138+
local fdp = luzer.FuzzedDataProvider(buf)
139+
local str = fdp:consume_string(4)
140+
141+
local b = {}
142+
str:gsub(".", function(c) table.insert(b, c) end)
143+
local count = 0
144+
if b[1] == "o" then count = count + 1 end
145+
if b[2] == "o" then count = count + 1 end
146+
if b[3] == "p" then count = count + 1 end
147+
if b[4] == "s" then count = count + 1 end
148+
149+
if count == 4 then assert(nil) end
150+
end
151+
152+
local args = {
153+
only_ascii = 1,
154+
print_pcs = 1,
155+
}
156+
157+
luzer.Fuzz(TestOneInput, nil, args)
158+
```

docs/getting-started/new_project_guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Programming language the project is written in. Values you can specify include:
102102
* [`jvm` (Java, Kotlin, Scala and other JVM-based languages)]({{ site.baseurl }}//getting-started/new-project-guide/jvm-lang/)
103103
* [`swift`]({{ site.baseurl }}//getting-started/new-project-guide/swift-lang/)
104104
* [`javascript`]({{ site.baseurl }}//getting-started/new-project-guide/javascript-lang/)
105+
* [`lua`]({{ site.baseurl }}//getting-started/new-project-guide/lua-lang/)
105106

106107
### primary_contact, auto_ccs {#primary}
107108
The primary contact and list of other contacts to be CCed. Each person listed gets access to ClusterFuzz, including crash reports and fuzzer statistics, and are auto-cced on new bugs filed in the OSS-Fuzz

infra/base-images/base-runner/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ ENV PATH="$PATH:/usr/local/rvm/rubies/ruby-3.3.1/bin"
118118
ENV GEM_HOME="$OUT/fuzz-gem"
119119
ENV GEM_PATH="/install/ruzzy"
120120

121+
RUN apt-get update && apt-get install -y luarocks
122+
121123
# Do this last to make developing these files easier/faster due to caching.
122124
COPY bad_build_check \
123125
coverage \

infra/base-images/base-runner/bad_build_check

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ function check_mixed_sanitizers {
330330
return 0
331331
fi
332332

333+
if [ "${FUZZING_LANGUAGE:-}" = "lua" ]; then
334+
# Sanitizer runtime is loaded via LD_PRELOAD, so this check does not apply.
335+
return 0
336+
fi
337+
333338
# For fuzztest fuzzers point to the binary instead of launcher script.
334339
if [[ $FUZZER == *"@"* ]]; then
335340
FUZZER=(${FUZZER//@/ }[0])
@@ -426,6 +431,10 @@ function check_architecture {
426431
FUZZER=${FUZZER}.pkg
427432
fi
428433

434+
if [ "${FUZZING_LANGUAGE:-}" = "lua" ]; then
435+
return 0;
436+
fi
437+
429438
# For fuzztest fuzzers point to the binary instead of launcher script.
430439
if [[ $FUZZER == *"@"* ]]; then
431440
FUZZER=(${FUZZER//@/ }[0])

infra/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
'go',
2828
'javascript',
2929
'jvm',
30+
'lua',
3031
'python',
3132
'rust',
3233
'swift',

projects/lua-example/Dockerfile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright 2023-2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
################################################################################
16+
17+
FROM gcr.io/oss-fuzz-base/base-builder
18+
19+
RUN apt-get update && apt-get install -y build-essential make
20+
21+
COPY build.sh $SRC/
22+
COPY compile_lua_fuzzer $SRC/
23+
COPY luzer-scm-1.rockspec $SRC/
24+
25+
# For real projects, you would clone your repo in the next step.
26+
RUN mkdir -p $SRC/example
27+
28+
# Ideally, you have already configured fuzz tests in your repo so
29+
# that they run (in luzer regression mode) as part of unit testing.
30+
# Keeping the fuzz tests in sync with the source code ensures that
31+
# they are adjusted continue to work after code changes. Here,
32+
# we copy them into the example project directory.
33+
COPY example_basic.lua $SRC/example/fuzz_basic.lua
34+
35+
WORKDIR $SRC/example

projects/lua-example/build.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash -eu
2+
# Copyright 2023-2025 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
################################################################################
17+
18+
# Install dependencies.
19+
apt install -y liblua5.1-0 liblua5.1-0-dev lua5.1 cmake luarocks \
20+
clang-18 libclang-rt-18-dev libc++-18-dev
21+
22+
# Install Lua dependencies.
23+
# luarocks install --tree=lua_modules --server=https://luarocks.org/dev luzer
24+
# TODO: A custom rockspec is used because custom branch is required,
25+
# see https://github.com/ligurio/luzer/issues/63.
26+
luarocks install --tree=lua_modules $SRC/luzer-scm-1.rockspec
27+
zip -r lua_modules.zip lua_modules
28+
29+
LUA_RUNTIME_NAME=lua
30+
31+
# Build fuzzers.
32+
$SRC/compile_lua_fuzzer lua-example $LUA_RUNTIME_NAME fuzz_basic.lua
33+
34+
cp fuzz_basic.lua "$OUT/"
35+
cp /usr/bin/lua5.1 "$OUT/$LUA_RUNTIME_NAME"
36+
cp lua_modules.zip "$OUT/"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash -eu
2+
# Copyright 2025 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
################################################################################
17+
18+
project=$1
19+
# Lua runtime name.
20+
lua_runtime=$2
21+
# Path the fuzz target source file relative to the project's root.
22+
fuzz_target=$3
23+
24+
fuzzer_basename=$(basename -s .lua "$fuzz_target")
25+
26+
# Create an execution wrapper that executes luzer with the correct
27+
# arguments.
28+
echo "#!/bin/bash
29+
30+
# LLVMFuzzerTestOneInput so that the wrapper script is recognized
31+
# as a fuzz target for 'check_build'.
32+
project_dir=\$(dirname \"\$0\")
33+
eval \$(luarocks --tree lua_modules path)
34+
LD_PRELOAD=\$project_dir/lua_modules/lib/lua/5.1/libfuzzer_with_asan.so \
35+
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$project_dir/llvm-symbolizer:detect_leaks=0 \
36+
\$project_dir/$lua_runtime \$project_dir/$fuzz_target \$@" > "$OUT/$fuzzer_basename"
37+
38+
chmod +x "$OUT/$fuzzer_basename"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
local luzer = require("luzer")
2+
3+
local function TestOneInput(buf)
4+
local fdp = luzer.FuzzedDataProvider(buf)
5+
local str = fdp:consume_string(4)
6+
7+
local b = {}
8+
str:gsub(".", function(c) table.insert(b, c) end)
9+
local count = 0
10+
if b[1] == "o" then count = count + 1 end
11+
if b[2] == "o" then count = count + 1 end
12+
if b[3] == "p" then count = count + 1 end
13+
if b[4] == "s" then count = count + 1 end
14+
15+
if count == 4 then assert(nil) end
16+
end
17+
18+
local args = {
19+
only_ascii = 1,
20+
print_pcs = 1,
21+
}
22+
23+
luzer.Fuzz(TestOneInput, nil, args)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package = "luzer"
2+
version = "scm-1"
3+
source = {
4+
url = "git+https://github.com/ligurio/luzer",
5+
branch = "ligurio/oss-fuzz",
6+
}
7+
8+
description = {
9+
summary = "A coverage-guided, native Lua fuzzer",
10+
detailed = [[ luzer is a coverage-guided Lua fuzzing engine. It supports
11+
fuzzing of Lua code, but also C extensions written for Lua. Luzer is based off
12+
of libFuzzer. When fuzzing native code, luzer can be used in combination with
13+
Address Sanitizer or Undefined Behavior Sanitizer to catch extra bugs. ]],
14+
homepage = "https://github.com/ligurio/luzer",
15+
maintainer = "Sergey Bronnikov <[email protected]>",
16+
license = "ISC",
17+
}
18+
19+
dependencies = {
20+
"lua >= 5.1",
21+
}
22+
23+
build = {
24+
type = "cmake",
25+
-- luacheck: push no max_comment_line_length
26+
-- https://github.com/luarocks/luarocks/blob/7ed653f010671b3a7245be9adcc70068c049ef68/docs/config_file_format.md#config-file-format
27+
-- luacheck: pop
28+
variables = {
29+
CMAKE_LUADIR = "$(LUADIR)",
30+
CMAKE_LIBDIR = "$(LIBDIR)",
31+
CMAKE_BUILD_TYPE = "RelWithDebInfo",
32+
CMAKE_C_COMPILER = "clang-18",
33+
CMAKE_CXX_COMPILER = "clang++-18",
34+
},
35+
}

0 commit comments

Comments
 (0)