Skip to content

Commit

Permalink
Move Dockerfile logic into extconf.rb
Browse files Browse the repository at this point in the history
This change is in preparation for building a proper gem that can be
installed from RubyGems.
  • Loading branch information
mschwager committed Feb 1, 2024
1 parent 1a003f5 commit ac30448
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 53 deletions.
29 changes: 6 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,11 @@ RUN wget -q -O $CLANG_FILE $CLANG_URL && \
tar xf $CLANG_FILE -C $CLANG_DIR --strip-components 1 && \
rm $CLANG_FILE

# https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
ENV CC="$CLANG_DIR/bin/clang"
ENV CXX="$CLANG_DIR/bin/clang++"
ENV LDSHARED="$CLANG_DIR/bin/clang -shared"
ENV LDSHAREDXX="$CLANG_DIR/bin/clang++ -shared"
ENV ASAN_SYMBOLIZER_PATH="$CLANG_DIR/bin/llvm-symbolizer"
ENV PATH="$PATH:$CLANG_DIR/bin"

ENV FUZZER_NO_MAIN_LIB="$CLANG_DIR/lib/clang/17/lib/$CLANG_ARCH-unknown-linux-gnu/libclang_rt.fuzzer_no_main.a"
ENV ASAN_LIB="$CLANG_DIR/lib/clang/17/lib/$CLANG_ARCH-unknown-linux-gnu/libclang_rt.asan.a"
ENV ASAN_STRIPPED_LIB="/tmp/libclang_rt.asan.a"
ENV ASAN_MERGED_LIB="/tmp/asan_with_fuzzer.so"

# https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#why-this-is-necessary
RUN cp "$ASAN_LIB" "$ASAN_STRIPPED_LIB"
RUN ar d "$ASAN_STRIPPED_LIB" asan_preinit.cc.o asan_preinit.cpp.o
RUN "$CXX" \
-Wl,--whole-archive \
"$FUZZER_NO_MAIN_LIB" \
"$ASAN_STRIPPED_LIB" \
-Wl,--no-whole-archive \
-lpthread -ldl -shared \
-o "$ASAN_MERGED_LIB"
ENV CC="clang"
ENV CXX="clang++"
ENV LDSHARED="clang -shared"
ENV LDSHAREDXX="clang++ -shared"

# The MAKE variable allows overwriting the make command at runtime. This forces the
# Ruby C extension to respect ENV variables when compiling, like CC, CFLAGS, etc.
Expand All @@ -71,7 +53,8 @@ WORKDIR ruzzy/
RUN bundler3.1 install

COPY . .
RUN rake compile
RUN gem build
RUN gem install --verbose ruzzy-*.gem

ENTRYPOINT ["./entrypoint.sh"]
CMD ["-help=1"]
9 changes: 6 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ require 'rake/testtask'
require 'rake/extensiontask'

Rake::TestTask.new do |t|
require 'ruzzy'

# This is required for tests that use cruzzy functionality
ENV['LD_PRELOAD'] = ENV['ASAN_MERGED_LIB']
ENV['LD_PRELOAD'] = Ruzzy.ext_path + '/' + 'asan_with_fuzzer.so'

t.verbose = true
end

Rake::ExtensionTask.new 'cruzzy' do |ext|
ext.lib_dir = 'lib/cruzzy'
end

Rake::ExtensionTask.new 'cruzzy/dummy' do |ext|
ext.lib_dir = 'lib/cruzzy/dummy'
Rake::ExtensionTask.new 'dummy' do |ext|
ext.lib_dir = 'lib/dummy'
end
2 changes: 1 addition & 1 deletion bin/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

require 'ruzzy'

dummy_test_one_input = ->(data) { Ruzzy.c_dummy_test_one_input(data) }
dummy_test_one_input = ->(data) { Ruzzy.dummy_test_one_input(data) }

Ruzzy.fuzz(dummy_test_one_input)
5 changes: 2 additions & 3 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/bin/bash

export LD_PRELOAD=${ASAN_MERGED_LIB}

ruby -Ilib bin/dummy.rb "$@"
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy.ext_path')/asan_with_fuzzer.so \
ruby bin/dummy.rb "$@"
76 changes: 65 additions & 11 deletions ext/cruzzy/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,80 @@

require 'mkmf'
require 'open3'
require 'tempfile'

lib_name = 'libclang_rt.fuzzer_no_main.a'
env_name = 'FUZZER_NO_MAIN_LIB'
clang_name = 'clang'
CLANG = 'clang'
FUZZER_NO_MAIN_LIB = 'FUZZER_NO_MAIN_LIB'

lib_path, status = Open3.capture2(clang_name, "--print-file-name", lib_name)
find_executable(CLANG)

if !status.success?
puts("Could not find #{lib_name} using #{clang_name}.")
lib_path = ENV.fetch(env_name, nil)
if lib_path.nil?
puts("Could not find #{lib_name} in #{env_name}.")
puts("Please include #{clang_name} in your path or specify #{env_name} ENV variable.")
def get_clang_file_name(file_name)
stdout, status = Open3.capture2(CLANG, '--print-file-name', file_name)
status.success? && File.exist?(stdout.strip) ? stdout.strip : false
end

def merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
# https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#why-this-is-necessary
Tempfile.create do |file|
file.write(File.open(asan_lib).read)

stdout, status = Open3.capture2(
'ar',
'd',
file.path,
'asan_preinit.cc.o',
'asan_preinit.cpp.o'
)

