ruzzy 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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: []