ruzzy 0.5.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d1625761614ef3e6e00d5b36a446dbbba3e880e75269b81e5e44f88b46945f4
4
- data.tar.gz: eb5733016d8caf73b5230a1277e561781649101ebc614707f91a30b558056afc
3
+ metadata.gz: 64fa385ff3dca5a231ebc02511f7240143838b4cf4314a8ced35a57127ac2dc2
4
+ data.tar.gz: 1cd94db1a0b30debdc03caddf39394b3759b11f85d0b2ef78037e5769b0761ed
5
5
  SHA512:
6
- metadata.gz: e46ea2f68f183a5f3c87fd2ce1aeb1e36e2cbd6cbcb699aaa52e0bdee9d485959118ef3dedae073a78079d0afc94838b01ca8ac3a7f0babd59603930f7964b90
7
- data.tar.gz: 04bb8723b7dd1ea06abdbc3b52fa6a9cc05e9c333f8f263aaaad6fec6b3b49c01f8eb3fb062c044486aa1325bd41754e60e9829688799358d49fc865df7f38df
6
+ metadata.gz: ed77800fbbe359a7b2be3e215b00d052b2bf8f1038da0e2ad88c6a8ea9d66bcf2d875ed5658592262ef35a18a23c4b1f5fcc9d44e7f669fc81817310dda96122
7
+ data.tar.gz: 80ef311558dd4c4aa88697014e08a6b06a164d182a16d5c4718ce9355bc6b2f82c52bce11d3eaa1d1071df2a900eef9ef46148013ad7099ed1fbf07df173b6c3
data/ext/cruzzy/cruzzy.c CHANGED
@@ -7,16 +7,39 @@
7
7
  #include <unistd.h>
8
8
 
9
9
  #include <ruby.h>
10
+ #include <ruby/debug.h>
11
+
12
+ // This constant is defined in the Ruby C implementation, but it's internal
13
+ // only. Fortunately the event hooking still respects this constant being
14
+ // passed from an external source. For more information see:
15
+ // https://github.com/ruby/ruby/blob/v3_3_0/vm_core.h#L2182-L2184
16
+ #define RUBY_EVENT_COVERAGE_BRANCH 0x020000
10
17
 
11
18
  // 128 arguments should be enough for anybody
12
19
  #define MAX_ARGS_SIZE 128
13
20
 
14
- int LLVMFuzzerRunDriver(
21
+ // TODO: should we mmap like Atheris?
22
+ #define MAX_COUNTERS 8192
23
+
24
+ extern int LLVMFuzzerRunDriver(
15
25
  int *argc,
16
26
  char ***argv,
17
27
  int (*cb)(const uint8_t *data, size_t size)
18
28
  );
19
29
 
30
+ extern void __sanitizer_cov_8bit_counters_init(uint8_t *start, uint8_t *stop);
31
+ extern void __sanitizer_cov_pcs_init(uint8_t *pcs_beg, uint8_t *pcs_end);
32
+ extern void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2);
33
+ extern void __sanitizer_cov_trace_div8(uint64_t val);
34
+
35
+ struct PCTableEntry {
36
+ void *pc;
37
+ long flags;
38
+ };
39
+
40
+ struct PCTableEntry PCTABLE[MAX_COUNTERS];
41
+ uint8_t COUNTERS[MAX_COUNTERS];
42
+ uint32_t COUNTER = 0;
20
43
  VALUE PROC_HOLDER = Qnil;
21
44
 
22
45
  static VALUE c_libfuzzer_is_loaded(VALUE self)
@@ -36,18 +59,21 @@ static VALUE c_libfuzzer_is_loaded(VALUE self)
36
59
 
37
60
  int ATEXIT_RETCODE = 0;
38
61
 
