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 +4 -4
- data/ext/cruzzy/cruzzy.c +119 -6
- data/ext/cruzzy/extconf.rb +35 -10
- data/lib/ruzzy.rb +77 -4
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0abba0ffb63d4c50fbbb33fb3624299f8421293a02d65e199525c75b4dbd5f46
|
4
|
+
data.tar.gz: 4787489da1373bb820cd66d62a913a4c9c33c49bda68f251dae25745ccac3679
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
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
|
}
|
data/ext/cruzzy/extconf.rb
CHANGED
@@ -6,7 +6,7 @@ require 'tempfile'
|
|
6
6
|
require 'rbconfig'
|
7
7
|
require 'logger'
|
8
8
|
|
9
|
-
LOGGER = Logger.new(
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
16
|
-
(
|
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
|
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 :
|
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.
|
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-
|
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.
|
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.
|
101
|
+
rubygems_version: 3.5.3
|
88
102
|
signing_key:
|
89
103
|
specification_version: 4
|
90
104
|
summary: A Ruby C extension fuzzer
|