frr-cli-fuzzer 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 378f404d19c9beb961473414202aa8f24e5faad8
4
+ data.tar.gz: 5a5aff0028114fc40add71aba2a389e555c3c0e1
5
+ SHA512:
6
+ metadata.gz: 55459c77035f9d77cbe35218e20c9594bb53bb8424885329e2c89f394ca72dc105f5085c26b136255a4926120a847764dabc94e044848b30d689f75788db9c14
7
+ data.tar.gz: 513b03ec3aa21574310abd7663d5c2d9536d6254df5ef3c616febdb1b86b624a28664634ccfae9a7a45f474c3e80a02e7812ee07168d22cabfbd0a4d5a786f21
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in frr-cli-fuzzer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Renato Westphal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # FrrCliFuzzer
2
+
3
+ ## Installation
4
+
5
+ After checking out the repo, run `bin/setup` to install the dependencies (currently, only the _ffi_ gem).
6
+
7
+ ## Usage
8
+
9
+ Edit _config.yml_ as desired. Run the CLI fuzzer using the following command:
10
+ ```sh
11
+ ./bin/frr-cli-fuzzer config.yml
12
+ ```
13
+
14
+ Once the tests complete, the results are displayed in the standard output like this:
15
+ ```
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
22
+ ```
23
+
24
+ More details about which commands caused which segmentation faults are available in the _output.txt_ file.
25
+
26
+ ## Contributing
27
+
28
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rwestphal/frr-cli-fuzzer.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ $VERBOSE = true
3
+
4
+ require 'yaml'
5
+ require_relative '../lib/frr-cli-fuzzer.rb'
6
+
7
+ # Signal handler.
8
+ trap('INT') do
9
+ FrrCliFuzzer.print_results
10
+ exit(0)
11
+ end
12
+
13
+ # Parse command-line options.
14
+ if ARGV.size != 1
15
+ puts "Usage: #{File.basename(__FILE__)} config.yml"
16
+ exit(1)
17
+ end
18
+ config_file = ARGV[0]
19
+
20
+ # Read configuration file.
21
+ begin
22
+ config = YAML.load_file(config_file)
23
+ rescue SystemCallError, Psych::SyntaxError, ArgumentError => e
24
+ warn e.message
25
+ warn e.backtrace
26
+ exit(1)
27
+ end
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'])
42
+ FrrCliFuzzer.gen_configs
43
+ FrrCliFuzzer.start_daemons
44
+ FrrCliFuzzer.test_fuzzing
45
+ FrrCliFuzzer.print_results
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config.yml ADDED
@@ -0,0 +1,215 @@
1
+ ---
2
+
3
+ fuzzer:
4
+ iterations: 0
5
+ random-order: true
6
+ runstatedir: "/tmp/frr-cli-fuzzer"
7
+
8
+ frr-build-parameters:
9
+ # FRR's sysconfdir (--sysconfdir).
10
+ sysconfdir: "/etc/frr"
11
+
12
+ # FRR's localstatedir (--localstatedir).
13
+ localstatedir: "/var/run/frr"
14
+
15
+ # FRR's user (--enable-user).
16
+ user: "frr"
17
+
18
+ # FRR's group (--enable-group).
19
+ group: "frr"
20
+
21
+ daemons:
22
+ - zebra
23
+ #- bgpd
24
+ - ospfd
25
+ - ospf6d
26
+ - isisd
27
+ - fabricd
28
+ - ripd
29
+ - ripngd
30
+ - eigrpd
31
+ - pimd
32
+ - ldpd
33
+ - nhrpd
34
+ - babeld
35
+ #- bfdd
36
+ - pbrd
37
+ - staticd
38
+ - sharpd
39
+
40
+ regexps:
41
+ A.B.C.D/M: "1.1.1.1/32"
42
+ A.B.C.D: "1.1.1.1"
43
+ X:X::X:X/M: "2001:db8::1/128"
44
+ X:X::X:X: "2001:db8::1"
45
+ INTERFACE: "eth99"
46
+ IFNAME: "eth99"
47
+ AA:BB:CC...: "1:1:1 2:2:2"
48
+ AA:BB:CC: "1:1:1"
49
+ AA:NN...: "1:1 2:2"
50
+ AA:NN: "1:1"
51
+ ASN:nn_or_IP-address:nn: "1:1"
52
+ ASN:NN_OR_IP-ADDRESS:NN: "1:1"
53
+ RTLIST...: "1:1 2:2"
54
+ YY:YY:YY:YY:YY:YY: "11:11:11:11:11:11"
55
+ MAC: "11:11:11:11:11:11"
56
+ M:A:C: "11:11:11:11:11:11"
57
+ M:A:C/M: "11:11:11:11:11:11/48"
58
+ HH:MM:SS: "10:10:10"
59
+ MONTH: "January"
60
+ BANDWIDTH: "1000"
61
+ PERCENTAGE: "50"
62
+
63
+ whitelist:
64
+ #- ^show (ip|ipv6)
65
+ #- redistribute
66
+
67
+ blacklist:
68
+ - output file
69
+ - ^write
70
+ - ^copy
71
+ - ^list
72
+ - ^find
73
+ - ^exit
74
+ - ^quit
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
+ #- ^(no )?debug
83
+
84
+ 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"
130
+
131
+ configs:
132
+ all: |
133
+ hostname %(daemon)
134
+ log file %(runstatedir)/%(daemon).log
135
+ log commands
136
+ !
137
+ debug northbound
138
+ !
139
+ zebra: |
140
+ route-map WORD permit 10
141
+ vrf WORD
142
+ !
143
+ bgpd: |
144
+ route-map WORD permit 10
145
+ ip prefix-list WORD permit any
146
+ access-list WORD permit any
147
+ !
148
+ router bgp 1
149
+ neighbor 1.1.1.1 remote-as 1
150
+ neighbor 2001:db8::1 remote-as 1
151
+ neighbor WORD peer-group
152
+ neighbor WORD remote-as 1
153
+ !
154
+ address-family ipv6 unicast
155
+ neighbor 1.1.1.1 activate
156
+ neighbor 2001:db8::1 activate
157
+ neighbor WORD activate
158
+ exit-address-family
159
+ !
160
+ !
161
+ router bgp 2 view VIEWVRFNAME
162
+ !
163
+ ospfd: |
164
+ route-map WORD permit 10
165
+ !
166
+ router ospf
167
+ !
168
+ ospf6d: |
169
+ route-map WORD permit 10
170
+ !
171
+ router ospf6
172
+ !
173
+ isisd: |
174
+ route-map WORD permit 10
175
+ !
176
+ fabricd: |
177
+ route-map WORD permit 10
178
+ !
179
+ ripd: |
180
+ route-map WORD permit 10
181
+ ip prefix-list WORD permit any
182
+ access-list WORD permit any
183
+ !
184
+ !key chain WORD
185
+ ! key 2147483647
186
+ !
187
+ router rip
188
+ distribute-list WORD in eth99
189
+ !
190
+ ripngd: |
191
+ route-map WORD permit 10
192
+ ipv6 prefix-list WORD permit any
193
+ ipv6 access-list WORD permit any
194
+ !
195
+ router ripng
196
+ distribute-list WORD in eth99
197
+ !
198
+ eigrpd: |
199
+ route-map WORD permit 10
200
+ !
201
+ pimd: |
202
+ route-map WORD permit 10
203
+ vrf WORD
204
+ !
205
+ ldpd: |
206
+ nhrpd: |
207
+ babeld: |
208
+ bfdd: |
209
+ pbrd: |
210
+ staticd: |
211
+ vrf WORD
212
+ !
213
+ sharpd: |
214
+ route-map WORD permit 10
215
+ !
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ require 'frr-cli-fuzzer/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "frr-cli-fuzzer"
7
+ spec.version = FrrCliFuzzer::VERSION
8
+ spec.authors = ["Renato Westphal"]
9
+ spec.email = ["renato@opensourcerouting.org"]
10
+
11
+ spec.summary = %q{FRR CLI fuzzer.}
12
+ spec.homepage = "https://github.com/rwestphal/frr-cli-fuzzer"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = "bin"
17
+ spec.executables = "frr-cli-fuzzer"
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.11"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_dependency "ffi"
23
+ end
@@ -0,0 +1,29 @@
1
+ require 'ffi'
2
+
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
12
+
13
+ # include/uapi/linux/sched.h
14
+ CLONE_NEWNS = 0x00020000
15
+ CLONE_NEWPID = 0x20000000
16
+ CLONE_NEWNET = 0x40000000
17
+
18
+ # include/uapi/linux/fs.h
19
+ MS_NOSUID = (1 << 1)
20
+ MS_NODEV = (1 << 2)
21
+ MS_NOEXEC = (1 << 3)
22
+ MS_REC = (1 << 14)
23
+ MS_PRIVATE = (1 << 18)
24
+
25
+ # include/uapi/linux/prctl.h
26
+ PR_SET_PDEATHSIG = 1
27
+ PR_SET_CHILD_SUBREAPER = 36
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ module FrrCliFuzzer
2
+ class LinuxNamespace
3
+ attr_accessor :pid
4
+
5
+ # Create a child process running on a separate network and mount namespace.
6
+ def fork_and_unshare
7
+ begin
8
+ io_in, io_out = IO.pipe
9
+
10
+ pid = Kernel.fork do
11
+ unshare(LibC::CLONE_NEWNS | LibC::CLONE_NEWPID | LibC::CLONE_NEWNET)
12
+
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)
24
+
25
+ mount_propagation(LibC::MS_REC | LibC::MS_PRIVATE)
26
+ mount_proc
27
+ yield
28
+ end
29
+ $VERBOSE = warn_level
30
+ io_out.puts "#{pid}"
31
+ exit(0)
32
+ end
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)
40
+ end
41
+ end
42
+
43
+ # Set the mount propagation of the process.
44
+ def mount_propagation(flags)
45
+ mount('none', '/', nil, flags, nil)
46
+ end
47
+
48
+ # Mount the proc filesystem (useful after creating a new PID namespace).
49
+ 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)
53
+ end
54
+
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
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module FrrCliFuzzer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,275 @@
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
+
7
+ module FrrCliFuzzer
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'
14
+
15
+ class << self
16
+ def init(iterations: nil,
17
+ random_order: nil,
18
+ runstatedir: nil,
19
+ frr_build_parameters: nil,
20
+ daemons: nil,
21
+ configs: nil,
22
+ nodes: nil,
23
+ regexps: nil,
24
+ whitelist: nil,
25
+ blacklist: nil)
26
+ # Load configuration and default values if necessary.
27
+ @iterations = iterations || DFLT_ITERATIONS
28
+ @random_order = random_order || false
29
+ @runstatedir = runstatedir || DFLT_RUNSTATEDIR
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 || []
36
+ @configs = configs || []
37
+ @nodes = nodes || []
38
+ @regexps = regexps || []
39
+ @whitelist = whitelist || []
40
+ @blacklist = blacklist || []
41
+
42
+ # Initialize counters.
43
+ @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
49
+ @segfaults = {}
50
+
51
+ # Security check to prevent accidental deletion of data.
52
+ unless @runstatedir.include?('frr-cli-fuzzer')
53
+ abort("The runstatedir configuration parameter must contain "\
54
+ "'frr-cli-fuzzer' somewhere in the path.")
55
+ end
56
+ FileUtils.rm_rf(@runstatedir)
57
+ FileUtils.mkdir_p(@runstatedir)
58
+ FileUtils.chown_R(@frr['user'], @frr['group'], @runstatedir)
59
+
60
+ # Create a new process on a new pid, mount and network namespace.
61
+ @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
+
69
+ # 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"
78
+ end
79
+
80
+ # Bind mount a path under the configured runstatedir.
81
+ def mount(path, user, group)
82
+ source = "#{@runstatedir}/#{path}"
83
+ FileUtils.mkdir_p(path)
84
+ FileUtils.mkdir_p(source)
85
+ FileUtils.chown_R(user, group, source)
86
+ system("#{nsenter} mount --bind #{source} #{path}")
87
+ end
88
+
89
+ # Save configuration in the file system.
90
+ def save_config(daemon, config)
91
+ path = "#{@runstatedir}/#{@frr['sysconfdir']}/#{daemon}.conf"
92
+ File.open(path, 'w') { |file| file.write(config) }
93
+ end
94
+
95
+ # Generate FRR configuration file.
96
+ def gen_config(daemon)
97
+ config = @configs['all'] || ''
98
+ config += @configs[daemon] || ''
99
+
100
+ # Replace variables.
101
+ config.gsub!('%(daemon)', daemon)
102
+ config.gsub!('%(runstatedir)', @runstatedir)
103
+
104
+ save_config(daemon, config)
105
+ end
106
+
107
+ # Generate FRR configuration files.
108
+ def gen_configs
109
+ save_config('vtysh', '')
110
+ @daemons.each do |daemon|
111
+ gen_config(daemon)
112
+ end
113
+ end
114
+
115
+ # Start a FRR daemon.
116
+ def start_daemon(daemon)
117
+ # Remove old pid file if it exists.
118
+ FileUtils.rm_f("#{@runstatedir}/#{@frr['localstatedir']}/#{daemon}.pid")
119
+
120
+ # Spawn new process.
121
+ pid = Process.spawn("#{nsenter} #{daemon} -d --log=stdout "\
122
+ ">> #{@runstatedir}/#{daemon}.stdout "\
123
+ "2>> #{@runstatedir}/#{daemon}.stderr")
124
+ Process.detach(pid)
125
+ end
126
+
127
+ # Start all FRR daemons.
128
+ def start_daemons
129
+ @daemons.each do |daemon|
130
+ start_daemon(daemon)
131
+ end
132
+ end
133
+
134
+ # Check if a FRR daemon is still alive.
135
+ def daemon_alive?(daemon)
136
+ `#{nsenter} ps aux | grep #{daemon} | grep -E -v "defunct|grep"` != ''
137
+ end
138
+
139
+ # Check if a command should be white-list filtered.
140
+ def filter_whitelist(command)
141
+ return false if @whitelist.empty?
142
+
143
+ @whitelist.each do |regexp|
144
+ return false if command =~ /#{regexp}/
145
+ end
146
+ true
147
+ end
148
+
149
+ # Check if a command should be black-list filtered.
150
+ def filter_blacklist(command)
151
+ @blacklist.each do |regexp|
152
+ return true if command =~ /#{regexp}/
153
+ end
154
+ false
155
+ end
156
+
157
+ # Prepare command to be used by the CLI fuzzing tester.
158
+ def prepare_command(command)
159
+ new_command = ''
160
+
161
+ command.split.each do |word|
162
+ # Custom regexps.
163
+ @regexps.each_pair do |input, option|
164
+ word.sub!(input, option)
165
+ end
166
+
167
+ # Handle intervals.
168
+ if word =~ /(\d+\-\d+)/
169
+ interval = word.scanf('(%d-%d)')
170
+ new_command << interval[1].to_s
171
+ else
172
+ new_command << word
173
+ end
174
+
175
+ # Append whitespace after each word.
176
+ new_command << ' '
177
+ end
178
+
179
+ new_command.rstrip
180
+ end
181
+
182
+ # Obtain array of the commands we want to test.
183
+ def prepare_commmands
184
+ commands = []
185
+
186
+ @nodes.each do |hierarchy|
187
+ permutations = `#{nsenter} vtysh #{hierarchy} -c \"list permutations\"`
188
+ permutations.each_line do |command|
189
+ command = command.strip
190
+
191
+ # Check whitelist and blacklist.
192
+ if filter_whitelist(command)
193
+ puts "filtering (whitelist): #{command}"
194
+ @counters['filtered-whitelist'] += 1
195
+ next
196
+ end
197
+ if filter_blacklist(command)
198
+ puts "filtering (blacklist): #{command}"
199
+ @counters['filtered-blacklist'] += 1
200
+ next
201
+ end
202
+
203
+ @counters['non-filtered-cmds'] += 1
204
+
205
+ commands.push("vtysh #{hierarchy} -c \"#{prepare_command(command)}\"")
206
+ end
207
+ end
208
+ puts "non-filtered commands: #{@counters['non-filtered-cmds']}"
209
+
210
+ commands
211
+ end
212
+
213
+ # Send command to all running FRR daemons.
214
+ def send_command(command)
215
+ puts "testing: #{command}"
216
+
217
+ vtysh_log = "#{@runstatedir}/vtysh.txt"
218
+ File.open(vtysh_log, 'a') { |f| f.puts command }
219
+ system("#{nsenter} #{command} >> #{vtysh_log} 2>&1")
220
+ end
221
+
222
+ # Print the results of the fuzzing tests.
223
+ def print_results
224
+ puts "\nresults:"
225
+ puts "- non-filtered commands: #{@counters['non-filtered-cmds']}"
226
+ puts "- whitelist filtered commands: #{@counters['filtered-whitelist']}"
227
+ puts "- blacklist filtered commands: #{@counters['filtered-blacklist']}"
228
+ puts "- tested commands: #{@counters['tested-cmds']}"
229
+ puts "- segfaults detected: #{@counters['segfaults']}"
230
+ @segfaults.each_pair do |msg, count|
231
+ puts " (x#{count}) #{msg}"
232
+ end
233
+ end
234
+
235
+ # Log a segfault to both the standard output and to the fuzzer output file.
236
+ def log_segfault(daemon, command)
237
+ msg = "#{daemon} aborted: #{command}"
238
+ puts msg
239
+ File.open("#{@runstatedir}/output.txt", 'a') { |f| f.puts msg }
240
+
241
+ @counters['segfaults'] += 1
242
+ @segfaults[msg] = @segfaults[msg].to_i + 1
243
+ end
244
+
245
+ # Start fuzzing tests.
246
+ def test_fuzzing
247
+ iteration = 0
248
+ commands = prepare_commmands
249
+ return if commands.empty?
250
+
251
+ loop do
252
+ iteration += 1
253
+ puts "\nfuzz iteration: ##{iteration}"
254
+ commands.shuffle! if @random_order
255
+
256
+ # Iterate over all commands.
257
+ commands.each do |command|
258
+ @counters['tested-cmds'] += 1
259
+ send_command(command)
260
+
261
+ # Check if all daemons are still alive.
262
+ @daemons.each do |daemon|
263
+ next if daemon_alive?(daemon)
264
+
265
+ log_segfault(daemon, command)
266
+ start_daemon(daemon)
267
+ end
268
+ end
269
+
270
+ # Check if this is the last iteration.
271
+ break if @iterations > 0 && iteration == @iterations
272
+ end
273
+ end
274
+ end
275
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: frr-cli-fuzzer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Renato Westphal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ffi
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - renato@opensourcerouting.org
58
+ executables:
59
+ - frr-cli-fuzzer
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/frr-cli-fuzzer
69
+ - bin/setup
70
+ - config.yml
71
+ - frr-cli-fuzzer.gemspec
72
+ - lib/frr-cli-fuzzer.rb
73
+ - lib/frr-cli-fuzzer/libc.rb
74
+ - lib/frr-cli-fuzzer/linux_namespace.rb
75
+ - lib/frr-cli-fuzzer/version.rb
76
+ homepage: https://github.com/rwestphal/frr-cli-fuzzer
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.5.2.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: FRR CLI fuzzer.
100
+ test_files: []