fuzzbert 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -22,21 +22,7 @@ For further information on random testing, here are two excellent starting point
22
22
  ## Defining a random test
23
23
 
24
24
  FuzzBert defines an RSpec-like DSL that can be used to define different fuzzing
25
- scenarios. The DSL uses three words: `fuzz`, `deploy` and `data`. `fuzz` can be
26
- thought of as defining a new scenario, such as "fuzz this command line tool",
27
- "fuzz this particular URL of my web app" or "fuzz this library method taking
28
- external input".
29
-
30
- Within a `fuzz` block, there must be one occurence of `deploy` and one or several
31
- occurences of `data`. The `deploy` block is the spot where we deliver the random
32
- payload that has been generated. It is agnostic about the actual target in order to
33
- leave you free to fuzz whatever you require in your particular case. The `data`
34
- blocks define the shape of the random data being generated. There can be more than
35
- one such block because it is often beneficial to not only shoot completely random
36
- data at the target - you often want to deliver more structured data as well, trying
37
- to find the edge cases deeper within your code. Good random test suites make use
38
- of both - totally random data as well as structured data - in order to cover as
39
- much "code surface" as possible.
25
+ scenarios. The DSL uses three words: `fuzz`, `deploy` and `data`.
40
26
 
41
27
  Here is a quick example that fuzzes `JSON.parse`:
42
28
 
@@ -78,6 +64,21 @@ fuzz "JSON.parse" do
78
64
  end