39
- static void ruzzy_exit() {
62
+ __attribute__((__noreturn__)) static void ruzzy_exit()
63
+ {
40
64
  _exit(ATEXIT_RETCODE);
41
65
  }
42
66
 
43
- static void graceful_exit(int code) {
67
+ __attribute__((__noreturn__)) static void graceful_exit(int code)
68
+ {
44
69
  // Disable libFuzzer's atexit
45
70
  ATEXIT_RETCODE = code;
46
71
  atexit(ruzzy_exit);
47
72
  exit(code);
48
73
  }
49
74
 
50
- static void sigint_handler(int signal) {
75
+ __attribute__((__noreturn__)) static void sigint_handler(int signal)
76
+ {
51
77
  fprintf(
52
78
  stderr,
53
79
  "Signal %d (%s) received. Exiting...\n",
@@ -78,7 +104,7 @@ static int proc_caller(const uint8_t *data, size_t size)
78
104
  );
79
105
  }
80
106
 
81
- return NUM2INT(result);
107
+ return FIX2INT(result);
82
108
  }
83
109
 
84
110
  static VALUE c_fuzz(VALUE self, VALUE test_one_input, VALUE args)
@@ -120,7 +146,91 @@ static VALUE c_fuzz(VALUE self, VALUE test_one_input, VALUE args)
120
146
  // https://llvm.org/docs/LibFuzzer.html#using-libfuzzer-as-a-library
121
147
  int result = LLVMFuzzerRunDriver(&args_len, &args_ptr, proc_caller);
122
148
 
123
- return INT2NUM(result);
149
+ return INT2FIX(result);
150
+ }
151
+
152
+ static VALUE c_trace_cmp8(VALUE self, VALUE arg1, VALUE arg2) {
153
+ // Ruby numerics include both integers and floats. Integers are further
154
+ // divided into fixnums and bignums. Fixnums can be 31-bit or 63-bit
155
+ // integers depending on the bit size of a long. Bignums are arbitrary
156
+ // precision integers. This function can only handle fixnums because
157
+ // sancov only provides comparison tracing up to 8-byte integers.
158
+ if (FIXNUM_P(arg1) && FIXNUM_P(arg2)) {
159
+ long arg1_val = NUM2LONG(arg1);
160
+ long arg2_val = NUM2LONG(arg2);
161
+ __sanitizer_cov_trace_cmp8((uint64_t) arg1_val, (uint64_t) arg2_val);
162
+ }
163
+
164
+ return Qnil;
165
+ }
166
+
167
+ static VALUE c_trace_div8(VALUE self, VALUE val) {
168
+ if (FIXNUM_P(val)) {
169
+ long val_val = NUM2LONG(val);
170
+ __sanitizer_cov_trace_div8((uint64_t) val_val);
171
+ }
172
+
173
+ return Qnil;
174
+ }
175
+
176
+ static void event_hook_branch(VALUE counter_hash, rb_trace_arg_t *tracearg) {
177
+ VALUE path = rb_tracearg_path(tracearg);
178
+ ID path_sym = rb_intern_str(path);
179
+ VALUE lineno = rb_tracearg_lineno(tracearg);
180
+ VALUE tuple = rb_ary_new_from_args(2, INT2NUM(path_sym), lineno);
181
+ VALUE existing_counter = rb_hash_lookup(counter_hash, tuple);
182
+
183
+ int counter_index;
184
+
185
+ if (NIL_P(existing_counter)) {
186
+ rb_hash_aset(counter_hash, tuple, INT2FIX(COUNTER));
187
+ counter_index = COUNTER++;
188
+ } else {
189
+ counter_index = FIX2INT(existing_counter);
190
+ }
191
+
192
+ COUNTERS[counter_index % MAX_COUNTERS]++;
193
+ }
194
+
195
+ static void enable_branch_coverage_hooks()
196
+ {
197
+ // Call Coverage.start(branches: true) to activate branch coverage hooks.
198
+ // Branch coverage hooks will not be activated without this call despite
199
+ // adding the event hooks. I suspect rb_set_coverages must be called
200
+ // first, which initializes some global state that we do not have direct
201
+ // access to. Calling start initializes coverage state here:
202
+ // https://github.com/ruby/ruby/blob/v3_3_0/ext/coverage/coverage.c#L112-L120
203
+ // If rb_set_coverages is not called, then rb_get_coverages returns a NULL
204
+ // pointer, which appears to effectively disable coverage collection here:
205
+ // https://github.com/ruby/ruby/blob/v3_3_0/iseq.c
206
+ rb_require("coverage");
207
+ VALUE coverage_mod = rb_const_get(rb_cObject, rb_intern("Coverage"));
208
+ VALUE hash_arg = rb_hash_new();
209
+ rb_hash_aset(hash_arg, ID2SYM(rb_intern("branches")), Qtrue);
210
+ rb_funcall(coverage_mod, rb_intern("start"), 1, hash_arg);
211
+ }
212
+
213
+ static VALUE c_trace(VALUE self, VALUE harness_path)
214
+ {
215
+ VALUE counter_hash = rb_hash_new();
216
+
217
+ __sanitizer_cov_8bit_counters_init(COUNTERS, COUNTERS + MAX_COUNTERS);
218
+ __sanitizer_cov_pcs_init((uint8_t *)PCTABLE, (uint8_t *)(PCTABLE + MAX_COUNTERS));
219
+
220
+ rb_event_flag_t events = RUBY_EVENT_COVERAGE_BRANCH;
221
+ rb_event_hook_flag_t flags = (
222
+ RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG
223
+ );
224
+ rb_add_event_hook2(
225
+ (rb_event_hook_func_t) event_hook_branch,
226
+ events,
227
+ counter_hash,
228
+ flags
229
+ );
230
+
231
+ enable_branch_coverage_hooks();
232
+
233
+ return rb_require(StringValueCStr(harness_path));
124
234
  }
125
235
 
126
236
  void Init_cruzzy()
@@ -133,4 +243,7 @@ void Init_cruzzy()
133
243
  VALUE ruzzy = rb_const_get(rb_cObject, rb_intern("Ruzzy"));
134
244
  rb_define_module_function(ruzzy, "c_fuzz", &c_fuzz, 2);
135
245
  rb_define_module_function(ruzzy, "c_libfuzzer_is_loaded", &c_libfuzzer_is_loaded, 0);
246
+ rb_define_module_function(ruzzy, "c_trace_cmp8", &c_trace_cmp8, 2);
247
+ rb_define_module_function(ruzzy, "c_trace_div8", &c_trace_div8, 1);
248
+ rb_define_module_function(ruzzy, "c_trace", &c_trace, 1);
136
249
  }
