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 +4 -4
- data/.rubocop.yml +8 -0
- data/Gemfile +2 -2
- data/README.md +80 -13
- data/Rakefile +1 -1
- data/bin/frr-cli-fuzzer +14 -16
- data/config.yml +191 -55
- data/frr-cli-fuzzer.gemspec +3 -4
- data/lib/frr-cli-fuzzer.rb +101 -80
- data/lib/frr-cli-fuzzer/libc.rb +32 -9
- data/lib/frr-cli-fuzzer/linux_namespace.rb +45 -45
- data/lib/frr-cli-fuzzer/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e8ad21ba8f6f41b93c2d61d4d68dd123a5707c0
|
4
|
+
data.tar.gz: 0dec0e76549f98d5b8e8c6eb76106d83cda3d647
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d57fb7833789767b120997653f1e12264fff0d50a29b9fd25ac51562c562bf864b89d6e911a65e53b4a4305e5a10f6ce517d58c519d78234f3387592f1acbfc
|
7
|
+
data.tar.gz: a90704a5aeeedeb5a07e3547a3dbdb821c0858911e32e81ff932b4fce872af4a9ca8b52a4554858bcc43ff4ab82add1043d80e80665b1b8d7a6ad6f0596e5678
|
data/.rubocop.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,28 +1,95 @@
|
|
1
|
-
#
|
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
|
10
|
-
```
|
11
|
-
|
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
|
-
|
73
|
+
* Edit _/etc/sysctl.conf_:
|
15
74
|
```
|
16
|
-
|
17
|
-
|
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
|
-
|
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 :
|
2
|
+
task default: :spec
|
data/bin/frr-cli-fuzzer
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
$VERBOSE = true
|
3
3
|
|
4
|
-
require
|
5
|
-
require_relative
|
4
|
+
require "yaml"
|
5
|
+
require_relative "../lib/frr-cli-fuzzer.rb"
|
6
6
|
|
7
7
|
# Signal handler.
|
8
|
-
trap(
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
63
|
+
global_whitelist:
|
64
64
|
#- ^show (ip|ipv6)
|
65
65
|
#- redistribute
|
66
66
|
|
67
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
- -c "configure terminal" -c "
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
- -c "configure terminal" -c "router
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
- -c "configure terminal" -c "
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
- -c "configure terminal" -c "
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
- -c "configure terminal" -c "
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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 %(
|
270
|
+
log file %(logfile)
|
135
271
|
log commands
|
136
272
|
!
|
137
273
|
debug northbound
|
data/frr-cli-fuzzer.gemspec
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
|
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 =
|
10
|
+
spec.summary = "FRR CLI fuzzer."
|
12
11
|
spec.homepage = "https://github.com/rwestphal/frr-cli-fuzzer"
|
13
12
|
spec.license = "MIT"
|
14
13
|
|
data/lib/frr-cli-fuzzer.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
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 =
|
10
|
-
DFLT_FRR_SYSCONFDIR =
|
11
|
-
DFLT_FRR_LOCALSTATE_DIR =
|
12
|
-
DFLT_FRR_USER =
|
13
|
-
DFLT_FRR_GROUP =
|
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
|
-
|
25
|
-
|
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[
|
32
|
-
@frr[
|
33
|
-
@frr[
|
34
|
-
@frr[
|
35
|
-
|
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
|
-
@
|
40
|
-
@
|
40
|
+
@global_whitelist = global_whitelist || []
|
41
|
+
@global_blacklist = global_blacklist || []
|
41
42
|
|
42
43
|
# Initialize counters.
|
43
44
|
@counters = {}
|
44
|
-
@counters[
|
45
|
-
@counters[
|
46
|
-
@counters[
|
47
|
-
@counters[
|
48
|
-
@counters[
|
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?(
|
53
|
+
unless @runstatedir.include?("frr-cli-fuzzer")
|
53
54
|
abort("The runstatedir configuration parameter must contain "\
|
54
|
-
"
|
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[
|
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
|
-
|
71
|
-
|
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
|
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,
|
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[
|
98
|
-
config += @configs[daemon] ||
|
86
|
+
config = @configs["all"] || ""
|
87
|
+
config += @configs[daemon] || ""
|
99
88
|
|
100
89
|
# Replace variables.
|
101
|
-
config.gsub!(
|
102
|
-
config.gsub!(
|
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(
|
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}
|
122
|
-
"
|
123
|
-
"
|
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
|
-
|
133
|
+
def filter_whitelist(command, whitelist)
|
134
|
+
whitelist += @global_whitelist
|
135
|
+
|
136
|
+
return false if whitelist.empty?
|
142
137
|
|
143
|
-
|
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
|
-
|
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(
|
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 |
|
187
|
-
|
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[
|
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[
|
200
|
+
@counters["filtered-blacklist"] += 1
|
200
201
|
next
|
201
202
|
end
|
202
203
|
|
203
|
-
@counters[
|
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
|
-
|
218
|
-
|
219
|
-
|
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,
|
231
|
-
puts " (x#{
|
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}/
|
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
|
-
|
242
|
-
|
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[
|
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
|
data/lib/frr-cli-fuzzer/libc.rb
CHANGED
@@ -1,14 +1,37 @@
|
|
1
|
-
require
|
1
|
+
require "ffi"
|
2
2
|
|
3
3
|
module FrrCliFuzzer
|
4
|
-
# Bindings for a few libc
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
8
|
-
io_in, io_out = IO.pipe
|
15
|
+
io_in, io_out = IO.pipe
|
9
16
|
|
10
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
io_out.puts "#{pid}"
|
31
|
-
exit(0)
|
32
|
-
end
|
32
|
+
LibC.prctl(LibC::PR_SET_PDEATHSIG, 10, 0, 0, 0)
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
$
|
39
|
-
|
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(
|
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(
|
51
|
-
mount(
|
52
|
-
|
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
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
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:
|
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-
|
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
|