ruzzy 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d1625761614ef3e6e00d5b36a446dbbba3e880e75269b81e5e44f88b46945f4
4
+ data.tar.gz: eb5733016d8caf73b5230a1277e561781649101ebc614707f91a30b558056afc
5
+ SHA512:
6
+ metadata.gz: e46ea2f68f183a5f3c87fd2ce1aeb1e36e2cbd6cbcb699aaa52e0bdee9d485959118ef3dedae073a78079d0afc94838b01ca8ac3a7f0babd59603930f7964b90
7
+ data.tar.gz: 04bb8723b7dd1ea06abdbc3b52fa6a9cc05e9c333f8f263aaaad6fec6b3b49c01f8eb3fb062c044486aa1325bd41754e60e9829688799358d49fc865df7f38df
@@ -0,0 +1,136 @@
1
+ #include <dlfcn.h>
2
+ #include <stdlib.h>
3
+ #include <stdint.h>
4
+ #include <stdio.h>
5
+ #include <signal.h>
6
+ #include <string.h>
7
+ #include <unistd.h>
8
+
9
+ #include <ruby.h>
10
+
11
+ // 128 arguments should be enough for anybody
12
+ #define MAX_ARGS_SIZE 128
13
+
14
+ int LLVMFuzzerRunDriver(
15
+ int *argc,
16
+ char ***argv,
17
+ int (*cb)(const uint8_t *data, size_t size)
18
+ );
19
+
20
+ VALUE PROC_HOLDER = Qnil;
21
+
22
+ static VALUE c_libfuzzer_is_loaded(VALUE self)
23
+ {
24
+ void *self_lib = dlopen(NULL, RTLD_LAZY);
25
+
26
+ if (!self_lib) {
27
+ return Qfalse;
28
+ }
29
+
30
+ void *sym = dlsym(self_lib, "LLVMFuzzerRunDriver");
31
+
32
+ dlclose(self_lib);
33
+
34
+ return sym ? Qtrue : Qfalse;
35
+ }
36
+
37
+ int ATEXIT_RETCODE = 0;
38
+
39
+ static void ruzzy_exit() {
40
+ _exit(ATEXIT_RETCODE);
41
+ }
42
+
43
+ static void graceful_exit(int code) {
44
+ // Disable libFuzzer's atexit
45
+ ATEXIT_RETCODE = code;
46
+ atexit(ruzzy_exit);
47
+ exit(code);
48
+ }
49
+
50
+ static void sigint_handler(int signal) {
51
+ fprintf(
52
+ stderr,
53
+ "Signal %d (%s) received. Exiting...\n",
54
+ signal,
55
+ strsignal(signal)
56
+ );
57
+ graceful_exit(signal);
58
+ }
59
+
60
+ static int proc_caller(const uint8_t *data, size_t size)
61
+ {
62
+ VALUE arg = rb_str_new((char *)data, size);
63
+ VALUE rb_args = rb_ary_new3(1, arg);
64
+ VALUE result = rb_proc_call(PROC_HOLDER, rb_args);
65
+
66
+ // By default, Ruby procs and lambdas will return nil if an explicit return
67
+ // is not specified. Rather than forcing callers to specify a return, let's
68
+ // handle the nil case for them and continue adding the input to the corpus.
69
+ if (NIL_P(result)) {
70
+ // https://llvm.org/docs/LibFuzzer.html#rejecting-unwanted-inputs
71
+ return 0;
72
+ }
73
+
74
+ if (!FIXNUM_P(result)) {
75
+ rb_raise(
76
+ rb_eTypeError,
77
+ "fuzz target function did not return an integer or nil"
78
+ );
79
+ }
80
+
81
+ return NUM2INT(result);
82
+ }
83
+
84
+ static VALUE c_fuzz(VALUE self, VALUE test_one_input, VALUE args)
85
+ {
86
+ char *argv[MAX_ARGS_SIZE];
87
+ int args_len = RARRAY_LEN(args);
88
+
89
+ // Assume caller always passes in at least the program name as args[0]
90
+ if (args_len <= 0) {
91
+ rb_raise(
92
+ rb_eRuntimeError,
93
+ "zero arguments passed, we assume at least the program name is present"
94
+ );
95
+ }
96
+
97
+ // Account for NULL byte at the end
98
+ if ((args_len + 1) >= MAX_ARGS_SIZE) {
99
+ rb_raise(
100
+ rb_eRuntimeError,
101
+ "cannot specify %d or more arguments",
102
+ MAX_ARGS_SIZE
103
+ );
104
+ }
105
+
106
+ if (!rb_obj_is_proc(test_one_input)) {
107
+ rb_raise(rb_eRuntimeError, "expected a proc or lambda");
108
+ }
109
+
110
+ PROC_HOLDER = test_one_input;
111
+
112
+ for (int i = 0; i < args_len; i++) {
113
+ VALUE arg = RARRAY_PTR(args)[i];
114
+ argv[i] = StringValuePtr(arg);
115
+ }
116
+ argv[args_len] = NULL;
117
+
118
+ char **args_ptr = &argv[0];
119
+
120
+ // https://llvm.org/docs/LibFuzzer.html#using-libfuzzer-as-a-library
121
+ int result = LLVMFuzzerRunDriver(&args_len, &args_ptr, proc_caller);
122
+
123
+ return INT2NUM(result);
124
+ }
125
+
126
+ void Init_cruzzy()
127
+ {
128
+ if (signal(SIGINT, sigint_handler) == SIG_ERR) {
129
+ fprintf(stderr, "Could not set SIGINT signal handler\n");
130
+ exit(1);
131
+ }
132
+
133
+ VALUE ruzzy = rb_const_get(rb_cObject, rb_intern("Ruzzy"));
134
+ rb_define_module_function(ruzzy, "c_fuzz", &c_fuzz, 2);
135
+ rb_define_module_function(ruzzy, "c_libfuzzer_is_loaded", &c_libfuzzer_is_loaded, 0);
136
+ }
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'open3'
5
+ require 'tempfile'
6
+ require 'rbconfig'
7
+ require 'logger'
8
+
9
+ LOGGER = Logger.new(STDERR)
10
+ LOGGER.level = ENV.key?('RUZZY_DEBUG') ? Logger::DEBUG : Logger::INFO
11
+
12
+ # These ENV variables really shouldn't be used because we don't support
13
+ # compilers other than clang, like gcc, etc. Instead prefer to properly include
14
+ # clang in your PATH. But they're here if you really need them. Also note that
15
+ # *technically* Ruby does not support C extensions compiled with a different
16
+ # compiler than Ruby itself was compiled with. So we're on somewhat shaky
17
+ # ground here. For more information see:
18
+ # https://github.com/rubygems/rubygems/issues/1508
19
+ CC = ENV.fetch('CC', 'clang')
20
+ CXX = ENV.fetch('CXX', 'clang++')
21
+ AR = ENV.fetch('AR', 'ar')
22
+ FUZZER_NO_MAIN_LIB_ENV = 'FUZZER_NO_MAIN_LIB'
23
+
24
+ LOGGER.debug("Ruby CC: #{RbConfig::CONFIG['CC']}")
25
+ LOGGER.debug("Ruby CXX: #{RbConfig::CONFIG['CXX']}")
26
+ LOGGER.debug("Ruby AR: #{RbConfig::CONFIG['AR']}")
27
+
28
+ find_executable(CC)
29
+ find_executable(CXX)
30
+
31
+ def get_clang_file_name(file_name)
32
+ stdout, status = Open3.capture2(CC, '--print-file-name', file_name)
33
+ success = status.success?
34
+ exists = success ? File.exist?(stdout.strip) : false
35
+ LOGGER.debug("Search for #{file_name} using #{CC}: success=#{success} exists=#{exists}")
36
+ success && exists ? stdout.strip : false
37
+ end
38
+
39
+ def merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
40
+ merged_output = 'asan_with_fuzzer.so'
41
+
42
+ # https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#why-this-is-necessary
43
+ Tempfile.create do |file|
44
+ file.write(File.open(asan_lib).read)
45
+
46
+ LOGGER.debug("Creating ASAN archive at #{file.path}")
47
+ _, status = Open3.capture2(
48
+ AR,
49
+ 'd',
50
+ file.path,
51
+ 'asan_preinit.cc.o',
52
+ 'asan_preinit.cpp.o'
53
+ )
54
+ unless status.success?
55
+ LOGGER.error("The #{AR} archive command failed.")
56
+ exit(1)
57
+ end
58
+
59
+ LOGGER.debug("Merging ASAN at #{file.path} and libFuzzer at #{fuzzer_no_main_lib} to #{merged_output}")
60
+ _, status = Open3.capture2(
61
+ CXX,
62
+ '-Wl,--whole-archive',
63
+ fuzzer_no_main_lib,
64
+ file.path,
65
+ '-Wl,--no-whole-archive',
66
+ '-lpthread',
67
+ '-ldl',
68
+ '-shared',
69
+ '-o',
70
+ merged_output
71
+ )
72
+ unless status.success?
73
+ LOGGER.error("The #{CXX} shared object merging command failed.")
74
+ exit(1)
75
+ end
76
+ end
77
+ end
78
+
79
+ fuzzer_no_main_libs = [
80
+ 'libclang_rt.fuzzer_no_main.a',
81
+ 'libclang_rt.fuzzer_no_main-aarch64.a',
82
+ 'libclang_rt.fuzzer_no_main-x86_64.a'
83
+ ]
84
+ fuzzer_no_main_lib = fuzzer_no_main_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)
85
+
86
+ unless fuzzer_no_main_lib
87
+ LOGGER.warn("Could not find fuzzer_no_main using #{CC}.")
88
+ fuzzer_no_main_lib = ENV.fetch(FUZZER_NO_MAIN_LIB_ENV, nil)
89
+ if fuzzer_no_main_lib.nil?
90
+ LOGGER.error("Could not find fuzzer_no_main in #{FUZZER_NO_MAIN_LIB_ENV}.")
91
+ LOGGER.error("Please include #{CC} in your path or specify #{FUZZER_NO_MAIN_LIB_ENV} ENV variable.")
92
+ exit(1)
93
+ end
94
+ end
95
+
96
+ asan_libs = [
97
+ 'libclang_rt.asan.a',
98
+ 'libclang_rt.asan-aarch64.a',
99
+ 'libclang_rt.asan-x86_64.a'
100
+ ]
101
+ asan_lib = asan_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)
102
+
103
+ unless asan_lib
104
+ LOGGER.error("Could not find asan using #{CC}.")
105
+ exit(1)
106
+ end
107
+
108
+ merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
109
+
110
+ # The LOCAL_LIBS variable allows linking arbitrary libraries into Ruby C
111
+ # extensions. It is supported by the Ruby mkmf library and C extension Makefile.
112
+ # For more information, see https://github.com/ruby/ruby/blob/master/lib/mkmf.rb.
113
+ $LOCAL_LIBS = fuzzer_no_main_lib
114
+
115
+ create_makefile('cruzzy/cruzzy')
data/ext/dummy/dummy.c ADDED
@@ -0,0 +1,40 @@
1
+ #include <stdlib.h>
2
+ #include <stdint.h>
3
+
4
+ #include <ruby.h>
5
+
6
+ // https://llvm.org/docs/LibFuzzer.html#toy-example
7
+ static int _c_dummy_test_one_input(const uint8_t *data, size_t size)
8
+ {
9
+ char test[] = {'a', 'b', 'c'};
10
+
11
+ if (size > 0 && data[0] == 'H') {
12
+ if (size > 1 && data[1] == 'I') {
13
+ // This code exists specifically to test the driver and ensure
14
+ // libFuzzer is functioning as expected, so we can safely ignore
15
+ // the warning.
16
+ #pragma clang diagnostic push
17
+ #pragma clang diagnostic ignored "-Warray-bounds"
18
+ test[1024] = 'd';
19
+ #pragma clang diagnostic pop
20
+ }
21
+ }
22
+
23
+ return 0;
24
+ }
25
+
26
+ static VALUE c_dummy_test_one_input(VALUE self, VALUE data)
27
+ {
28
+ int result = _c_dummy_test_one_input(
29
+ (uint8_t *)RSTRING_PTR(data),
30
+ RSTRING_LEN(data)
31
+ );
32
+
33
+ return INT2NUM(result);
34
+ }
35
+
36
+ void Init_dummy()
37
+ {
38
+ VALUE ruzzy = rb_const_get(rb_cObject, rb_intern("Ruzzy"));
39
+ rb_define_module_function(ruzzy, "c_dummy_test_one_input", &c_dummy_test_one_input, 1);
40
+ }
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ # https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
6
+ $CFLAGS = '-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g'
7
+ $CXXFLAGS = '-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g'
8
+
9
+ create_makefile('dummy/dummy')
data/lib/ruzzy.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ # A Ruby C extension fuzzer
6
+ module Ruzzy
7
+ require 'cruzzy/cruzzy'
8
+
9
+ DEFAULT_ARGS = [$PROGRAM_NAME] + ARGV
10
+
11
+ def fuzz(test_one_input, args = DEFAULT_ARGS)
12
+ c_fuzz(test_one_input, args)
13
+ end
14
+
15
+ def ext_path
16
+ (Pathname.new(__FILE__).parent.parent + 'ext' + 'cruzzy').to_s
17
+ end
18
+
19
+ def dummy_test_one_input(data)
20
+ # This 'require' depends on LD_PRELOAD, so it's placed inside the function
21
+ # scope. This allows us to run ext_path for LD_PRELOAD and not have a
22
+ # circular dependency.
23
+ require 'dummy/dummy'
24
+
25
+ c_dummy_test_one_input(data)
26
+ end
27
+
28
+ module_function :fuzz
29
+ module_function :ext_path
30
+ module_function :dummy_test_one_input
31
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruzzy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Trail of Bits
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake-compiler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.60'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.60'
55
+ description:
56
+ email: support@trailofbits.com
57
+ executables: []
58
+ extensions:
59
+ - ext/cruzzy/extconf.rb
60
+ - ext/dummy/extconf.rb
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ext/cruzzy/cruzzy.c
64
+ - ext/cruzzy/extconf.rb
65
+ - ext/dummy/dummy.c
66
+ - ext/dummy/extconf.rb
67
+ - lib/ruzzy.rb
68
+ homepage: https://rubygems.org/gems/ruzzy
69
+ licenses:
70
+ - AGPL-3.0-only
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 3.1.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.0.3.1
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: A Ruby C extension fuzzer
91
+ test_files: []