@@ -6,7 +6,7 @@ require 'tempfile'
6
6
  require 'rbconfig'
7
7
  require 'logger'
8
8
 
9
- LOGGER = Logger.new(STDERR)
9
+ LOGGER = Logger.new($stderr)
10
10
  LOGGER.level = ENV.key?('RUZZY_DEBUG') ? Logger::DEBUG : Logger::INFO
11
11
 
12
12
  # These ENV variables really shouldn't be used because we don't support
@@ -36,27 +36,26 @@ def get_clang_file_name(file_name)
36
36
  success && exists ? stdout.strip : false
37
37
  end
38
38
 
39
- def merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
40
- merged_output = 'asan_with_fuzzer.so'
41
-
39
+ def merge_sanitizer_libfuzzer_lib(sanitizer_lib, fuzzer_no_main_lib, merged_output, *preinits)
42
40
  # https://github.com/google/atheris/blob/master/native_extension_fuzzing.md#why-this-is-necessary
43
41
  Tempfile.create do |file|
44
- file.write(File.open(asan_lib).read)
42
+ LOGGER.debug("Creating #{sanitizer_lib} sanitizer archive at #{file.path}")
43
+
44
+ file.write(File.open(sanitizer_lib).read)
45
45
 
46
- LOGGER.debug("Creating ASAN archive at #{file.path}")
47
46
  _, status = Open3.capture2(
48
47
  AR,
49
48
  'd',
50
49
  file.path,
51
- 'asan_preinit.cc.o',
52
- 'asan_preinit.cpp.o'
50
+ *preinits
53
51
  )
54
52
  unless status.success?
55
53
  LOGGER.error("The #{AR} archive command failed.")
56
54
  exit(1)
57
55
  end
58
56
 
