Class: Ruzzy::FuzzedDataProvider

Inherits:
Object
  • Object
show all
Defined in:
lib/ruzzy/fuzzed_data_provider.rb

Overview

Splits raw fuzzer bytes into typed Ruby values.

FuzzedDataProvider wraps a binary string (typically from libFuzzer via Ruzzy.fuzz) and provides methods to consume typed values from it. This enables fuzz targets that test APIs accepting typed arguments rather than raw byte strings.

Following libFuzzer’s FuzzedDataProvider.h design, strings and raw bytes are consumed from the front of the buffer, while integers are consumed from the end. This bidirectional consumption lets the fuzzer modify structural decisions (integers controlling lengths, indices, variant selection) independently from content (string payloads, raw bytes), improving mutation quality.

Examples:

Basic usage in a fuzz target

test_one_input = lambda do |data|
  fdp = Ruzzy::FuzzedDataProvider.new(data)
  name = fdp.consume_random_length_string(50)
  age = fdp.consume_int_in_range(0, 150)
  score = fdp.consume_float_in_range(0.0, 100.0)
  role = fdp.pick_value_in_list(['admin', 'user', 'guest'])
  User.new(name: name, age: age, score: score, role: role).validate!
end
Ruzzy.fuzz(test_one_input)

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ FuzzedDataProvider

Returns a new instance of FuzzedDataProvider.



29
30
31
32
33
34
35
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 29

def initialize(data)
  @data = data
  # Front cursor for strings/bytes (advances forward)
  @front = 0
  # Back cursor for integers (advances backward)
  @back = @data.bytesize
end

Instance Method Details

#consume_boolObject

Consume a boolean from the end of the buffer. Returns false when no data remains.



154
155
156
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 154

def consume_bool
  (consume_uint(1) & 1) == 1
end

#consume_bytes(count) ⇒ Object

Consume up to count raw bytes from the front of the buffer. Returns a binary-encoded String.



46
47
48
49
50
51
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 46

def consume_bytes(count)
  count = clamp_count(count)
  result = @data.byteslice(@front, count)
  @front += count
  result.force_encoding(Encoding::BINARY)
end

#consume_floatObject

Consume a Float spanning the full double range. Matches libFuzzer’s ConsumeFloatingPoint.



190
191
192
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 190

def consume_float
  consume_float_in_range(-Float::MAX, Float::MAX)
end

#consume_float_in_range(min, max) ⇒ Object

Consume a Float in [min, max] from the end of the buffer. Returns min when no data remains. Raises ArgumentError if min > max.

Raises:

  • (ArgumentError)


170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 170

def consume_float_in_range(min, max)
  raise ArgumentError, 'min must be <= max' if min > max
  return min if min == max

  range = max - min
  if range.infinite?
    # Overflow: split the range and recurse
    mid = min / 2.0 + max / 2.0
    if consume_bool
      consume_float_in_range(mid, max)
    else
      consume_float_in_range(min, mid)
    end
  else
    min + range * consume_probability
  end
end

#consume_int(count) ⇒ Object

Consume a signed integer from the end of the buffer. Reads count bytes and interprets as two’s complement. Returns 0 when no data remains.



111
112
113
114
115
116
117
118
119
120
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 111

def consume_int(count)
  unsigned = consume_uint(count)
  return 0 if count.zero?

  bits = count * 8
  max_unsigned = 1 << bits
  half = max_unsigned >> 1

  unsigned >= half ? unsigned - max_unsigned : unsigned
end

#consume_int_in_range(min, max) ⇒ Object

Consume an integer in [min, max] from the end of the buffer. Returns min when no data remains. Raises ArgumentError if min > max.

Matches libFuzzer’s ConsumeIntegralInRange: consumes only as many bytes from the end as needed to cover the range.

Raises:

  • (ArgumentError)


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 128

def consume_int_in_range(min, max)
  raise ArgumentError, "min (#{min}) must be <= max (#{max})" if min > max

  range = max - min
  return min if range.zero?

  # Consume bytes from the end, one at a time, until we've covered the range.
  # This matches libFuzzer: only consume bytes while (range >> offset) > 0.
  result = 0
  offset = 0
  while offset < 64 && (range >> offset).positive? && remaining_bytes.positive?
    @back -= 1
    result = (result << 8) | @data.getbyte(@back)
    offset += 8
  end

  if range == (1 << offset) - 1
    # range+1 is a power of 2, modulo is identity
    min + result
  else
    min + (result % (range + 1))
  end
end

#consume_probabilityObject

Consume a Float in [0.0, 1.0] from the end of the buffer. Returns 0.0 when no data remains.



162
163
164
165
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 162

def consume_probability
  raw = consume_uint(8)
  raw.to_f / 18_446_744_073_709_551_615.0 # 2^64 - 1
end

#consume_random_length_string(max_length = remaining_bytes) ⇒ Object

Consume a variable-length string from the front of the buffer. The string terminates when a backslash followed by a non-backslash byte is encountered, or when max_length characters are consumed. This encoding lets the fuzzer easily control string length through single-byte mutations.

Matches libFuzzer’s ConsumeRandomLengthString.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 60

def consume_random_length_string(max_length = remaining_bytes)
  result = +''
  max_length.times do
    break if remaining_bytes.zero?

    byte = consume_front_byte
    char = byte.chr(Encoding::BINARY)

    if char == '\\' && remaining_bytes.positive?
      next_byte = consume_front_byte
      next_char = next_byte.chr(Encoding::BINARY)
      break if next_char != '\\'

      result << '\\'
    else
      result << char
    end
  end
  result
end

#consume_remaining_as_stringObject

Consume all remaining bytes as a String.



87
88
89
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 87

def consume_remaining_as_string
  consume_remaining_bytes
end

#consume_remaining_bytesObject

Consume all remaining bytes. Returns a binary-encoded String.



82
83
84
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 82

def consume_remaining_bytes
  consume_bytes(remaining_bytes)
end

#consume_uint(count) ⇒ Object

Consume an unsigned integer from the end of the buffer. Reads up to count bytes in little-endian order from the back. Returns 0 when no data remains.



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 96

def consume_uint(count)
  return 0 if count <= 0 || remaining_bytes.zero?

  actual = [count, remaining_bytes].min
  result = 0
  actual.times do |i|
    @back -= 1
    result |= @data.getbyte(@back) << (i * 8)
  end
  result
end

#pick_value_in_list(list) ⇒ Object

Return a random element from list, consuming bytes from the end. Raises ArgumentError if the list is empty.

Raises:

  • (ArgumentError)


198
199
200
201
202
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 198

def pick_value_in_list(list)
  raise ArgumentError, 'list must not be empty' if list.empty?

  list[consume_int_in_range(0, list.length - 1)]
end

#remaining_bytesObject

Returns the number of unconsumed bytes remaining.



38
39
40
# File 'lib/ruzzy/fuzzed_data_provider.rb', line 38

def remaining_bytes
  @back - @front
end