frr-cli-fuzzer 0.1.0 → 1.0.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
  SHA1:
3
- metadata.gz: 378f404d19c9beb961473414202aa8f24e5faad8
4
- data.tar.gz: 5a5aff0028114fc40add71aba2a389e555c3c0e1
3
+ metadata.gz: 5e8ad21ba8f6f41b93c2d61d4d68dd123a5707c0
4
+ data.tar.gz: 0dec0e76549f98d5b8e8c6eb76106d83cda3d647
5
5
  SHA512:
6
- metadata.gz: 55459c77035f9d77cbe35218e20c9594bb53bb8424885329e2c89f394ca72dc105f5085c26b136255a4926120a847764dabc94e044848b30d689f75788db9c14
7
- data.tar.gz: 513b03ec3aa21574310abd7663d5c2d9536d6254df5ef3c616febdb1b86b624a28664634ccfae9a7a45f474c3e80a02e7812ee07168d22cabfbd0a4d5a786f21
6
+ metadata.gz: 8d57fb7833789767b120997653f1e12264fff0d50a29b9fd25ac51562c562bf864b89d6e911a65e53b4a4305e5a10f6ce517d58c519d78234f3387592f1acbfc
7
+ data.tar.gz: a90704a5aeeedeb5a07e3547a3dbdb821c0858911e32e81ff932b4fce872af4a9ca8b52a4554858bcc43ff4ab82add1043d80e80665b1b8d7a6ad6f0596e5678
@@ -0,0 +1,8 @@
1
+ Style/MutableConstant:
2
+ Enabled: false
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/SymbolArray:
8
+ EnforcedStyle: brackets
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in frr-cli-fuzzer.gemspec
3
+ # Specify your gem"s dependencies in frr-cli-fuzzer.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -1,28 +1,95 @@
1
- # FrrCliFuzzer
1
+ # FRR CLI Fuzzer
2
+
3
+ The FRR CLI fuzzer works by executing all existing CLI commands (obtained using the `list permutations` command) and checking for segmentation faults.
4
+
5
+ This program receives as input a configuration file specifying the test parameters, which are mostly self explanatory. The [config.yml](config.yml) file can be used as a reference configuration.
6
+
7
+ The CLI fuzzer uses Linux PID, mount and network namespaces to run on a completely isolated environment, which allows multiple instances of the CLI fuzzer to run concurrently. Linux is the only supported platform.
2
8
 
3
9
  ## Installation
4
10
 
5
- After checking out the repo, run `bin/setup` to install the dependencies (currently, only the _ffi_ gem).
11
+ After checking out the repo, run `bin/setup` to install the dependencies (currently, only the _ffi_ gem):
12
+ ```
13
+ $ git clone https://github.com/rwestphal/frr-cli-fuzzer
14
+ $ cd frr-cli-fuzzer
15
+ # ./bin/setup
16
+ ```
17
+
18
+ Alternatively, install the latest version of the _frr-cli-fuzzer_ gem using the following command:
19
+ ```
20
+ # gem install frr-cli-fuzzer
21
+ ```
22
+
23
+ > NOTE: in order to install this gem it might be necessary to install the `ruby-dev` or `ruby-devel` package first.
6
24
 
7
25
  ## Usage
8
26
 
9
- Edit _config.yml_ as desired. Run the CLI fuzzer using the following command:
10
- ```sh
11
- ./bin/frr-cli-fuzzer config.yml
27
+ Edit [config.yml](config.yml) to configure the test parameters. Run the CLI fuzzer using the following command:
28
+ ```
29
+ # frr-cli-fuzzer config.yml
30
+ ```
31
+
32
+ Once the tests complete, the results are displayed in the standard output. Example:
33
+ ```
34
+ results:
35
+ - non-filtered commands: 197
36
+ - whitelist filtered commands: 0
37
+ - blacklist filtered commands: 11
38
+ - tested commands: 426
39
+ - segfaults detected: 5
40
+ (x3) ripd aborted: vtysh -c "configure terminal" -c "router rip" -c "allow-ecmp"
41
+ PIDs: 7 342 686
42
+ (x2) ripd aborted: vtysh -c "configure terminal" -c "router rip" -c "no allow-ecmp"
43
+ PIDs: 225 547
44
+ ```
45
+
46
+ The `runstatedir` (_/tmp/frr-cli-fuzzer/_ by default) directory will contain the following files:
47
+ * _segfaults.txt_: log of the detected segmentation faults.
48
+ * _*.log.<PID>_: log files of the FRR daemons.
49
+ * _*.stdout.<PID>_: capture of the standard output of the FRR daemons.
50
+ * _*.stderr.<PID>_: capture of the standard error of the FRR daemons.
51
+ * _vtysh.stdout_: capture of the standard output of vtysh.
52
+ * _vtysh.stderr_: capture of the standard error of vtysh.
53
+
54
+ It's recommend to build FRR with compiler optimizations (e.g. `-O2`) to allow the CLI fuzzer to test more commands per second.
55
+
56
+ If desired, it's possible to run multiple instances of the CLI fuzzer at the same time.
57
+ For that, each instance must use a different configuration file, and the `runstatedir` parameter (under the `fuzzer` section) must be different among all running instances to separate their running state data.
58
+
59
+ To run the CLI fuzzer for a specific amount of time, use the `timeout` command. Example:
60
+ ```
61
+ # timeout --signal=INT 12h frr-cli-fuzzer config.yml
62
+ ```
63
+
64
+ ## Core Dumps
65
+
66
+ It's suggested to enable the generation of core dumps to make it easier to debug the segfaults triggered by the CLI fuzzer. This can be done by following the steps below:
67
+ * Create the _/var/crash_ directory to store the core dumps:
68
+ ```
69
+ # mkdir /var/crash
70
+ # chmod 0777 /var/crash
12
71
  ```
13
72
 
14
- Once the tests complete, the results are displayed in the standard output like this:
73
+ * Edit _/etc/sysctl.conf_:
15
74
  ```
