ruzzy 0.5.0 → 0.6.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: 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