ruzzy 0.5.0 → 0.6.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: 0abba0ffb63d4c50fbbb33fb3624299f8421293a02d65e199525c75b4dbd5f46
4
+ data.tar.gz: 4787489da1373bb820cd66d62a913a4c9c33c49bda68f251dae25745ccac3679
5
5
  SHA512:
6
- metadata.gz: e46ea2f68f183a5f3c87fd2ce1aeb1e36e2cbd6cbcb699aaa52e0bdee9d485959118ef3dedae073a78079d0afc94838b01ca8ac3a7f0babd59603930f7964b90
7
- data.tar.gz: 04bb8723b7dd1ea06abdbc3b52fa6a9cc05e9c333f8f263aaaad6fec6b3b49c01f8eb3fb062c044486aa1325bd41754e60e9829688799358d49fc865df7f38df
6
+ metadata.gz: c029eca231fbeb0d8b19b469a6cb2de99117a939e35c9f42622f803d06a0b4ad85ceacc25dff1bba9e56b1179358ec8fff054a35d894faa11398727891697216
7
+ data.tar.gz: cb761ff27a1fa8d462295d9022d03490338d2608d94cddc8454fb2919fd308e8ee7ea18b4735e601574ca1366de266607c2966be18e27af21ab1c3e243f60237
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.setup(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 setup 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("setup"), 1, hash_arg);
211
+ }
212
+
213
+ static VALUE c_trace_branch(VALUE self)
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 Qnil;
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_branch", &c_trace_branch, 0);
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',
@@ -105,7 +104,33 @@ unless asan_lib
105
104
  exit(1)
106
105
  end
107
106
 
108
- merge_asan_libfuzzer_lib(asan_lib, fuzzer_no_main_lib)
107
+ merge_sanitizer_libfuzzer_lib(
108
+ asan_lib,
109
+ fuzzer_no_main_lib,
110
+ 'asan_with_fuzzer.so',
111
+ 'asan_preinit.cc.o',
112
+ 'asan_preinit.cpp.o'
113
+ )
114
+
115
+ ubsan_libs = [
116
+ 'libclang_rt.ubsan_standalone.a',
117
+ 'libclang_rt.ubsan_standalone-aarch64.a',
118
+ 'libclang_rt.ubsan_standalone-x86_64.a'
119
+ ]
120
+ ubsan_lib = ubsan_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)
121
+
122
+ unless ubsan_lib
123
+ LOGGER.error("Could not find ubsan using #{CC}.")
124
+ exit(1)
125
+ end
126
+
127
+ merge_sanitizer_libfuzzer_lib(
128
+ ubsan_lib,
129
+ fuzzer_no_main_lib,
130
+ 'ubsan_with_fuzzer.so',
131
+ 'ubsan_init_standalone_preinit.cc.o',
132
+ 'ubsan_init_standalone_preinit.cpp.o'
133
+ )
109
134
 
110
135
  # The LOCAL_LIBS variable allows linking arbitrary libraries into Ruby C
111
136
  # extensions. It is supported by the Ruby mkmf library and C extension Makefile.
data/lib/ruzzy.rb CHANGED
@@ -7,18 +7,21 @@ 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
18
+ def dummy
19
+ fuzz(->(data) { Ruzzy.dummy_test_one_input(data) })
17
20
  end
18
21
 
19
22
  def dummy_test_one_input(data)
20
23
  # 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
24
+ # scope. This allows us to access EXT_PATH for LD_PRELOAD and not have a
22
25
  # circular dependency.
23
26
  require 'dummy/dummy'
24
27
 
@@ -26,6 +29,76 @@ module Ruzzy
26
29
  end
27
30
 
28
31
  module_function :fuzz
29
- module_function :ext_path
32
+ module_function :dummy
30
33
  module_function :dummy_test_one_input
31
34
  end
35
+
36
+ # Hook Integer operations for tracing in SantizerCoverage
37
+ class Integer
38
+ alias ruzzy_eeql ==
39
+ alias ruzzy_eeeql ===
40
+ alias ruzzy_eql? eql?
41
+ alias ruzzy_spc <=>
42
+ alias ruzzy_lt <
43
+ alias ruzzy_le <=
44
+ alias ruzzy_gt >
45
+ alias ruzzy_ge >=
46
+ alias ruzzy_divo /
47
+ alias ruzzy_div div
48
+ alias ruzzy_divmod divmod
49
+
50
+ def ==(other)
51
+ Ruzzy.c_trace_cmp8(self, other)
52
+ ruzzy_eeql(other)
53
+ end
54
+
55
+ def ===(other)
56
+ Ruzzy.c_trace_cmp8(self, other)
57
+ ruzzy_eeeql(other)
58
+ end
59
+
60
+ def eql?(other)
61
+ Ruzzy.c_trace_cmp8(self, other)
62
+ ruzzy_eql?(other)
63
+ end
64
+
65
+ def <=>(other)
66
+ Ruzzy.c_trace_cmp8(self, other)
67
+ ruzzy_spc(other)
68
+ end
69
+
70
+ def <(other)
71
+ Ruzzy.c_trace_cmp8(self, other)
72
+ ruzzy_lt(other)
73
+ end
74
+
75
+ def <=(other)
76
+ Ruzzy.c_trace_cmp8(self, other)
77
+ ruzzy_le(other)
78
+ end
79
+
80
+ def >(other)
81
+ Ruzzy.c_trace_cmp8(self, other)
82
+ ruzzy_gt(other)
83
+ end
84
+
85
+ def >=(other)
86
+ Ruzzy.c_trace_cmp8(self, other)
87
+ ruzzy_ge(other)
88
+ end
89
+
90
+ def /(other)
91
+ Ruzzy.c_trace_div8(other)
92
+ ruzzy_divo(other)
93
+ end
94
+
95
+ def div(other)
96
+ Ruzzy.c_trace_div8(other)
97
+ ruzzy_div(other)
98
+ end
99
+
100
+ def divmod(other)
101
+ Ruzzy.c_trace_div8(other)
102
+ ruzzy_divmod(other)
103
+ end
104
+ 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.6.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-02-13 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,14 +91,14 @@ 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
104
  summary: A Ruby C extension fuzzer