16
- Results:
17
- - Non-filtered commands: 465
18
- - Whitelist filtered commands: 0
19
- - Blacklist filtered commands: 20
20
- - Tested commands: 465
21
- - Segfaults detected: 6
75
+ kernel.core_pattern = /var/crash/core-%e-signal-%s-pid-%p-ts-%t
76
+ fs.suid_dumpable = 1
22
77
  ```
23
78
 
24
- More details about which commands caused which segmentation faults are available in the _output.txt_ file.
79
+ * Edit _/etc/security/limits.conf_:
80
+ ```
81
+ * soft core unlimited
82
+ root soft core unlimited
83
+ * hard core unlimited
84
+ root hard core unlimited
85
+ ```
86
+
87
+ Reboot the system for the changes to take effect.
25
88
 
26
89
  ## Contributing
27
90
 
28
91
  Bug reports and pull requests are welcome on GitHub at https://github.com/rwestphal/frr-cli-fuzzer.
92
+
93
+ ## License
94
+
95
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
data/Rakefile CHANGED
@@ -1,2 +1,2 @@
1
1
  require "bundler/gem_tasks"
2
- task :default => :spec
2
+ task default: :spec
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  $VERBOSE = true
3
3
 
4
- require 'yaml'
5
- require_relative '../lib/frr-cli-fuzzer.rb'
4
+ require "yaml"
5
+ require_relative "../lib/frr-cli-fuzzer.rb"
6
6
 
7
7
  # Signal handler.
8
- trap('INT') do
8
+ trap("INT") do
9
9
  FrrCliFuzzer.print_results
10
10
  exit(0)
11
11
  end
@@ -26,19 +26,17 @@ rescue SystemCallError, Psych::SyntaxError, ArgumentError => e
26
26
  exit(1)
27
27
  end
28
28
 
29
- FrrCliFuzzer::LibC.prctl(FrrCliFuzzer::LibC::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0)
30
-
31
- # Start fuzzer and print the results when we're done.
32
- FrrCliFuzzer.init(iterations: config.dig('fuzzer', 'iterations'),
33
- random_order: config.dig('fuzzer', 'random-order'),
34
- runstatedir: config.dig('fuzzer', 'runstatedir'),
35
- frr_build_parameters: config['frr-build-parameters'],
36
- daemons: config['daemons'],
37
- configs: config['configs'],
38
- nodes: config['nodes'],
39
- regexps: config['regexps'],
40
- whitelist: config['whitelist'],
41
- blacklist: config['blacklist'])
29
+ # Start fuzzer and print the results when we"re done.
30
+ FrrCliFuzzer.init(iterations: config.dig("fuzzer", "iterations"),
31
+ random_order: config.dig("fuzzer", "random-order"),
32
+ runstatedir: config.dig("fuzzer", "runstatedir"),
33
+ frr_build_parameters: config["frr-build-parameters"],
34
+ daemons: config["daemons"],
35
+ configs: config["configs"],
36
+ nodes: config["nodes"],
37
+ regexps: config["regexps"],
38
+ global_whitelist: config["global_whitelist"],
39
+ global_blacklist: config["global_blacklist"])
42
40
  FrrCliFuzzer.gen_configs
43
41
  FrrCliFuzzer.start_daemons
44
42
  FrrCliFuzzer.test_fuzzing
data/config.yml CHANGED
@@ -20,7 +20,7 @@ frr-build-parameters:
20
20
 
21
21
  daemons:
22
22
  - zebra
23
- #- bgpd
23
+ - bgpd
24
24
  - ospfd
25
25
  - ospf6d
26
26
  - isisd
@@ -60,11 +60,11 @@ regexps:
60
60
  BANDWIDTH: "1000"
61
61
  PERCENTAGE: "50"
62
62
 
63
- whitelist:
63
+ global_whitelist:
64
64
  #- ^show (ip|ipv6)
65
65
  #- redistribute
66
66
 
67
- blacklist:
67
+ global_blacklist:
68
68
  - output file
69
69
  - ^write
70
70
  - ^copy
@@ -73,65 +73,201 @@ blacklist:
73
73
  - ^exit
74
74
  - ^quit
75
75
  - ^end
76
- - ^(no )?(ip|ipv6) route
77
- - ^show (ip|ipv6) (route|fib)
78
- - ^no router bgp
79
- - ^no neighbor (A.B.C.D|X:X::X:X|WORD)$
80
- - ^no neighbor (A.B.C.D|X:X::X:X|WORD) remote-as
81
- #- ospf
82
76
  #- ^(no )?debug
83
77
 
84
78
  nodes:
85
- #- ""
86
- - -c "configure terminal"
87
- - -c "configure terminal" -c "interface eth99"
88
- - -c "configure terminal" -c "interface eth99" -c "link-params"
89
- - -c "configure terminal" -c "route-map RMAP permit 1"
90
- #- -c "configure terminal" -c "router bgp 1"
91
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 unicast"
92
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 multicast"
93
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 vpn"
94
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 labeled-unicast"
95
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 flowspec"
96
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 unicast"
97
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 multicast"
98
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 vpn"
99
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 labeled-unicast"
100
- #- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 flowspec"
101
- #- -c "configure terminal" -c "router bgp 1" -c "address-family l2vpn evpn"
102
- #- -c "configure terminal" -c "router bgp 1" -c "vnc defaults"
103
- #- -c "configure terminal" -c "router bgp 1" -c "vnc nve-group NAME"
104
- #- -c "configure terminal" -c "router bgp 1" -c "vnc l2-group NAME"
105
- #- -c "configure terminal" -c "router bgp 1" -c "vrf-policy NAME"
106
- - -c "configure terminal" -c "key chain WORD"
107
- - -c "configure terminal" -c "key chain WORD" -c "key 255"
108
- - -c "configure terminal" -c "router babel"
109
- - -c "configure terminal" -c "router ospf"
110
- - -c "configure terminal" -c "router ospf6"
111
- - -c "configure terminal" -c "router isis 1"
112
- - -c "configure terminal" -c "router openfabric 1"
113
- - -c "configure terminal" -c "router rip"
114
- - -c "configure terminal" -c "router ripng"
115
- - -c "configure terminal" -c "router eigrp 1"
116
- - -c "configure terminal" -c "mpls ldp"
117
- - -c "configure terminal" -c "mpls ldp" -c "address-family ipv4"
118
- - -c "configure terminal" -c "mpls ldp" -c "address-family ipv4" -c "interface eth99"
119
- - -c "configure terminal" -c "mpls ldp" -c "address-family ipv6"
120
- - -c "configure terminal" -c "mpls ldp" -c "address-family ipv6" -c "interface eth99"
121
- - -c "configure terminal" -c "l2vpn WORD type vpls"
122
- - -c "configure terminal" -c "l2vpn WORD type vpls" -c "member pseudowire mpw0"
123
- - -c "configure terminal" -c "line vty"
124
- - -c "configure terminal" -c "logical-router 1 ns /var/run/netns/ns1"
125
- - -c "configure terminal" -c "vrf RED"
126
- - -c "configure terminal" -c "nexthop-group NHGROUP"
127
- - -c "configure terminal" -c "pbr-map WORD seq 100"
128
- #- -c "configure terminal" -c "bfd"
129
- #- -c "configure terminal" -c "bfd" -c "peer 1.1.1.1"
79
+ - hierarchy: -c ""
80
+ whitelist:
81
+ blacklist:
82
+ - ^show (ip|ipv6) (route|fib)
83
+ - ^sharp install
84
+
85
+ - hierarchy: -c "configure terminal"
86
+ whitelist:
87
+ blacklist:
88
+ - ^(no )?log
89
+ - ^(no )?(ip|ipv6) route
90
+ - ^no router bgp
91
+
92
+ - hierarchy: -c "configure terminal" -c "interface eth99"
93
+ whitelist:
94
+ blacklist:
95
+
96
+ - hierarchy: -c "configure terminal" -c "interface eth99" -c "link-params"
97
+ whitelist:
98
+ blacklist:
99
+
100
+ - hierarchy: -c "configure terminal" -c "route-map RMAP permit 1"
101
+ whitelist:
102
+ blacklist:
103
+
104
+ - hierarchy: -c "configure terminal" -c "router bgp 1"
105
+ whitelist:
106
+ blacklist:
107
+ - ^no neighbor (A.B.C.D|X:X::X:X|WORD)$
108
+ - ^no neighbor (A.B.C.D|X:X::X:X|WORD) remote-as
109
+
110
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 unicast"
111
+ whitelist:
112
+ blacklist:
113
+
114
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 multicast"
115
+ whitelist:
116
+ blacklist:
117
+
118
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 vpn"
119
+ whitelist:
120
+ blacklist:
121
+
122
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 labeled-unicast"
123
+ whitelist:
124
+ blacklist:
125
+
126
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 flowspec"
127
+ whitelist:
128
+ blacklist:
129
+
130
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 unicast"
131
+ whitelist:
132
+ blacklist:
133
+
134
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 multicast"
135
+ whitelist:
136
+ blacklist:
137
+
138
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 vpn"
139
+ whitelist:
140
+ blacklist:
141
+
142
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 labeled-unicast"
143
+ whitelist:
144
+ blacklist:
145
+
146
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 flowspec"
147
+ whitelist:
148
+ blacklist:
149
+
150
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "address-family l2vpn evpn"
151
+ whitelist:
152
+ blacklist:
153
+
154
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "vnc defaults"
155
+ whitelist:
156
+ blacklist:
157
+
158
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "vnc nve-group NAME"
159
+ whitelist:
160
+ blacklist:
161
+
162
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "vnc l2-group NAME"
163
+ whitelist:
164
+ blacklist:
165
+
166
+ - hierarchy: -c "configure terminal" -c "router bgp 1" -c "vrf-policy NAME"
167
+ whitelist:
168
+ blacklist:
169
+
170
+ - hierarchy: -c "configure terminal" -c "key chain WORD"
171
+ whitelist:
172
+ blacklist:
173
+
174
+ - hierarchy: -c "configure terminal" -c "key chain WORD" -c "key 255"
175
+ whitelist:
176
+ blacklist:
177
+
178
+ - hierarchy: -c "configure terminal" -c "router babel"
179
+ whitelist:
180
+ blacklist:
181
+
182
+ - hierarchy: -c "configure terminal" -c "router ospf"
183
+ whitelist:
184
+ blacklist:
185
+
186
+ - hierarchy: -c "configure terminal" -c "router ospf6"
187
+ whitelist:
188
+ blacklist:
189
+
190
+ - hierarchy: -c "configure terminal" -c "router isis 1"
191
+ whitelist:
192
+ blacklist:
193
+
194
+ - hierarchy: -c "configure terminal" -c "router openfabric 1"
195
+ whitelist:
196
+ blacklist:
197
+
198
+ - hierarchy: -c "configure terminal" -c "router rip"
199
+ whitelist:
200
+ blacklist:
201
+ - redistribute
202
+
203
+ - hierarchy: -c "configure terminal" -c "router ripng"
204
+ whitelist:
205
+ blacklist:
206
+
207
+ - hierarchy: -c "configure terminal" -c "router eigrp 1"
208
+ whitelist:
209
+ blacklist:
210
+
211
+ - hierarchy: -c "configure terminal" -c "mpls ldp"
212
+ whitelist:
213
+ blacklist:
214
+
215
+ - hierarchy: -c "configure terminal" -c "mpls ldp" -c "address-family ipv4"
216
+ whitelist:
217
+ blacklist:
218
+
219
+ - hierarchy: -c "configure terminal" -c "mpls ldp" -c "address-family ipv4" -c "interface eth99"
220
+ whitelist:
221
+ blacklist:
222
+
223
+ - hierarchy: -c "configure terminal" -c "mpls ldp" -c "address-family ipv6"
224
+ whitelist:
225
+ blacklist:
226
+
227
+ - hierarchy: -c "configure terminal" -c "mpls ldp" -c "address-family ipv6" -c "interface eth99"
228
+ whitelist:
229
+ blacklist:
230
+
231
+ - hierarchy: -c "configure terminal" -c "l2vpn WORD type vpls"
232
+ whitelist:
233
+ blacklist:
234
+
235
+ - hierarchy: -c "configure terminal" -c "l2vpn WORD type vpls" -c "member pseudowire mpw0"
236
+ whitelist:
237
+ blacklist:
238
+
239
+ - hierarchy: -c "configure terminal" -c "line vty"
240
+ whitelist:
241
+ blacklist:
242
+
243
+ - hierarchy: -c "configure terminal" -c "logical-router 1 ns /var/run/netns/ns1"
244
+ whitelist:
245
+ blacklist:
246
+
247
+ - hierarchy: -c "configure terminal" -c "vrf RED"
248
+ whitelist:
249
+ blacklist:
250
+
251
+ - hierarchy: -c "configure terminal" -c "nexthop-group NHGROUP"
252
+ whitelist:
253
+ blacklist:
254
+
255
+ - hierarchy: -c "configure terminal" -c "pbr-map WORD seq 100"
256
+ whitelist:
257
+ blacklist:
258
+
259
+ #- hierarchy: -c "configure terminal" -c "bfd"
260
+ # whitelist:
261
+ # blacklist:
262
+
263
+ #- hierarchy: -c "configure terminal" -c "bfd" -c "peer 1.1.1.1"
264
+ # whitelist:
265
+ # blacklist:
130
266
 
131
267
  configs:
132
268
  all: |
133
269
  hostname %(daemon)
134
- log file %(runstatedir)/%(daemon).log
270
+ log file %(logfile)
135
271
  log commands
136
272
  !
137
273
  debug northbound
@@ -1,6 +1,5 @@
1
- # coding: utf-8
2
- $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
- require 'frr-cli-fuzzer/version'
1
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__)
2
+ require "frr-cli-fuzzer/version"
4
3
 
5
4
  Gem::Specification.new do |spec|
6
5
  spec.name = "frr-cli-fuzzer"
@@ -8,7 +7,7 @@ Gem::Specification.new do |spec|
8
7
  spec.authors = ["Renato Westphal"]
9
8
  spec.email = ["renato@opensourcerouting.org"]
10
9
 
11
- spec.summary = %q{FRR CLI fuzzer.}
10
+ spec.summary = "FRR CLI fuzzer."
12
11
  spec.homepage = "https://github.com/rwestphal/frr-cli-fuzzer"
13
12
  spec.license = "MIT"
14
13
 
@@ -1,16 +1,16 @@
1
- require 'fileutils'
2
- require 'scanf'
3
- require_relative 'frr-cli-fuzzer/libc'
4
- require_relative 'frr-cli-fuzzer/linux_namespace'
5
- require_relative 'frr-cli-fuzzer/version'
1
+ require "fileutils"
2
+ require "scanf"
3
+ require_relative "frr-cli-fuzzer/libc"
4
+ require_relative "frr-cli-fuzzer/linux_namespace"
5
+ require_relative "frr-cli-fuzzer/version"
6
6
 
7
7
  module FrrCliFuzzer
8
8
  DFLT_ITERATIONS = 1
9
- DFLT_RUNSTATEDIR = '/tmp/frr-cli-fuzzer'
10
- DFLT_FRR_SYSCONFDIR = '/etc/frr'
11
- DFLT_FRR_LOCALSTATE_DIR = '/var/run/frr'
12
- DFLT_FRR_USER = 'frr'
13
- DFLT_FRR_GROUP = 'frr'
9
+ DFLT_RUNSTATEDIR = "/tmp/frr-cli-fuzzer"
10
+ DFLT_FRR_SYSCONFDIR = "/etc/frr"
11
+ DFLT_FRR_LOCALSTATE_DIR = "/var/run/frr"
12
+ DFLT_FRR_USER = "frr"
13
+ DFLT_FRR_GROUP = "frr"
14
14
 
15
15
  class << self
16
16
  def init(iterations: nil,
@@ -21,93 +21,82 @@ module FrrCliFuzzer
21
21
  configs: nil,
22
22
  nodes: nil,
23
23
  regexps: nil,
24
- whitelist: nil,
25
- blacklist: nil)
24
+ global_whitelist: nil,
25
+ global_blacklist: nil)
26
26
  # Load configuration and default values if necessary.
27
27
  @iterations = iterations || DFLT_ITERATIONS
28
28
  @random_order = random_order || false
29
29
  @runstatedir = runstatedir || DFLT_RUNSTATEDIR
30
30
  @frr = frr_build_parameters || []
31
- @frr['sysconfdir'] ||= DFLT_FRR_SYSCONFDIR
32
- @frr['localstatedir'] ||= DFLT_FRR_LOCALSTATE_DIR
33
- @frr['user'] ||= DFLT_FRR_USER
34
- @frr['group'] ||= DFLT_FRR_GROUP
35
- @daemons = daemons || []
31
+ @frr["sysconfdir"] ||= DFLT_FRR_SYSCONFDIR
32
+ @frr["localstatedir"] ||= DFLT_FRR_LOCALSTATE_DIR
33
+ @frr["user"] ||= DFLT_FRR_USER
34
+ @frr["group"] ||= DFLT_FRR_GROUP
35
+ daemons ||= []
36
+ @daemons = Hash[daemons.collect { |daemon| [daemon, nil] }]
36
37
  @configs = configs || []
37
38
  @nodes = nodes || []
38
39
  @regexps = regexps || []
39
- @whitelist = whitelist || []
40
- @blacklist = blacklist || []
40
+ @global_whitelist = global_whitelist || []
41
+ @global_blacklist = global_blacklist || []
41
42
 
42
43
  # Initialize counters.
43
44
  @counters = {}
44
- @counters['non-filtered-cmds'] = 0
45
- @counters['filtered-blacklist'] = 0
46
- @counters['filtered-whitelist'] = 0
47
- @counters['tested-cmds'] = 0
48
- @counters['segfaults'] = 0
45
+ @counters["non-filtered-cmds"] = 0
46
+ @counters["filtered-blacklist"] = 0
47
+ @counters["filtered-whitelist"] = 0
48
+ @counters["tested-cmds"] = 0
49
+ @counters["segfaults"] = 0
49
50
  @segfaults = {}
50
51
 
51
52
  # Security check to prevent accidental deletion of data.
52
- unless @runstatedir.include?('frr-cli-fuzzer')
53
+ unless @runstatedir.include?("frr-cli-fuzzer")
53
54
  abort("The runstatedir configuration parameter must contain "\
54
- "'frr-cli-fuzzer' somewhere in the path.")
55
+ "\"frr-cli-fuzzer\" somewhere in the path.")
55
56
  end
56
57
  FileUtils.rm_rf(@runstatedir)
57
58
  FileUtils.mkdir_p(@runstatedir)
58
- FileUtils.chown_R(@frr['user'], @frr['group'], @runstatedir)
59
+ FileUtils.chown_R(@frr["user"], @frr["group"], @runstatedir)
59
60
 
60
61
  # Create a new process on a new pid, mount and network namespace.
61
62
  @ns = LinuxNamespace.new
62
- @ns.fork_and_unshare do
63
- # This is the init process of this fuzzer. We need to reap the zombies.
64
- trap(:CHLD) { Process.wait }
65
- trap(:INT, :IGNORE)
66
- sleep
67
- end
68
63
 
69
64
  # Bind mount FRR directories.
70
- mount(@frr['sysconfdir'], @frr['user'], @frr['group'])
71
- mount(@frr['localstatedir'], @frr['user'], @frr['group'])
72
- end
73
-
74
- # nsenter(1) is a standard tool from the util-linux package. It can be used
75
- # to run a program with namespaces of other processes.
76
- def nsenter
77
- "nsenter -t #{@ns.pid} --mount --pid --net"
65
+ bind_mount(@frr["sysconfdir"], @frr["user"], @frr["group"])
66
+ bind_mount(@frr["localstatedir"], @frr["user"], @frr["group"])
78
67
  end
79
68
 
80
69
  # Bind mount a path under the configured runstatedir.
81
- def mount(path, user, group)
70
+ def bind_mount(path, user, group)
82
71
  source = "#{@runstatedir}/#{path}"
83
72
  FileUtils.mkdir_p(path)
84
73
  FileUtils.mkdir_p(source)
85
74
  FileUtils.chown_R(user, group, source)
86
- system("#{nsenter} mount --bind #{source} #{path}")
75
+ system("#{@ns.nsenter} mount --bind #{source} #{path}")
87
76
  end
88
77
 
89
78
  # Save configuration in the file system.
90
79
  def save_config(daemon, config)
91
80
  path = "#{@runstatedir}/#{@frr['sysconfdir']}/#{daemon}.conf"
92
- File.open(path, 'w') { |file| file.write(config) }
81
+ File.open(path, "w") { |file| file.write(config) }
93
82
  end
94
83
 
95
84
  # Generate FRR configuration file.
96
85
  def gen_config(daemon)
97
- config = @configs['all'] || ''
98
- config += @configs[daemon] || ''
86
+ config = @configs["all"] || ""
87
+ config += @configs[daemon] || ""
99
88
 
100
89
  # Replace variables.
101
- config.gsub!('%(daemon)', daemon)
102
- config.gsub!('%(runstatedir)', @runstatedir)
90
+ config.gsub!("%(daemon)", daemon)
91
+ config.gsub!("%(logfile)", "#{@runstatedir}/#{daemon}.log")
103
92
 
104
93
  save_config(daemon, config)
105
94
  end
106
95
 
107
96
  # Generate FRR configuration files.
108
97
  def gen_configs
109
- save_config('vtysh', '')
110
- @daemons.each do |daemon|
98
+ save_config("vtysh", "")
99
+ @daemons.keys.each do |daemon|
111
100
  gen_config(daemon)
112
101
  end
113
102
  end
@@ -118,37 +107,45 @@ module FrrCliFuzzer
118
107
  FileUtils.rm_f("#{@runstatedir}/#{@frr['localstatedir']}/#{daemon}.pid")
119
108
 
120
109
  # Spawn new process.
121
- pid = Process.spawn("#{nsenter} #{daemon} -d --log=stdout "\
122
- ">> #{@runstatedir}/#{daemon}.stdout "\
123
- "2>> #{@runstatedir}/#{daemon}.stderr")
110
+ pid = Process.spawn("#{@ns.nsenter} #{daemon} --log=stdout -d",
111
+ out: "#{@runstatedir}/#{daemon}.stdout",
112
+ err: "#{@runstatedir}/#{daemon}.stderr")
124
113
  Process.detach(pid)
114
+
115
+ # Obtain the PID of the daemon as seen in the PID namespace where
116
+ # it resides.
117
+ @daemons[daemon] = `#{@ns.nsenter} pidof -s #{daemon}`.rstrip
125
118
  end