59
- LOGGER.debug("Merging ASAN at #{file.path} and libFuzzer at #{fuzzer_no_main_lib} to #{merged_output}")
57
+ LOGGER.debug("Merging sanitizer at #{file.path} with libFuzzer at #{fuzzer_no_main_lib} to #{merged_output}")
58
+
60
59
  _, status = Open3.capture2(
61
60
  CXX,
62
61
  '-Wl,--whole-archive',
@@ -65,6 +64,7 @@ def merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
65
64
  '-Wl,--no-whole-archive',
66
65
  '-lpthread',
67
66
  '-ldl',
67
+ '-lstdc++',
68
68
  '-shared',
69
69
  '-o',
70
70
  merged_output
@@ -105,11 +105,39 @@ unless asan_lib
105
105
  exit(1)
106
106
  end
107
107
 
108
- merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
108
+ merge_sanitizer_libfuzzer_lib(
109
+ asan_lib,
110
+ fuzzer_no_main_lib,
111
+ 'asan_with_fuzzer.so',
112
+ 'asan_preinit.cc.o',
113
+ 'asan_preinit.cpp.o'
114
+ )
115
+
116
+ ubsan_libs = [
117
+ 'libclang_rt.ubsan_standalone.a',
118
+ 'libclang_rt.ubsan_standalone-aarch64.a',
119
+ 'libclang_rt.ubsan_standalone-x86_64.a'
120
+ ]
121
+ ubsan_lib = ubsan_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)
122
+
123
+ unless ubsan_lib
124
+ LOGGER.error("Could not find ubsan using #{CC}.")
125
+ exit(1)
126
+ end
127
+
128
+ merge_sanitizer_libfuzzer_lib(
129
+ ubsan_lib,
130
+ fuzzer_no_main_lib,
131
+ 'ubsan_with_fuzzer.so',
132
+ 'ubsan_init_standalone_preinit.cc.o',
133
+ 'ubsan_init_standalone_preinit.cpp.o'
134
+ )
109
135
 
110
136
  # The LOCAL_LIBS variable allows linking arbitrary libraries into Ruby C
111
137
  # extensions. It is supported by the Ruby mkmf library and C extension Makefile.
112
138
  # For more information, see https://github.com/ruby/ruby/blob/master/lib/mkmf.rb.
113
139
  $LOCAL_LIBS = fuzzer_no_main_lib
114
140
 
141
+ $LIBS << ' -lstdc++'
142
+
115
143
  create_makefile('cruzzy/cruzzy')
data/ext/dummy/dummy.c CHANGED
@@ -6,17 +6,18 @@
6
6
  // https://llvm.org/docs/LibFuzzer.html#toy-example
7
7
  static int _c_dummy_test_one_input(const uint8_t *data, size_t size)