79
65
  ```
80
66
 
67
+ `fuzz` can be thought of as defining a new scenario, such as "fuzz this command
68
+ line tool", "fuzz this particular URL of my web app" or "fuzz this library method
69
+ taking external input".
70
+
71
+ Within a `fuzz` block, there must be one occurence of `deploy` and one or several
72
+ occurences of `data`. The `deploy` block is the spot where we deliver the random
73
+ payload that has been generated. It is agnostic about the actual target in order to
74
+ leave you free to fuzz whatever you require in your particular case. The `data`
75
+ blocks define the shape of the random data being generated. There can be more than
76
+ one such block because it is often beneficial to not only shoot completely random
77
+ data at the target - you often want to deliver more structured data as well, trying
78
+ to find the edge cases deeper within your code. Good random test suites make use
79
+ of both - totally random data as well as structured data - in order to cover as
80
+ much "code surface" as possible.
81
+
81
82
  The `deploy` block takes the generated data as a parameter. The block itself is
82
83
  responsible of deploying the payload. An execution is considered successful if
83
84
  the `deploy` block passes with no uncaught error being raised. If an error slips
@@ -88,14 +89,91 @@ considered as a failure.
88
89
  choose completely custom lambdas of your own or use those predefined for you in
89
90
  `FuzzBert::Generators`.
90
91
 
92
+ ## Running a random test
93
+
94
+ Once the FuzzBert files are set up, you may run your tests similar to how you
95
+ would run unit tests:
96
+
97
+ fuzzbert "fuzz/**/fuzz_*.rb"
98
+
99
+ If your FuzzBert files are already in a directory named 'fuzz' and each of them
100
+ begins with 'fuzz_', you may omit the pattern altogether.
101
+
102
+ Each `fuzz` block defines a `TestSuite`. These are executed in a round-robin manner.
103
+ Each individual `TestSuite` will then apply the `deploy` block with a sample of
104
+ data generated successively by each one of the `data` blocks. Once all `data` blocks
105
+ are used up, the next `TestSuite` will be executed etc. By default, a FuzzBert
106
+ fuzzing session runs forever, until the process is either killed or by manually hitting
107
+ `CTRL+C` for example. This was a deliberate design choice since random testing suites
108
+ need to be run for quite some time to be effective. It's something you want to run over
109
+ the weekend rather than for a couple of minutes. Still, it can make sense to explicitly
110
+ limit the number of runs, for example when integrating FuzzBert with a CI server or
111
+ with Travis. You can do so by passing the `--limit` parameter to the `fuzzbert`
112
+ executable:
113
+
114
+ fuzzbert --limit 1000000 "fuzz/**/fuzz_*.rb"
115
+
116
+ Every single execution of `deploy` is run in a separate process. The main reason for
117
+ this is that we typically want to detect hard crashes when a C extension or even Ruby
118
+ itself encounters an input it can't handle. Besides being able to cope with these cases,
119
+ running in separate processes proves beneficial otherwise as well: by default, FuzzBert
120
+ runs the tests in four separate processes at once, therefore utilizing your CPU's cores
121
+ effectively. You can tweak that setting with `--pool-size` to set this number to 1
122
+ (for completely sequential runs) or to the exact number of cores your CPU offers.
123
+
124
+ fuzzbert --pool-size 1 my/fuzzbert/file
125
+
126
+ ## What happens if we encounter a bug?
127
+
128
+ If a test should end up failing (either the process crashed completely or caused an
129
+ uncaught error), FuzzBert will output the failing test on your terminal and tell you
130
+ where it stored the data that caused this. This conveniently allows you to run FuzzBert
131
+ over the weekend and when you return on Monday, the troubleshooters will sit there all
132
+ lined up for you to go through and filter. By using the `--console` command line switch
133
+ you can tell FuzzBert to not explicitly store the data, but echoing the data that
134
+ caused the crash to the terminal instead.
135
+
136
+ fuzzbert --console "fuzz/**/fuzz_*.rb"
137
+
138
+ If you don't want to litter your current working directory with the files generated
139
+ by FuzzBert, you can also specify a specific path to where they should be saved
140
+ instead:
141
+
142
+ fuzzbert --bug-dir bugs "fuzz/**/fuzz_*.rb"
143
+
144
+ This is still not quite what you want to happen in case a test crashes? There's
145
+ also the possibility to define a handler of your own:
146
+
147
+ ```ruby
148
+ require 'fuzzbert'
149
+
150
+ class MyHandler
151
+ def handle(error_data)
152
+ #create an issue in the bug tracker
153
+ puts error_data[:id]
154
+ p error_data[:data]
155
+ puts error_data[:pid]
156
+ puts error_data[:status]
157
+ end
158
+ end
159
+
160
+ fuzz "Define here as usual" do
161
+ ...
162
+ end
163
+ ```
164
+
165
+ Then tell fuzzbert by telling it about the custom handler:
166
+
167
+ fuzzbert --handler MyHandler my/fuzzbert/file
168
+
91
169
  ## Templates
92
170
 
93
171
  Using the approach described so far is most useful for binary protocols, but as
94
- soon as largely String-based data is needed it can soon become tedious. What you
95
- actually want in these situations is some sort of template mechanism that comes
96
- with mostly fixed data and only replaces a few selected parts with randomly
97
- generated data. This, too, is possible with FuzzBert, it comes with a minimal
98
- templating language:
172
+ soon as you work with mainly String-based data this can quickly become a chore.
173
+ What you actually want in these situations is some sort of template mechanism
174
+ that comes with mostly fixed data and only replaces a few selected parts with
175
+ randomly generated data. This, too, is possible with FuzzBert, it comes with a
176
+ minimal templating language:
99
177
 
100
178
  ```ruby
101
179
  require 'fuzzbert'
@@ -119,6 +197,10 @@ fuzz "My Web App" do
119
197
  end
120
198
  ```
121
199
 
200
+ Simply specify your template variables using `${..}` and assign a callback for
201
+ them via `set`. Of course you may escape the dollar sign with a back slash as
202
+ usual.
203
+
122
204
  ## Mutators
123
205
 
124
206
  Mutation is the principle used in "Babysitting an Army of Monkeys". The basis for
@@ -141,37 +223,8 @@ fuzz "Web App" do
141
223
  end
142
224
  ```
143
225
 