126
119
 
127
120
  # Start all FRR daemons.
128
121
  def start_daemons
129
- @daemons.each do |daemon|
122
+ @daemons.keys.each do |daemon|
130
123
  start_daemon(daemon)
131
124
  end
132
125
  end
133
126
 
134
127
  # Check if a FRR daemon is still alive.
135
128
  def daemon_alive?(daemon)
136
- `#{nsenter} ps aux | grep #{daemon} | grep -E -v "defunct|grep"` != ''
129
+ `#{@ns.nsenter} ps aux | grep #{daemon} | grep -E -v "defunct|grep"` != ""
137
130
  end
138
131
 
139
132
  # Check if a command should be white-list filtered.
140
- def filter_whitelist(command)
141
- return false if @whitelist.empty?
133
+ def filter_whitelist(command, whitelist)
134
+ whitelist += @global_whitelist
135
+
136
+ return false if whitelist.empty?
142
137
 
143
- @whitelist.each do |regexp|
138
+ whitelist.each do |regexp|
144
139
  return false if command =~ /#{regexp}/
145
140
  end
146
141
  true
147
142
  end
148
143
 
149
144
  # Check if a command should be black-list filtered.
150
- def filter_blacklist(command)
151
- @blacklist.each do |regexp|
145
+ def filter_blacklist(command, blacklist)
146
+ blacklist += @global_blacklist
147
+
148
+ blacklist.each do |regexp|
152
149
  return true if command =~ /#{regexp}/
