A coverage-guided fuzzer for pure Ruby code and Ruby C extensions.
Ruzzy is heavily inspired by Google's Atheris, a Python fuzzer. Like Atheris, Ruzzy uses libFuzzer for its coverage instrumentation and fuzzing engine. Ruzzy also supports AddressSanitizer and UndefinedBehaviorSanitizer when fuzzing C extensions. If you'd like to learn more about the inspiration behind Ruzzy, see our paper: Design and Implementation of a Coverage-Guided Ruby Fuzzer.
Table of contents:
Currently, Ruzzy only supports Linux x86-64 and AArch64/ARM64. If you'd like to run Ruzzy on a Mac or Windows, you can build the Dockerfile
and/or use the development environment. Ruzzy requires a recent version of clang
(tested back to 14.0.0
), preferably the latest release.
Install Ruzzy with the following command:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
gem install ruzzy
There's a lot going on here, so let's break it down:
- The
MAKE
environment variable overrides themake
command when compiling the Ruzzy C extension. This tellsmake
to respect subsequent environment variables when compiling the extension. - The rest of the environment variables are used during compilation to ensure we're using the proper
clang
binaries. This ensures we have the latestclang
features, which are necessary for proper fuzzing.
If you run into issues installing, then you can run the following command to get debugging output:
RUZZY_DEBUG=1 gem install --verbose ruzzy
Ruzzy includes a toy example to demonstrate how it works. First, set the following environment variable:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
Understanding these options isn't necessary, but if you're curious click here.
- Memory allocation failures are common and low impact (DoS), so skip them for now.
- Like Python, the Ruby interpreter leaks data, so ignore these for now.
- Ruby recommends disabling sigaltstack.
You can then run the example with the following command:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOAD
is required for the same reasons as Atheris. However, unlike ASAN_OPTIONS
, you probably do not want to export
it as it may interfere with other programs.
It should quickly produce a crash like the following:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2527961537
...
==45==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c0009bab80 at pc 0xffff99ea1b44 bp 0xffffce8a67d0 sp 0xffffce8a67c8
...
SUMMARY: AddressSanitizer: heap-use-after-free /var/lib/gems/3.1.0/gems/ruzzy-0.7.0/ext/dummy/dummy.c:18:24 in _c_dummy_test_one_input
...
==45==ABORTING
MS: 4 EraseBytes-CopyPart-CopyPart-ChangeBit-; base unit: 410e5346bca8ee150ffd507311dd85789f2e171e
0x48,0x49,
HI
artifact_prefix='./'; Test unit written to ./crash-253420c1158bc6382093d409ce2e9cff5806e980
Base64: SEk=
We can see that it correctly found the input ("HI"
) that produced a memory violation. For more information, see dummy.c
to see why this violation occurred.
You can re-run the crash case with the following command:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
The following sanitizers are available:
Ruzzy::ASAN_PATH
for AddressSanitizerRuzzy::UBSAN_PATH
for UndefinedBehaviorSanitizer
Let's fuzz a small Ruby script as an example. Fuzzing pure Ruby code requires two Ruby scripts: a tracer script and a fuzzing harness. The tracer script is required due to an implementation detail of the Ruby interpreter. Understanding the details of this interaction, other than the fact that it's necessary, is not required.
First, the tracer script, let's call it test_tracer.rb
:
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
Next, the fuzzing harness, let's call it test_harness.rb
:
# frozen_string_literal: true
require 'ruzzy'
def fuzzing_target(input)
if input.length == 4
if input[0] == 'F'
if input[1] == 'U'
if input[2] == 'Z'
if input[3] == 'Z'
raise
end
end
end
end
end
end
test_one_input = lambda do |data|
fuzzing_target(data) # Your fuzzing target would go here
return 0
end
Ruzzy.fuzz(test_one_input)
You can run this file and start fuzzing with the following command:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
It should quickly produce a crash like the following:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2311041000
...
/app/ruzzy/bin/test_harness.rb:12:in `block in <top (required)>': unhandled exception
from /var/lib/gems/3.1.0/gems/ruzzy-0.7.0/lib/ruzzy.rb:15:in `c_fuzz'
from /var/lib/gems/3.1.0/gems/ruzzy-0.7.0/lib/ruzzy.rb:15:in `fuzz'
from /app/ruzzy/bin/test_harness.rb:35:in `<top (required)>'
from bin/test_tracer.rb:7:in `require_relative'
from bin/test_tracer.rb:7:in `<main>'
...
SUMMARY: libFuzzer: fuzz target exited
MS: 1 CopyPart-; base unit: 24b4b428cf94c21616893d6f94b30398a49d27cc
0x46,0x55,0x5a,0x5a,
FUZZ
artifact_prefix='./'; Test unit written to ./crash-aea2e3923af219a8956f626558ef32f30a914ebc
Base64: RlVaWg==
We can see that it correctly found the input ("FUZZ"
) that produced an exception.
To fuzz your own target, modify the test_one_input
lambda
to call your target function.
Let's fuzz the msgpack-ruby
library as an example. First, install the gem:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
CFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
CXXFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
gem install msgpack
In addition to the environment variables used when compiling Ruzzy, we're specifying CFLAGS
and CXXFLAGS
. These flags aid in the fuzzing process. They enable helpful functionality like an address sanitizer, and improved stack trace information. For more information see AddressSanitizerFlags.
Next, we need a fuzzing harness for msgpack
. The following may be familiar to those with libFuzzer experience:
# frozen_string_literal: true
require 'msgpack'
require 'ruzzy'
test_one_input = lambda do |data|
begin
MessagePack.unpack(data)
rescue Exception
# We're looking for memory corruption, not Ruby exceptions
end
return 0
end
Ruzzy.fuzz(test_one_input)
Let's call this file fuzz_msgpack.rb
. You can run this file and start fuzzing with the following command:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
libFuzzer options can be passed to the Ruby script like so:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
See libFuzzer options for more information.
To fuzz your own target, modify the test_one_input
lambda
to call your target function.
Bugs found using Ruzzy:
Development can be done locally, or using the Dockerfile
provided in this repository.
You can build the Ruzzy Docker image with the following command:
docker build --tag ruzzy .
Then, you can shell into the container using the following command:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
We use rake-compiler
to compile Ruzzy's C extensions.
You can compile the C extensions within the container with the following command:
rake compile
We use rake
unit tests to test Ruby code.
You can run the tests within the container with the following command:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
We use rubocop
to lint Ruby code.
You can run rubocop
within the container with the following command:
rubocop
Ruzzy is automatically released to RubyGems when a new git tag is pushed.
To release a new version run the following commands:
git tag vX.X.X
git push --tags
- Ruby C extensions
- https://guides.rubygems.org/gems-with-extensions/
- https://www.rubyguides.com/2018/03/write-ruby-c-extension/
- https://rubyreferences.github.io/rubyref/advanced/extensions.html
- https://silverhammermba.github.io/emberb/c/
- https://ruby-doc.org/3.3.0/extension_rdoc.html
- https://ruby-doc.org/3.3.0/stdlibs/mkmf/MakeMakefile.html
- https://github.com/flavorjones/ruby-c-extensions-explained
- https://github.com/ruby/ruby/blob/v3_3_0/lib/mkmf.rb
- Ruby fuzzing
- Atheris
- https://github.com/google/atheris/blob/master/native_extension_fuzzing.md
- https://security.googleblog.com/2020/12/how-atheris-python-fuzzer-works.html
- https://github.com/google/atheris/blob/2.3.0/setup.py
- https://github.com/google/atheris/blob/2.3.0/src/native/core.cc
- https://github.com/google/atheris/blob/2.3.0/src/native/tracer.cc
- https://github.com/google/atheris/blob/2.3.0/src/native/counters.cc
- https://github.com/google/atheris/blob/2.3.0/src/instrument_bytecode.py
- Coverage
- https://calabi-yau.space/blog/sanitizer-coverage-interface.html
- https://carstein.github.io/2020/05/21/writing-simple-fuzzer-4.html
- https://h0mbre.github.io/Fuzzing-Like-A-Caveman-5/
- https://github.com/mirrorer/afl/blob/master/docs/technical_details.txt
- https://lcamtuf.coredump.cx/afl/historical_notes.txt
- https://www.code-intelligence.com/blog/the-magic-behind-feedback-based-fuzzing
- https://blog.includesecurity.com/2024/04/coverage-guided-fuzzing-extending-instrumentation/
- https://git.sr.ht/~myrrc/ba-thesis/blob/master/thesis.pdf
- https://www.politesi.polimi.it/bitstream/10589/173614/3/2021_04_Frighetto.pdf
- https://wcventure.github.io/FuzzingPaper/Paper/SP18_ColLAFL.pdf
- https://www.ndss-symposium.org/wp-content/uploads/2020/02/24422.pdf
- https://mboehme.github.io/paper/ICSE22.pdf
- https://www.usenix.org/system/files/raid2019-wang-jinghan.pdf