144
- ## How the tests are performed
145
-
146
- Each `fuzz` block defines a `TestSuite`. These are executed in a round-robin manner.
147
- Each individual `TestSuite` will then apply the `deploy` block with a sample of
148
- data generated successively by each one of the `data` blocks. Once all `data` blocks
149
- were used, the next `TestSuite` will be executed and so on... By default, a FuzzBert
150
- fuzzing session runs forever, until the process is either killed or by manually hitting
151
- `CTRL+C` for example. This was a deliberate design choice since random testing suites
152
- need to be run for quite some time to be effective. It's something you want to run over
153
- the weekend rather than for a couple of minutes. Still, it can make sense to explicitly
154
- limit the number of runs, for example when integrating FuzzBert with a CI server or
155
- with Travis. You can do so by passing the `--limit` parameter to the `fuzzbert`
156
- executable.
157
-
158
- Every single execution of `deploy` is run in a separate process. The main reason for
159
- this is that we typically want to detect hard crashes when a C extension or even Ruby
160
- itself encounters an input it can't handle. Besides being able to cope with these cases,
161
- running in separate processes proves beneficial otherwise as well: by default, FuzzBert
162
- runs the tests in four separate processes at once, therefore utilizing your CPU's cores
163
- effectively. You can tweak that setting with `--pool-size` to set this number to 1
164
- (for completely sequential runs) or to the exact number of cores your CPU offers.
165
-
166
- ## What happens if we encounter a bug?
167
-
168
- If a test should end up failing (either the process crashed completely or caused an
169
- uncaught error), FuzzBert will output the failing test on your terminal and tell you
170
- where it stored the data that caused this. This conveniently allows you to run FuzzBert
171
- over the weekend and when you return on Monday, the troubleshooters will sit there all
172
- lined up for you to go through and filter. By using the `--console` command line switch
173
- you can tell FuzzBert to not explicitly store the data, but echoing the data that
174
- caused the crash to the terminal instead.
226
+ This will take the original JSON data and modify one byte each time data is being
227
+ generated.
175
228
 
176
229
  ## Rake integration
177
230
 
@@ -17,49 +17,82 @@ module FuzzBert::AutoRun
17
17
  end
18
18
 
19
19
  def run(options=nil)
20
- executor = options ? FuzzBert::Executor.new(TEST_CASES, options) : FuzzBert::Executor.new(TEST_CASES)
21
- executor.run
20
+ raise RuntimeError.new "No test cases were found" if TEST_CASES.empty?
21
+ FuzzBert::Executor.new(TEST_CASES, options).run
22
22
  end
23
23
 
24
24
  private; module_function
25
25
 
26
- def load_files(files)
27
- files.each do |pattern|
28
- Dir.glob(pattern).each { |f| load File.expand_path(f) }
26
+ def load_files(files)
27
+ files.each do |pattern|
28
+ Dir.glob(pattern).each { |f| load File.expand_path(f) }
29
+ end
29
30
  end
30
- end
31
31
 
32
- def process_args(args = [])
33
- options = {}
34
- orig_args = args.dup
32
+ def process_args(args = [])
33
+ options = {}
34
+ orig_args = args.dup
35
35
 
36
- OptionParser.new do |opts|
37
- opts.banner = 'FuzzBert options:'
38
- opts.version = FuzzBert::VERSION
36
+ OptionParser.new do |opts|
37
+ opts.banner = 'Usage: fuzzbert [OPTIONS] PATTERN [PATTERNS]'
38
+ opts.separator <<-EOS
39
39
 
40
- opts.on '-h', '--help', 'Display this help.' do
41
- puts opts
42
- exit
43
- end
40
+ Run your random tests by pointing fuzzbert to a single or many explicit files
41
+ or by providing a pattern. The default pattern is 'fuzz/**/fuzz_*.rb, assuming
42
+ that your FuzzBert files (files beginning with 'fuzz_') live in a directory
43
+ named fuzz located under the current directory that you are in.
44
44
 
45
- opts.on '--pool-size SIZE', Integer, "Sets the number of concurrently running processes to SIZE" do |n|
46
- options[:pool_size] = n.to_i
47
- end
45
+ By default, fuzzbert will run the tests you specify forever, be sure to hit
46
+ CTRL+C when you are done or specify a limit with '--limit'.
48
47
 
49
- opts.on '--limit LIMIT', Integer, "Instead of running permanently, fuzzing will be stopped after running LIMIT of instances" do |n|
50
- p n
51
- options[:limit] = n.to_i
52
- end
48
+ EOS
49
+
50
+ opts.version = FuzzBert::VERSION
51
+
52
+ opts.on '-h', '--help', 'Run ' do
53
+ puts opts
54
+ exit
55
+ end
56
+
57
+ opts.on '--pool-size SIZE', Integer, "Sets the number of concurrently running processes to SIZE. Default is 4." do |n|
58
+ options[:pool_size] = n.to_i
59
+ end
53
60
 
