ruzzy 0.7.0 → 0.8.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/extconf.rb +20 -11
- data/lib/ruzzy/fuzzed_data_provider.rb +221 -0
- data/lib/ruzzy.rb +6 -0
- metadata +4 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d564227ec95d197cadc085f2190a7089fbe5610d4e6f7c9d9ff5a84a4243208
|
|
4
|
+
data.tar.gz: aa7c37d65b75d202e650832ffad9589c3a6a750d162760d0af934256268115aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54cc91f765a3622756e3925f62e29003ef3169a8a413af4b450b320bdea979125ede2a203be2b2fac52114abf52b3b735f8d97e395f9181eb01f080b09648988
|
|
7
|
+
data.tar.gz: cc8895f40905092d68bbdad2557fd691c189b04f336bb360c0f0a1b3f82a52d0227f82e966248b2b70cfd694b35e464ecd967a76d0e0426e45f5ffa2e941444f
|
data/ext/cruzzy/extconf.rb
CHANGED
|
@@ -19,6 +19,7 @@ LOGGER.level = ENV.key?('RUZZY_DEBUG') ? Logger::DEBUG : Logger::INFO
|
|
|
19
19
|
CC = ENV.fetch('CC', 'clang')
|
|
20
20
|
CXX = ENV.fetch('CXX', 'clang++')
|
|
21
21
|
AR = ENV.fetch('AR', 'ar')
|
|
22
|
+
LD = ENV.fetch('LD', 'ld')
|
|
22
23
|
FUZZER_NO_MAIN_LIB_ENV = 'FUZZER_NO_MAIN_LIB'
|
|
23
24
|
|
|
24
25
|
LOGGER.debug("Ruby CC: #{RbConfig::CONFIG['CC']}")
|
|
@@ -66,6 +67,7 @@ def merge_sanitizer_libfuzzer_lib(sanitizer_lib, fuzzer_no_main_lib, merged_outp
|
|
|
66
67
|
'-ldl',
|
|
67
68
|
'-lstdc++',
|
|
68
69
|
'-shared',
|
|
70
|
+
"-fuse-ld=#{LD}",
|
|
69
71
|
'-o',
|
|
70
72
|
merged_output
|
|
71
73
|
)
|
|
@@ -76,18 +78,24 @@ def merge_sanitizer_libfuzzer_lib(sanitizer_lib, fuzzer_no_main_lib, merged_outp
|
|
|
76
78
|
end
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
'libclang_rt.fuzzer_no_main.a',
|
|
81
|
-
'libclang_rt.fuzzer_no_main-aarch64.a',
|
|
82
|
-
'libclang_rt.fuzzer_no_main-x86_64.a'
|
|
83
|
-
]
|
|
84
|
-
fuzzer_no_main_lib = fuzzer_no_main_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)
|
|
81
|
+
fuzzer_no_main_lib = ENV.fetch(FUZZER_NO_MAIN_LIB_ENV, nil)
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
LOGGER.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
if fuzzer_no_main_lib
|
|
84
|
+
LOGGER.info("Using #{FUZZER_NO_MAIN_LIB_ENV}=#{fuzzer_no_main_lib}")
|
|
85
|
+
unless File.exist?(fuzzer_no_main_lib)
|
|
86
|
+
LOGGER.error("#{FUZZER_NO_MAIN_LIB_ENV} file does not exist: #{fuzzer_no_main_lib}")
|
|
87
|
+
exit(1)
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
fuzzer_no_main_libs = [
|
|
91
|
+
'libclang_rt.fuzzer_no_main.a',
|
|
92
|
+
'libclang_rt.fuzzer_no_main-aarch64.a',
|
|
93
|
+
'libclang_rt.fuzzer_no_main-x86_64.a'
|
|
94
|
+
]
|
|
95
|
+
fuzzer_no_main_lib = fuzzer_no_main_libs.map { |lib| get_clang_file_name(lib) }.find(&:itself)
|
|
96
|
+
|
|
97
|
+
unless fuzzer_no_main_lib
|
|
98
|
+
LOGGER.error("Could not find fuzzer_no_main using #{CC}.")
|
|
91
99
|
LOGGER.error("Please include #{CC} in your path or specify #{FUZZER_NO_MAIN_LIB_ENV} ENV variable.")
|
|
92
100
|
exit(1)
|
|
93
101
|
end
|
|
@@ -139,5 +147,6 @@ merge_sanitizer_libfuzzer_lib(
|
|
|
139
147
|
$LOCAL_LIBS = fuzzer_no_main_lib
|
|
140
148
|
|
|
141
149
|
$LIBS << ' -lstdc++'
|
|
150
|
+
$DLDFLAGS << " -fuse-ld=#{LD}"
|
|
142
151
|
|
|
143
152
|
create_makefile('cruzzy/cruzzy')
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruzzy
|
|
4
|
+
# Splits raw fuzzer bytes into typed Ruby values.
|
|
5
|
+
#
|
|
6
|
+
# FuzzedDataProvider wraps a binary string (typically from libFuzzer via
|
|
7
|
+
# Ruzzy.fuzz) and provides methods to consume typed values from it. This
|
|
8
|
+
# enables fuzz targets that test APIs accepting typed arguments rather than
|
|
9
|
+
# raw byte strings.
|
|
10
|
+
#
|
|
11
|
+
# Following libFuzzer's FuzzedDataProvider.h design, strings and raw bytes
|
|
12
|
+
# are consumed from the *front* of the buffer, while integers are consumed
|
|
13
|
+
# from the *end*. This bidirectional consumption lets the fuzzer modify
|
|
14
|
+
# structural decisions (integers controlling lengths, indices, variant
|
|
15
|
+
# selection) independently from content (string payloads, raw bytes),
|
|
16
|
+
# improving mutation quality.
|
|
17
|
+
#
|
|
18
|
+
# @example Basic usage in a fuzz target
|
|
19
|
+
# test_one_input = lambda do |data|
|
|
20
|
+
# fdp = Ruzzy::FuzzedDataProvider.new(data)
|
|
21
|
+
# name = fdp.consume_random_length_string(50)
|
|
22
|
+
# age = fdp.consume_int_in_range(0, 150)
|
|
23
|
+
# score = fdp.consume_float_in_range(0.0, 100.0)
|
|
24
|
+
# role = fdp.pick_value_in_list(['admin', 'user', 'guest'])
|
|
25
|
+
# User.new(name: name, age: age, score: score, role: role).validate!
|
|
26
|
+
# end
|
|
27
|
+
# Ruzzy.fuzz(test_one_input)
|
|
28
|
+
class FuzzedDataProvider
|
|
29
|
+
def initialize(data)
|
|
30
|
+
@data = data
|
|
31
|
+
# Front cursor for strings/bytes (advances forward)
|
|
32
|
+
@front = 0
|
|
33
|
+
# Back cursor for integers (advances backward)
|
|
34
|
+
@back = @data.bytesize
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the number of unconsumed bytes remaining.
|
|
38
|
+
def remaining_bytes
|
|
39
|
+
@back - @front
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- Byte and String methods (consume from front) ---
|
|
43
|
+
|
|
44
|
+
# Consume up to +count+ raw bytes from the front of the buffer.
|
|
45
|
+
# Returns a binary-encoded String.
|
|
46
|
+
def consume_bytes(count)
|
|
47
|
+
count = clamp_count(count)
|
|
48
|
+
result = @data.byteslice(@front, count)
|
|
49
|
+
@front += count
|
|
50
|
+
result.force_encoding(Encoding::BINARY)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Consume a variable-length string from the front of the buffer.
|
|
54
|
+
# The string terminates when a backslash followed by a non-backslash
|
|
55
|
+
# byte is encountered, or when +max_length+ characters are consumed.
|
|
56
|
+
# This encoding lets the fuzzer easily control string length through
|
|
57
|
+
# single-byte mutations.
|
|
58
|
+
#
|
|
59
|
+
# Matches libFuzzer's ConsumeRandomLengthString.
|
|
60
|
+
def consume_random_length_string(max_length = remaining_bytes)
|
|
61
|
+
result = +''
|
|
62
|
+
max_length.times do
|
|
63
|
+
break if remaining_bytes.zero?
|
|
64
|
+
|
|
65
|
+
byte = consume_front_byte
|
|
66
|
+
char = byte.chr(Encoding::BINARY)
|
|
67
|
+
|
|
68
|
+
if char == '\\' && remaining_bytes.positive?
|
|
69
|
+
next_byte = consume_front_byte
|
|
70
|
+
next_char = next_byte.chr(Encoding::BINARY)
|
|
71
|
+
break if next_char != '\\'
|
|
72
|
+
|
|
73
|
+
result << '\\'
|
|
74
|
+
else
|
|
75
|
+
result << char
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Consume all remaining bytes. Returns a binary-encoded String.
|
|
82
|
+
def consume_remaining_bytes
|
|
83
|
+
consume_bytes(remaining_bytes)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Consume all remaining bytes as a String.
|
|
87
|
+
def consume_remaining_as_string
|
|
88
|
+
consume_remaining_bytes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# --- Integer methods (consume from end) ---
|
|
92
|
+
|
|
93
|
+
# Consume an unsigned integer from the end of the buffer.
|
|
94
|
+
# Reads up to +count+ bytes in little-endian order from the back.
|
|
95
|
+
# Returns 0 when no data remains.
|
|
96
|
+
def consume_uint(count)
|
|
97
|
+
return 0 if count <= 0 || remaining_bytes.zero?
|
|
98
|
+
|
|
99
|
+
actual = [count, remaining_bytes].min
|
|
100
|
+
result = 0
|
|
101
|
+
actual.times do |i|
|
|
102
|
+
@back -= 1
|
|
103
|
+
result |= @data.getbyte(@back) << (i * 8)
|
|
104
|
+
end
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Consume a signed integer from the end of the buffer.
|
|
109
|
+
# Reads +count+ bytes and interprets as two's complement.
|
|
110
|
+
# Returns 0 when no data remains.
|
|
111
|
+
def consume_int(count)
|
|
112
|
+
unsigned = consume_uint(count)
|
|
113
|
+
return 0 if count.zero?
|
|
114
|
+
|
|
115
|
+
bits = count * 8
|
|
116
|
+
max_unsigned = 1 << bits
|
|
117
|
+
half = max_unsigned >> 1
|
|
118
|
+
|
|
119
|
+
unsigned >= half ? unsigned - max_unsigned : unsigned
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Consume an integer in [min, max] from the end of the buffer.
|
|
123
|
+
# Returns +min+ when no data remains.
|
|
124
|
+
# Raises ArgumentError if min > max.
|
|
125
|
+
#
|
|
126
|
+
# Matches libFuzzer's ConsumeIntegralInRange: consumes only as many
|
|
127
|
+
# bytes from the end as needed to cover the range.
|
|
128
|
+
def consume_int_in_range(min, max)
|
|
129
|
+
raise ArgumentError, "min (#{min}) must be <= max (#{max})" if min > max
|
|
130
|
+
|
|
131
|
+
range = max - min
|
|
132
|
+
return min if range.zero?
|
|
133
|
+
|
|
134
|
+
# Consume bytes from the end, one at a time, until we've covered the range.
|
|
135
|
+
# This matches libFuzzer: only consume bytes while (range >> offset) > 0.
|
|
136
|
+
result = 0
|
|
137
|
+
offset = 0
|
|
138
|
+
while offset < 64 && (range >> offset).positive? && remaining_bytes.positive?
|
|
139
|
+
@back -= 1
|
|
140
|
+
result = (result << 8) | @data.getbyte(@back)
|
|
141
|
+
offset += 8
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if range == (1 << offset) - 1
|
|
145
|
+
# range+1 is a power of 2, modulo is identity
|
|
146
|
+
min + result
|
|
147
|
+
else
|
|
148
|
+
min + (result % (range + 1))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Consume a boolean from the end of the buffer.
|
|
153
|
+
# Returns false when no data remains.
|
|
154
|
+
def consume_bool
|
|
155
|
+
(consume_uint(1) & 1) == 1
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# --- Float methods (consume from end) ---
|
|
159
|
+
|
|
160
|
+
# Consume a Float in [0.0, 1.0] from the end of the buffer.
|
|
161
|
+
# Returns 0.0 when no data remains.
|
|
162
|
+
def consume_probability
|
|
163
|
+
raw = consume_uint(8)
|
|
164
|
+
raw.to_f / 18_446_744_073_709_551_615.0 # 2^64 - 1
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Consume a Float in [min, max] from the end of the buffer.
|
|
168
|
+
# Returns +min+ when no data remains.
|
|
169
|
+
# Raises ArgumentError if min > max.
|
|
170
|
+
def consume_float_in_range(min, max)
|
|
171
|
+
raise ArgumentError, 'min must be <= max' if min > max
|
|
172
|
+
return min if min == max
|
|
173
|
+
|
|
174
|
+
range = max - min
|
|
175
|
+
if range.infinite?
|
|
176
|
+
# Overflow: split the range and recurse
|
|
177
|
+
mid = min / 2.0 + max / 2.0
|
|
178
|
+
if consume_bool
|
|
179
|
+
consume_float_in_range(mid, max)
|
|
180
|
+
else
|
|
181
|
+
consume_float_in_range(min, mid)
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
min + range * consume_probability
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Consume a Float spanning the full double range.
|
|
189
|
+
# Matches libFuzzer's ConsumeFloatingPoint.
|
|
190
|
+
def consume_float
|
|
191
|
+
consume_float_in_range(-Float::MAX, Float::MAX)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# --- Selection methods ---
|
|
195
|
+
|
|
196
|
+
# Return a random element from +list+, consuming bytes from the end.
|
|
197
|
+
# Raises ArgumentError if the list is empty.
|
|
198
|
+
def pick_value_in_list(list)
|
|
199
|
+
raise ArgumentError, 'list must not be empty' if list.empty?
|
|
200
|
+
|
|
201
|
+
list[consume_int_in_range(0, list.length - 1)]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
# Consume a single byte from the front.
|
|
207
|
+
def consume_front_byte
|
|
208
|
+
return 0 if remaining_bytes.zero?
|
|
209
|
+
|
|
210
|
+
byte = @data.getbyte(@front)
|
|
211
|
+
@front += 1
|
|
212
|
+
byte
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def clamp_count(count)
|
|
216
|
+
count = 0 if count.negative?
|
|
217
|
+
count = remaining_bytes if count > remaining_bytes
|
|
218
|
+
count
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
data/lib/ruzzy.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'pathname'
|
|
4
|
+
require 'ruzzy/fuzzed_data_provider'
|
|
4
5
|
|
|
5
6
|
# A coverage-guided fuzzer for pure Ruby code and Ruby C extensions
|
|
6
7
|
module Ruzzy
|
|
@@ -25,6 +26,11 @@ module Ruzzy
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def dummy
|
|
29
|
+
# Load the instrumented shared object before calling fuzz so its coverage
|
|
30
|
+
# maps are registered before LLVMFuzzerRunDriver starts. Some fuzzer
|
|
31
|
+
# runtimes (e.g. LibAFL) require coverage maps to exist upfront.
|
|
32
|
+
require 'dummy/dummy'
|
|
33
|
+
|
|
28
34
|
fuzz(->(data) { dummy_test_one_input(data) })
|
|
29
35
|
end
|
|
30
36
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruzzy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Trail of Bits
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake
|
|
@@ -66,7 +65,6 @@ dependencies:
|
|
|
66
65
|
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '1.60'
|
|
69
|
-
description:
|
|
70
68
|
email: support@trailofbits.com
|
|
71
69
|
executables: []
|
|
72
70
|
extensions:
|
|
@@ -79,11 +77,11 @@ files:
|
|
|
79
77
|
- ext/dummy/dummy.c
|
|
80
78
|
- ext/dummy/extconf.rb
|
|
81
79
|
- lib/ruzzy.rb
|
|
80
|
+
- lib/ruzzy/fuzzed_data_provider.rb
|
|
82
81
|
homepage: https://rubygems.org/gems/ruzzy
|
|
83
82
|
licenses:
|
|
84
83
|
- AGPL-3.0-only
|
|
85
84
|
metadata: {}
|
|
86
|
-
post_install_message:
|
|
87
85
|
rdoc_options: []
|
|
88
86
|
require_paths:
|
|
89
87
|
- lib
|
|
@@ -98,8 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
96
|
- !ruby/object:Gem::Version
|
|
99
97
|
version: '0'
|
|
100
98
|
requirements: []
|
|
101
|
-
rubygems_version:
|
|
102
|
-
signing_key:
|
|
99
|
+
rubygems_version: 4.0.6
|
|
103
100
|
specification_version: 4
|
|
104
101
|
summary: A coverage-guided fuzzer for pure Ruby code and Ruby C extensions
|
|
105
102
|
test_files: []
|