153
150
  end
154
151
  false
@@ -156,7 +153,7 @@ module FrrCliFuzzer
156
153
 
157
154
  # Prepare command to be used by the CLI fuzzing tester.
158
155
  def prepare_command(command)
159
- new_command = ''
156
+ new_command = ""
160
157
 
161
158
  command.split.each do |word|
162
159
  # Custom regexps.
@@ -166,14 +163,14 @@ module FrrCliFuzzer
166
163
 
167
164
  # Handle intervals.
168
165
  if word =~ /(\d+\-\d+)/
169
- interval = word.scanf('(%d-%d)')
166
+ interval = word.scanf("(%d-%d)")
170
167
  new_command << interval[1].to_s
171
168
  else
172
169
  new_command << word
173
170
  end
174
171
 
175
172
  # Append whitespace after each word.
176
- new_command << ' '
173
+ new_command << " "
177
174
  end
178
175
 
179
176
  new_command.rstrip
@@ -183,24 +180,28 @@ module FrrCliFuzzer
183
180
  def prepare_commmands
184
181
  commands = []
185
182
 
186
- @nodes.each do |hierarchy|
187
- permutations = `#{nsenter} vtysh #{hierarchy} -c \"list permutations\"`
183
+ @nodes.each do |node|
184
+ hierarchy = node["hierarchy"]
185
+ whitelist = node["whitelist"] || []
186
+ blacklist = node["blacklist"] || []
187
+
188
+ permutations = `#{@ns.nsenter} vtysh #{hierarchy} -c \"list permutations\"`
188
189
  permutations.each_line do |command|