54
- opts.on '--console', "Output the failing cases including data on the console instead of saving them in a file" do
55
- options[:handler] = FuzzBert::Handler::Console.new
61
+ opts.on '--limit LIMIT', Integer, "Instead of running permanently, fuzzing will be stopped after running LIMIT of instances." do |n|
62
+ options[:limit] = n.to_i
63
+ end
64
+
65
+ opts.on '--console', "Output the failing cases including data on the console instead of saving them in a file." do
66
+ options[:handler] = FuzzBert::Handler::Console.new
67
+ end
68
+
69
+ opts.on '--sleep-delay SECONDS', Float, "Specify the number of SECONDS that the main process sleeps before checking that the limit has been reached. Default is 1." do |f|
70
+ options[:sleep_delay] = f.to_f
71
+ end
72
+
73
+ opts.on '--handler CLASS', String, "Specify the full path to a CLASS that will serve as your Handler." do |path|
74
+ #lazy initialization: the Handler must be defined in one of the fuzzer files
75
+ options[:handler] = Class.new do
76
+ @@path = path
77
+
78
+ def handle(id, data, pid, status)
79
+ @inner ||= Object.const_get(@@path).new
80
+ @inner.handle(id, data, pid, status)
81
+ end
82
+ end.new
83
+ end
84
+
85
+ opts.on '--bug-dir DIRECTORY', String, "The DIRECTORY where the resulting bug files will be stored. Default is the current directory." do |dir|
86
+ raise ArgumentError.new "#{dir} is not a directory" unless Dir.exists?(dir)
87
+ options[:handler] = FuzzBert::Handler::FileOutput.new(dir)
88
+ end
89
+
90
+ opts.parse! args
56
91
  end
57
92
 
58
- opts.parse! args
93
+ raise ArgumentError.new("No file pattern was given") if args.empty?
94
+ [options, args]
59
95
  end
60
96
 
61
- [options, args]
62
- end
63
-
64
97
  end
65
98
 
@@ -1,6 +1,8 @@
1
1
  module FuzzBert::DSL
2
2
  def fuzz(*args, &blk)
3
3
  suite = FuzzBert::TestSuite.create(*args, &blk)
4
+ raise RuntimeError.new "No 'deploy' block was given" unless suite.test
5
+ raise RuntimeError.new "No 'data' blocks were given" unless suite.generators
4
6
  FuzzBert::AutoRun.register(suite)
5
7
  end
6
8
  end
@@ -1,19 +1,67 @@
1
1
 
2
2
  module FuzzBert::Handler
3
+
4
+ module ConsoleHelper
5
+ def info(error_data)
6
+ id = error_data[:id]
7
+ status = error_data[:status]
8
+
9
+ crashed = status.termsig
10
+
11
+ if crashed
12
+ puts "The data caused a hard crash."
13
+ else
14
+ puts "The data caused an uncaught error."
15
+ end
16
+ end
17
+ end
18
+
3
19
  class FileOutput
4
- def handle(id, data, pid, status)
5
- prefix = status.termsig ? "crash" : "bug"
6
- filename = "#{prefix}#{pid}"
20
+ include FuzzBert::Handler::ConsoleHelper
21
+
22
+ def initialize(dir=nil)
23
+ @dir = dir
24
+ if @dir && !@dir.end_with?("/")
25
+ @dir << "/"
26
+ end
27
+ end
28
+
29
+ def handle(error_data)
30
+ id = error_data[:id]
31
+ data = error_data[:data]
32
+ status = error_data[:status]
33
+ pid = error_data[:pid]
34
+
35
+ crashed = status.termsig
36
+ prefix = crashed ? "crash" : "bug"
37
+
38
+ filename = "#{dir_prefix}#{prefix}#{pid}"
39
+ while File.exists?(filename)
40
+ filename << ('a'..'z').to_a.sample
41
+ end
7
42
  File.open(filename, "wb") { |f| f.print(data) }
43
+
8
44
  puts "#{id} failed. Data was saved as #{filename}."
45
+ info(error_data)
9
46
  end
47
+
48
+ private
49
+
50
+ def dir_prefix
51
+ return "./" unless @dir
52
+ @dir
53
+ end
10
54
  end
