From ac30448e2200d635c57baeb293d0a6973ce5cdc6 Mon Sep 17 00:00:00 2001 From: Matt Schwager Date: Thu, 1 Feb 2024 10:22:13 -0700 Subject: [PATCH] Move Dockerfile logic into extconf.rb This change is in preparation for building a proper gem that can be installed from RubyGems. --- Dockerfile | 29 +++--------- Rakefile | 9 ++-- bin/dummy.rb | 2 +- entrypoint.sh | 5 +- ext/cruzzy/extconf.rb | 76 ++++++++++++++++++++++++++----- ext/{cruzzy => }/dummy/dummy.c | 0 ext/{cruzzy => }/dummy/extconf.rb | 3 +- lib/ruzzy.rb | 18 +++++++- ruzzy.gemspec | 6 +-- test/test_ruzzy.rb | 16 ++++--- 10 files changed, 111 insertions(+), 53 deletions(-) rename ext/{cruzzy => }/dummy/dummy.c (100%) rename ext/{cruzzy => }/dummy/extconf.rb (70%) diff --git a/Dockerfile b/Dockerfile index 4aa8c74..4179a62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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. @@ -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"] diff --git a/Rakefile b/Rakefile index a81eba0..94ad1e0 100644 --- a/Rakefile +++ b/Rakefile @@ -4,8 +4,11 @@ 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 @@ -13,6 +16,6 @@ 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 diff --git a/bin/dummy.rb b/bin/dummy.rb index 9487c7d..83a6176 100644 --- a/bin/dummy.rb +++ b/bin/dummy.rb @@ -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) diff --git a/entrypoint.sh b/entrypoint.sh index 2c8bc76..b261475 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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 "$@" diff --git a/ext/cruzzy/extconf.rb b/ext/cruzzy/extconf.rb index 5289456..4a3ff94 100644 --- a/ext/cruzzy/extconf.rb +++ b/ext/cruzzy/extconf.rb @@ -2,19 +2,59 @@ 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 @@ -22,6 +62,20 @@ # 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') diff --git a/ext/cruzzy/dummy/dummy.c b/ext/dummy/dummy.c similarity index 100% rename from ext/cruzzy/dummy/dummy.c rename to ext/dummy/dummy.c diff --git a/ext/cruzzy/dummy/extconf.rb b/ext/dummy/extconf.rb similarity index 70% rename from ext/cruzzy/dummy/extconf.rb rename to ext/dummy/extconf.rb index 7f555fa..8ac0b3d 100644 --- a/ext/cruzzy/dummy/extconf.rb +++ b/ext/dummy/extconf.rb @@ -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') diff --git a/lib/ruzzy.rb b/lib/ruzzy.rb index 1a8a5ba..fd35121 100644 --- a/lib/ruzzy.rb +++ b/lib/ruzzy.rb @@ -1,9 +1,10 @@ # 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 @@ -11,5 +12,20 @@ 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 diff --git a/ruzzy.gemspec b/ruzzy.gemspec index e030915..6c1067c 100644 --- a/ruzzy.gemspec +++ b/ruzzy.gemspec @@ -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 = 'support@trailofbits.com' - 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' diff --git a/test/test_ruzzy.rb b/test/test_ruzzy.rb index ac242e0..5c96bdc 100644 --- a/test/test_ruzzy.rb +++ b/test/test_ruzzy.rb @@ -10,8 +10,8 @@ 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 @@ -19,8 +19,8 @@ def test_c_dummy_test_one_input_proc 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 @@ -28,11 +28,11 @@ def test_c_dummy_test_one_input_lambda 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 @@ -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