189
190
  command = command.strip
190
191
 
191
192
  # Check whitelist and blacklist.
192
- if filter_whitelist(command)
193
+ if filter_whitelist(command, whitelist)
193
194
  puts "filtering (whitelist): #{command}"
194
- @counters['filtered-whitelist'] += 1
195
+ @counters["filtered-whitelist"] += 1
195
196
  next
196
197
  end
197
- if filter_blacklist(command)
198
+ if filter_blacklist(command, blacklist)
198
199
  puts "filtering (blacklist): #{command}"
199
- @counters['filtered-blacklist'] += 1
200
+ @counters["filtered-blacklist"] += 1
200
201
  next
201
202
  end
202
203
 
203
- @counters['non-filtered-cmds'] += 1
204
+ @counters["non-filtered-cmds"] += 1
204
205
 
205
206
  commands.push("vtysh #{hierarchy} -c \"#{prepare_command(command)}\"")
206
207
  end
@@ -214,9 +215,12 @@ module FrrCliFuzzer
214
215
  def send_command(command)
215
216
  puts "testing: #{command}"
216
217
 
217
- vtysh_log = "#{@runstatedir}/vtysh.txt"
218
- File.open(vtysh_log, 'a') { |f| f.puts command }
219
- system("#{nsenter} #{command} >> #{vtysh_log} 2>&1")
218
+ ["stdout", "stderr"].each do |suffix|
219
+ File.open("#{@runstatedir}/vtysh.#{suffix}", "a") { |f| f.puts command }
220
+ end
221
+ Kernel.system("#{@ns.nsenter} #{command}",
222
+ out: ["#{@runstatedir}/vtysh.stdout", "a"],
223
+ err: ["#{@runstatedir}/vtysh.stderr", "a"])
220
224
  end