11
55
 
12
56
  class Console
13
- def handle(id, data, pid, status)
14
- puts "#{id} failed. Data: #{data.inspect}"
57
+ include FuzzBert::Handler::ConsoleHelper
58
+
59
+ def handle(error_data)
60
+ puts "#{error_data[:id]} failed. Data: #{error_data[:data].inspect}"
61
+ info(error_data)
15
62
  end
16
63
  end
64
+
17
65
  end
18
66
 
19
67
 
@@ -1,19 +1,27 @@
1
1
  class FuzzBert::Executor
2
2
 
3
- attr_reader :pool_size, :limit, :handler
3
+ attr_reader :pool_size, :limit, :handler, :sleep_delay
4
4
 
5
5
  DEFAULT_POOL_SIZE = 4
6
6
  DEFAULT_LIMIT = -1
7
7
  DEFAULT_HANDLER = FuzzBert::Handler::FileOutput
8
+ DEFAULT_SLEEP_DELAY = 1
8
9
 
9
- def initialize(suites, args = {
10
+ DEFAULT_ARGS = {
10
11
  pool_size: DEFAULT_POOL_SIZE,
11
12
  limit: DEFAULT_LIMIT,
12
- handler: DEFAULT_HANDLER.new
13
- })
13
+ handler: DEFAULT_HANDLER.new,
14
+ sleep_delay: DEFAULT_SLEEP_DELAY
15
+ }
16
+
17
+ def initialize(suites, args = DEFAULT_ARGS)
18
+ raise ArgumentError.new("No test cases were passed") unless suites
19
+
20
+ args ||= DEFAULT_ARGS
14
21
  @pool_size = args[:pool_size] || DEFAULT_POOL_SIZE
15
22
  @limit = args[:limit] || DEFAULT_LIMIT
16
23
  @handler = args[:handler] || DEFAULT_HANDLER.new
24
+ @sleep_delay = args[:sleep_delay] || DEFAULT_SLEEP_DELAY
17
25
  @data_cache = {}
18
26
  @n = 0
19
27
  @exiting = false
@@ -31,123 +39,129 @@ class FuzzBert::Executor
31
39
 
32
40
  private
33
41
 
34
- def run_instance(description, test, generator)
35
- data = generator.to_data
36
- pid = fork do
37
- begin
38
- test.run(data)
39
- rescue StandardError
40
- abort
42
+ def run_instance(description, test, generator)
43
+ data = generator.to_data
44
+ pid = fork do
45
+ begin
46
+ test.run(data)
47
+ rescue StandardError
48
+ abort
49
+ end
41
50
  end
51
+ id = "#{description}/#{generator.description}"
52
+ @data_cache[pid] = [id, data]
42
53
  end
43
- id = "#{description}/#{generator.description}"
44
- @data_cache[pid] = [id, data]
45
- end
46
54
 
47
- def trap_child_exit
48
- trap(:CHLD) do
49
- begin
50
- while exitval = Process.wait2(-1, Process::WNOHANG)
51
- pid = exitval[0]
52
- status = exitval[1]
53
- data_ary = @data_cache.delete(pid)
54
- unless status.success?
55
- handle(data_ary[0], data_ary[1], pid, status) unless interrupted(status)
56
- end
57
- @n += 1
58
- if @limit == -1 || @n < @limit
59
- run_instance(*@producer.next)
60
- else
61
- @running = false
55
+ def trap_child_exit
56
+ trap(:CHLD) do
57
+ begin
58
+ while exitval = Process.wait2(-1, Process::WNOHANG)
59
+ pid = exitval[0]
60
+ status = exitval[1]
61
+ data_ary = @data_cache.delete(pid)
62
+ unless status.success?
63
+ handle({
64
+ id: data_ary[0],
65
+ data: data_ary[1],
66
+ pid: pid,
67
+ status: status
68
+ }) unless interrupted(status)
69
+ end
70
+ @n += 1
71
+ if @limit == -1 || @n < @limit
72
+ run_instance(*@producer.next)
73
+ else
74
+ @running = false
75
+ end
62
76
  end
77
+ rescue Errno::ECHILD
63
78
  end
64
- rescue Errno::ECHILD
65
79
  end
66
80
  end
