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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64fa385ff3dca5a231ebc02511f7240143838b4cf4314a8ced35a57127ac2dc2
4
- data.tar.gz: 1cd94db1a0b30debdc03caddf39394b3759b11f85d0b2ef78037e5769b0761ed
3
+ metadata.gz: 7d564227ec95d197cadc085f2190a7089fbe5610d4e6f7c9d9ff5a84a4243208
4
+ data.tar.gz: aa7c37d65b75d202e650832ffad9589c3a6a750d162760d0af934256268115aa
5
5
  SHA512:
6
- metadata.gz: ed77800fbbe359a7b2be3e215b00d052b2bf8f1038da0e2ad88c6a8ea9d66bcf2d875ed5658592262ef35a18a23c4b1f5fcc9d44e7f669fc81817310dda96122
7
- data.tar.gz: 80ef311558dd4c4aa88697014e08a6b06a164d182a16d5c4718ce9355bc6b2f82c52bce11d3eaa1d1071df2a900eef9ef46148013ad7099ed1fbf07df173b6c3
6
+ metadata.gz: 54cc91f765a3622756e3925f62e29003ef3169a8a413af4b450b320bdea979125ede2a203be2b2fac52114abf52b3b735f8d97e395f9181eb01f080b09648988
7
+ data.tar.gz: cc8895f40905092d68bbdad2557fd691c189b04f336bb360c0f0a1b3f82a52d0227f82e966248b2b70cfd694b35e464ecd967a76d0e0426e45f5ffa2e941444f
@@ -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
- fuzzer_no_main_libs = [
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
- unless fuzzer_no_main_lib
87
- LOGGER.warn("Could not find fuzzer_no_main using #{CC}.")
88
- fuzzer_no_main_lib = ENV.fetch(FUZZER_NO_MAIN_LIB_ENV, nil)
89
- if fuzzer_no_main_lib.nil?
90
- LOGGER.error("Could not find fuzzer_no_main in #{FUZZER_NO_MAIN_LIB_ENV}.")
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.7.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: 2024-03-28 00:00:00.000000000 Z
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: 3.5.3
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: []