stdout, status = Open3.capture2(
ENV['CXX'],
'-Wl,--whole-archive',
fuzzer_no_main_lib,
file.path,
'-Wl,--no-whole-archive',
'-lpthread',
'-ldl',
'-shared',
'-o',
'asan_with_fuzzer.so'
)
end
end

fuzzer_no_main_libs = [
'libclang_rt.fuzzer_no_main.a',
'libclang_rt.fuzzer_no_main-aarch64.a',
'libclang_rt.fuzzer_no_main-x86_64.a'
]
fuzzer_no_main_lib = fuzzer_no_main_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)

unless fuzzer_no_main_lib
puts("Could not find fuzzer_no_main using #{CLANG}.")
fuzzer_no_main_lib = ENV.fetch(FUZZER_NO_MAIN_LIB, nil)
if fuzzer_no_main_lib.nil?
puts("Could not find fuzzer_no_main in #{FUZZER_NO_MAIN_LIB}.")
puts("Please include #{CLANG} in your path or specify #{FUZZER_NO_MAIN_LIB} ENV variable.")
exit(1)
end
end

# The LOCAL_LIBS variable allows linking arbitrary libraries into Ruby C
# extensions. It is supported by the Ruby mkmf library and C extension Makefile.
# For more information, see https://github.com/ruby/ruby/blob/master/lib/mkmf.rb.
$LOCAL_LIBS = lib_path
$LOCAL_LIBS = fuzzer_no_main_lib

asan_libs = [
'libclang_rt.asan.a',
'libclang_rt.asan-aarch64.a',
'libclang_rt.asan-x86_64.a'
]
asan_lib = asan_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)

unless asan_lib
puts("Could not find asan using #{CLANG}.")
exit(1)
end

merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)

create_makefile('cruzzy/cruzzy')
File renamed without changes.
3 changes: 2 additions & 1 deletion ext/cruzzy/dummy/extconf.rb → ext/dummy/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

require 'mkmf'

# https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
$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'

create_makefile('cruzzy/cruzzy/dummy')
create_makefile('dummy/dummy')
18 changes: 17 additions & 1 deletion lib/ruzzy.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
# frozen_string_literal: true

require 'pathname'

# A Ruby C extension fuzzer
module Ruzzy
require 'cruzzy/cruzzy'
require 'cruzzy/dummy/dummy'

DEFAULT_ARGS = [$PROGRAM_NAME] + ARGV

def fuzz(test_one_input, args = DEFAULT_ARGS)
c_fuzz(test_one_input, args)
end

def ext_path
(Pathname.new(__FILE__).parent.parent + 'ext' + 'cruzzy').to_s
end

def dummy_test_one_input(data)
# This 'require' depends on LD_PRELOAD, so it's placed inside the function
# scope. This allows us to run ext_path for LD_PRELOAD and not have a
# circular dependency.
require 'dummy/dummy'

c_dummy_test_one_input(data)
end

module_function :fuzz
module_function :ext_path
module_function :dummy_test_one_input
end
6 changes: 2 additions & 4 deletions ruzzy.gemspec
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# frozen_string_literal: true

require 'rake'

Gem::Specification.new do |s|
s.name = 'ruzzy'
s.version = '0.5.0'
s.summary = 'A Ruby C extension fuzzer'
s.authors = ['Trail of Bits']
s.email = '[email protected]'
s.files = FileList['lib/**/*.rb', 'ext/**/*.{rb,c,h}']
s.files = Dir['lib/**/*.rb'] + Dir['ext/**/*.{rb,c,h}']
s.homepage = 'https://rubygems.org/gems/ruzzy'
s.license = 'AGPL-3.0'
s.extensions = %w[ext/cruzzy/extconf.rb]
s.extensions = %w[ext/cruzzy/extconf.rb ext/dummy/extconf.rb]
s.required_ruby_version = '>= 3.1.0'

s.add_development_dependency 'rake', '~> 13.0'
Expand Down
16 changes: 10 additions & 6 deletions test/test_ruzzy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ def test_c_libfuzzer_is_loaded
assert_true(result)
end

def test_c_dummy_test_one_input_proc
dummy_test_one_input = proc { |data| Ruzzy.c_dummy_test_one_input(data) }
def test_dummy_test_one_input_proc
dummy_test_one_input = proc { |data| Ruzzy.dummy_test_one_input(data) }

result = dummy_test_one_input.call('test')
expected = 0

assert_equal(result, expected)
end

def test_c_dummy_test_one_input_lambda
dummy_test_one_input = ->(data) { Ruzzy.c_dummy_test_one_input(data) }
def test_dummy_test_one_input_lambda
dummy_test_one_input = ->(data) { Ruzzy.dummy_test_one_input(data) }

result = dummy_test_one_input.call('test')
expected = 0

assert_equal(result, expected)
end

def test_c_dummy_test_one_input_invalid_return
def test_dummy_test_one_input_invalid_return
omit("This test calls LLVMFuzzerRunDriver, which we don't have a good harness for yet")

dummy_test_one_input = lambda do |data|
Ruzzy.c_dummy_test_one_input(data)
Ruzzy.dummy_test_one_input(data)
'not an integer or nil'
end

Expand All @@ -46,4 +46,8 @@ def test_fuzz_without_proc
Ruzzy.fuzz('not a proc')
end
end

def test_ext_path
assert(Ruzzy.ext_path)
end
end

0 comments on commit ac30448

Please sign in to comment.