221
225
 
222
226
  # Print the results of the fuzzing tests.
@@ -227,19 +231,35 @@ module FrrCliFuzzer
227
231
  puts "- blacklist filtered commands: #{@counters['filtered-blacklist']}"
228
232
  puts "- tested commands: #{@counters['tested-cmds']}"
229
233
  puts "- segfaults detected: #{@counters['segfaults']}"
230
- @segfaults.each_pair do |msg, count|
231
- puts " (x#{count}) #{msg}"
234
+ @segfaults.each_pair do |msg, pids|
235
+ puts " (x#{pids.size}) #{msg}"
236
+ print " PIDs:"
237
+ pids.each { |pid| print " #{pid}" }
238
+ puts ""
232
239
  end
233
240
  end
234
241
 
235
242
  # Log a segfault to both the standard output and to the fuzzer output file.
236
243
  def log_segfault(daemon, command)
237
244
  msg = "#{daemon} aborted: #{command}"
245
+ pid = @daemons[daemon]
246
+
247
+ @counters["segfaults"] += 1
248
+ @segfaults[msg] ||= []
249
+ @segfaults[msg].push(pid)
250
+ msg << " (PID: #{pid})"
238
251
  puts msg
239
- File.open("#{@runstatedir}/output.txt", 'a') { |f| f.puts msg }
252
+ File.open("#{@runstatedir}/segfaults.txt", "a") { |f| f.puts msg }
253
+ end
254
+
255
+ # Append PID of the aborted daemon to its log files.
256
+ def rename_log_files(daemon)
257
+ pid = @daemons[daemon]
240
258
 