8
8
  {
9
- char test[] = {'a', 'b', 'c'};
9
+ volatile char boom = 'x';
10
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
11
+ if (size == 2) {
12
+ if (data[0] == 'H') {
13
+ if (data[1] == 'I') {
14
+ // Intentional heap-use-after-free for testing purposes
15
+ char * volatile ptr = malloc(128);
16
+ ptr[0] = 'x';
17
+ free(ptr);
18
+ boom = ptr[0];
19
+ (void) boom;
20
+ }
20
21
  }
21
22
  }
22
23
 
data/lib/ruzzy.rb CHANGED
@@ -2,30 +2,118 @@
2
2
 
3
3
  require 'pathname'
4
4
 
5
- # A Ruby C extension fuzzer
5
+ # A coverage-guided fuzzer for pure Ruby code and Ruby C extensions
6
6
  module Ruzzy
7
7
  require 'cruzzy/cruzzy'
8
8
 
9
9
  DEFAULT_ARGS = [$PROGRAM_NAME] + ARGV
10
+ EXT_PATH = Pathname.new(__FILE__).parent.parent / 'ext' / 'cruzzy'
11
+ ASAN_PATH = (EXT_PATH / 'asan_with_fuzzer.so').to_s
12
+ UBSAN_PATH = (EXT_PATH / 'ubsan_with_fuzzer.so').to_s
10
13
 
11
14
  def fuzz(test_one_input, args = DEFAULT_ARGS)
12
15
  c_fuzz(test_one_input, args)
13
16
  end
14
17
 
15
- def ext_path
16
- (Pathname.new(__FILE__).parent.parent + 'ext' + 'cruzzy').to_s
17
- end
18
-
19
18
  def dummy_test_one_input(data)
20
19
  # 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
20
+ # scope. This allows us to access EXT_PATH for LD_PRELOAD and not have a
22
21
  # circular dependency.
23
22
  require 'dummy/dummy'
24
23
 
25
24
  c_dummy_test_one_input(data)
26
25
  end
27
26
 
27
+ def dummy
28
+ fuzz(->(data) { dummy_test_one_input(data) })
29
+ end
30
+
31
+ def trace(harness_script)
32
+ harness_path = Pathname.new(harness_script)
33
+
34
+ # Mimic require_relative. If harness script is provided as an absolute path,
35
+ # then use that. If not, then assume the script is in the same directory as
36
+ # as the tracer script, i.e. the caller.
37
+ if !harness_path.absolute?
38
+ caller_path = Pathname.new(caller_locations.first.path)
39
+ harness_path = (caller_path.parent / harness_path).realpath
40
+ end
41
+
42
+ c_trace(harness_path.to_s)
43
+ end
44
+
28
45
  module_function :fuzz
29
- module_function :ext_path
30
46
  module_function :dummy_test_one_input
47
+ module_function :dummy
48
+ module_function :trace
49
+ end
50
+
51
+ # Hook Integer operations for tracing in SantizerCoverage
52
+ class Integer
53
+ alias ruzzy_eeql ==
54
+ alias ruzzy_eeeql ===
55
+ alias ruzzy_eql? eql?
56
+ alias ruzzy_spc <=>
57
+ alias ruzzy_lt <
58
+ alias ruzzy_le <=
59
+ alias ruzzy_gt >
60
+ alias ruzzy_ge >=
61
+ alias ruzzy_divo /
62
+ alias ruzzy_div div
63
+ alias ruzzy_divmod divmod
64
+
65
+ def ==(other)
66
+ Ruzzy.c_trace_cmp8(self, other)
67
+ ruzzy_eeql(other)
68
+ end
69
+
70
+ def ===(other)
71
+ Ruzzy.c_trace_cmp8(self, other)
72
+ ruzzy_eeeql(other)
73
+ end
74
+
75
+ def eql?(other)
76
+ Ruzzy.c_trace_cmp8(self, other)
77
+ ruzzy_eql?(other)
78
+ end
79
+
80
+ def <=>(other)
81
+ Ruzzy.c_trace_cmp8(self, other)
82
+ ruzzy_spc(other)
83
+ end
84
+
85
+ def <(other)
86
+ Ruzzy.c_trace_cmp8(self, other)
87
+ ruzzy_lt(other)
88
+ end
89
+
90
+ def <=(other)
91
+ Ruzzy.c_trace_cmp8(self, other)
92
+ ruzzy_le(other)
93
+ end
94
+
95
+ def >(other)
96
+ Ruzzy.c_trace_cmp8(self, other)
97
+ ruzzy_gt(other)
98
+ end
99
+
100
+ def >=(other)
101
+ Ruzzy.c_trace_cmp8(self, other)
102
+ ruzzy_ge(other)
103
+ end
104
+
105
+ def /(other)
106
+ Ruzzy.c_trace_div8(other)
107
+ ruzzy_divo(other)
108
+ end
109
+
110
+ def div(other)
111
+ Ruzzy.c_trace_div8(other)
112
+ ruzzy_div(other)
113
+ end
114
+
115
+ def divmod(other)
116
+ Ruzzy.c_trace_div8(other)
117
+ ruzzy_divmod(other)
118
+ end
31
119
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruzzy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Trail of Bits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-02 00:00:00.000000000 Z
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake-release
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rubocop
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -77,15 +91,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
91
  requirements:
78
92
  - - ">="
79
93
  - !ruby/object:Gem::Version
80
- version: 3.1.0
94
+ version: 3.0.0
81
95
  required_rubygems_version: !ruby/object:Gem::Requirement
82
96
  requirements:
83
97
  - - ">="
84
98
  - !ruby/object:Gem::Version
85
99
  version: '0'
86
100
  requirements: []
87
- rubygems_version: 3.0.3.1
101
+ rubygems_version: 3.5.3
88
102
  signing_key:
89
103
  specification_version: 4
90
- summary: A Ruby C extension fuzzer
104
+ summary: A coverage-guided fuzzer for pure Ruby code and Ruby C extensions
91
105
  test_files: []