67
- end
68
81
 
69
- def trap_interrupt
70
- trap(:INT) do
71
- exit! (1) if @exiting
72
- @exiting = true
73
- graceful_exit
82
+ def trap_interrupt
83
+ trap(:INT) do
84
+ exit! (1) if @exiting
85
+ @exiting = true
86
+ graceful_exit
87
+ end
74
88
  end
75
- end
76
89
 
77
- def graceful_exit
78
- puts "\nExiting...Interrupt again to exit immediately"
79
- begin
80
- while Process.wait; end
81
- rescue Errno::ECHILD
90
+ def graceful_exit
91
+ puts "\nExiting...Interrupt again to exit immediately"
92
+ begin
93
+ while Process.wait; end
94
+ rescue Errno::ECHILD
95
+ end
96
+ exit 0
82
97
  end
83
- exit 0
84
- end
85
-
86
- def handle(id, data, pid, status)
87
- @handler.handle(id, data, pid, status)
88
- end
89
98
 
90
- def interrupted(status)
91
- return false if status.exited?
92
- return true if status.termsig == nil || status.termsig == 2
93
- end
94
-
95
- def conditional_sleep
96
- sleep 0.1 until @running == false
97
- end
99
+ def handle(error_data)
100
+ @handler.handle(error_data)
101
+ end
98
102
 
99
- class DataProducer
100
- def initialize(suites)
101
- @ring = Ring.new(suites)
102
- update
103
+ def interrupted(status)
104
+ return false if status.exited?
105
+ return true if status.termsig == nil || status.termsig == 2
103
106
  end
104
107
 
105
- def update
106
- @suite = @ring.next
107
- @gen_iter = ProcessSafeEnumerator.new(@suite.generators)
108
+ def conditional_sleep
109
+ sleep @sleep_delay until @running == false
108
110
  end
109
111
 
110
- def next
111
- gen = nil
112
- until gen
113
- begin
114
- gen = @gen_iter.next
115
- rescue StopIteration
116
- update
117
- end
112
+ class DataProducer
113
+ def initialize(suites)
114
+ @ring = Ring.new(suites)
115
+ update
118
116
  end
119
- [@suite.description, @suite.test, gen]
120
- end
121
-
122
- class Ring
123
- def initialize(objs)
124
- @i = 0
125
- objs = [objs] unless objs.respond_to?(:each)
126
- @objs = objs.to_a
117
+
118
+ def update
119
+ @suite = @ring.next
120
+ @gen_iter = ProcessSafeEnumerator.new(@suite.generators)
127
121
  end
128
122
 
129
123
  def next
130
- obj = @objs[@i]
131
- @i = (@i + 1) % @objs.size
132
- obj
124
+ gen = nil
125
+ until gen
126
+ begin
127
+ gen = @gen_iter.next
128
+ rescue StopIteration
129
+ update
130
+ end
131
+ end
132
+ [@suite.description, @suite.test, gen]
133
133
  end
134
- end
134
+
135
+ class Ring
136
+ def initialize(objs)
137
+ @i = 0
138
+ objs = [objs] unless objs.respond_to?(:each)
139
+ @objs = objs.to_a
140
+ raise ArgumentError.new("No test cases found") if @objs.empty?
141
+ end
135
142
 
136
- #needed because the Fiber used for normal Enumerators has race conditions
137
- class ProcessSafeEnumerator
138
- def initialize(ary)
139
- @i = 0
140
- @ary = ary.to_a
143
+ def next
144
+ obj = @objs[@i]
145
+ @i = (@i + 1) % @objs.size
146
+ obj
147
+ end
141
148
  end
142
149
 
143
- def next
144
- obj = @ary[@i]
145
- raise StopIteration unless obj
146
- @i += 1
147
- obj
150
+ #needed because the Fiber used for normal Enumerators has race conditions
151
+ class ProcessSafeEnumerator
152
+ def initialize(ary)
153
+ @i = 0
154
+ @ary = ary.to_a
155
+ end
156
+
157
+ def next
158
+ obj = @ary[@i]
159
+ raise StopIteration unless obj
160
+ @i += 1
161
+ obj
162
+ end
148
163
  end
149
164
  end
150
- end
151
165
 
152
166
  end
153
167