241
- @counters['segfaults'] += 1
242
- @segfaults[msg] = @segfaults[msg].to_i + 1
259
+ ["log", "stdout", "stderr"].each do |suffix|
260
+ log_file = "#{@runstatedir}/#{daemon}.#{suffix}"
261
+ FileUtils.mv(log_file, "#{log_file}.#{pid}")
262
+ end
243
263
  end
244
264
 
245
265
  # Start fuzzing tests.
@@ -255,14 +275,15 @@ module FrrCliFuzzer
255
275
 
256
276
  # Iterate over all commands.
257
277
  commands.each do |command|
258
- @counters['tested-cmds'] += 1
278
+ @counters["tested-cmds"] += 1
259
279
  send_command(command)
260
280
 
261
281
  # Check if all daemons are still alive.
262
- @daemons.each do |daemon|
282
+ @daemons.keys.each do |daemon|
263
283
  next if daemon_alive?(daemon)
264
284
 
265
285
  log_segfault(daemon, command)
286
+ rename_log_files(daemon)
266
287
  start_daemon(daemon)
267
288
  end
268
289
  end
@@ -1,14 +1,37 @@
1
- require 'ffi'
1
+ require "ffi"
2
2
 
3
3
  module FrrCliFuzzer
4
- # Bindings for a few libc's functions.
5
- class LibC
6
- extend FFI::Library
7
- ffi_lib 'c'
8
-
9
- attach_function :unshare, [:int], :int
10
- attach_function :mount, [:string, :string, :string, :ulong, :pointer], :int
11
- attach_function :prctl, [:int, :long, :long, :long, :long], :int
4
+ # Bindings for a few libc"s functions.
5
+ module LibC
6
+ class Bindings
7
+ extend FFI::Library
8
+ ffi_lib "c"
9
+
10
+ attach_function :unshare, [:int], :int
11
+ attach_function :mount, [:string, :string, :string, :ulong, :pointer], :int
12
+ attach_function :prctl, [:int, :long, :long, :long, :long], :int
13
+ end
14
+
15
+ # Wrapper for mount(2).
16
+ def self.mount(source, target, fs_type, flags, data)
17
+ if Bindings.mount(source, target, fs_type, flags, data) < 0
18
+ raise SystemCallError.new("mount failed", FFI::LastError.error)
19
+ end
20
+ end
21
+
22
+ # Wrapper for unshare(2).
23
+ def self.unshare(flags)
24
+ if Bindings.unshare(flags) < 0
25
+ raise SystemCallError.new("unshare failed", FFI::LastError.error)
26
+ end
27
+ end
28
+
29
+ # Wrapper for prctl(2).
30
+ def self.prctl(option, arg2, arg3, arg4, arg5)
31
+ if Bindings.prctl(option, arg2, arg3, arg4, arg5) == -1
32
+ raise SystemCallError.new("prctl failed", FFI::LastError.error)
33
+ end
34
+ end
12
35
 
13
36
  # include/uapi/linux/sched.h
14
37
  CLONE_NEWNS = 0x00020000
@@ -1,69 +1,69 @@
1
1
  module FrrCliFuzzer
2
2
  class LinuxNamespace
3
- attr_accessor :pid
3
+ def initialize
4
+ fork_and_unshare do
5
+ # This is the init process of the new PID namespace. We need to reap
6
+ # the zombies.
7
+ trap(:CHLD) { Process.wait }
8
+ trap(:INT, :IGNORE)
9
+ sleep
10
+ end
11
+ end
4
12
 
5
13
  # Create a child process running on a separate network and mount namespace.
6
14
  def fork_and_unshare
7
- begin
8
- io_in, io_out = IO.pipe
15
+ io_in, io_out = IO.pipe
9
16
 
10
- pid = Kernel.fork do
11
- unshare(LibC::CLONE_NEWNS | LibC::CLONE_NEWPID | LibC::CLONE_NEWNET)
17
+ LibC.prctl(FrrCliFuzzer::LibC::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0)
12
18
 
13
- # Fork again to use the new PID namespace.
14
- # Need to supress a warning that is irrelevant for us.
15
- warn_level = $VERBOSE
16
- $VERBOSE = nil
17
- pid = Kernel.fork do
18
- # HACK: kill when parent dies.
19
- trap(:SIGUSR1) do
20
- LibC.prctl(LibC::PR_SET_PDEATHSIG, 15, 0, 0, 0)
21
- trap(:SIGUSR1, :IGNORE)
22
- end
23
- LibC.prctl(LibC::PR_SET_PDEATHSIG, 10, 0, 0, 0)
19
+ pid = Kernel.fork do
20
+ LibC.unshare(LibC::CLONE_NEWNS | LibC::CLONE_NEWPID | LibC::CLONE_NEWNET)
24
21
 
25
- mount_propagation(LibC::MS_REC | LibC::MS_PRIVATE)
26
- mount_proc
27
- yield
22
+ # Fork again to use the new PID namespace.
23
+ # Need to supress a warning that is irrelevant for us.
24
+ warn_level = $VERBOSE
25
+ $VERBOSE = nil
26
+ pid = Kernel.fork do
27
+ # HACK: kill when parent dies.
28
+ trap(:SIGUSR1) do
29
+ LibC.prctl(LibC::PR_SET_PDEATHSIG, 15, 0, 0, 0)
30
+ trap(:SIGUSR1, :IGNORE)
28
31
  end
29
- $VERBOSE = warn_level
30
- io_out.puts "#{pid}"
31
- exit(0)
32
- end
32
+ LibC.prctl(LibC::PR_SET_PDEATHSIG, 10, 0, 0, 0)
33
33
 
34
- @pid = io_in.gets.to_i
35
- Process.waitpid(pid)
36
- rescue SystemCallError => e
37
- $stderr.puts "System call error:: #{e.message}"
38
- $stderr.puts e.backtrace
39
- exit(1)
34
+ mount_propagation(LibC::MS_REC | LibC::MS_PRIVATE)
35
+ mount_proc
36
+ yield
37
+ end
38
+ $VERBOSE = warn_level
39
+ io_out.puts(pid)
40
+ exit(0)
40
41
  end
42
+
43
+ @pid = io_in.gets.to_i
44
+ Process.waitpid(pid)
45
+ rescue SystemCallError => e
46
+ warn "System call error:: #{e.message}"
47
+ warn e.backtrace
48
+ exit(1)
41
49
  end
42
50
 
43
51
  # Set the mount propagation of the process.
44
52
  def mount_propagation(flags)
45
- mount('none', '/', nil, flags, nil)
53
+ LibC.mount("none", "/", nil, flags, nil)
46
54
  end
47
55
 
48
56
  # Mount the proc filesystem (useful after creating a new PID namespace).
49
57
  def mount_proc
50
- mount('none', '/proc', nil, LibC::MS_REC | LibC::MS_PRIVATE, nil)
51
- mount('proc', '/proc', 'proc',
52
- LibC::MS_NOSUID | LibC::MS_NOEXEC | LibC::MS_NODEV, nil)
58
+ LibC.mount("none", "/proc", nil, LibC::MS_REC | LibC::MS_PRIVATE, nil)
59
+ LibC.mount("proc", "/proc", "proc",
60
+ LibC::MS_NOSUID | LibC::MS_NOEXEC | LibC::MS_NODEV, nil)
53
61
  end
54
62
 
55
- # Wrapper for mount(2).
56
- def mount(source, target, fs_type, flags, data)
57
- if LibC.mount(source, target, fs_type, flags, data) < 0
58
- raise SystemCallError.new('mount failed', FFI::LastError.error)
59
- end
60
- end
61
-
62
- # Wrapper for unshare(2).
63
- def unshare(flags)
64
- if LibC.unshare(flags) < 0
65
- raise SystemCallError.new('unshare failed', FFI::LastError.error)
66
- end
63
+ # nsenter(1) is a standard tool from the util-linux package. It can be used
64
+ # to run a program with namespaces of other processes.
65
+ def nsenter
66
+ "nsenter -t #{@pid} --mount --pid --net"
67
67
  end
68
68
  end
69
69
  end
@@ -1,3 +1,3 @@
1
1
  module FrrCliFuzzer
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frr-cli-fuzzer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renato Westphal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-06 00:00:00.000000000 Z
11
+ date: 2018-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -61,6 +61,7 @@ extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
63
  - ".gitignore"
64
+ - ".rubocop.yml"
64
65
  - Gemfile
65
66
  - LICENSE.txt
66
67
  - README.md