module FrrCliFuzzer

Constants

DFLT_FRR_GROUP
DFLT_FRR_LOCALSTATE_DIR
DFLT_FRR_SYSCONFDIR
DFLT_FRR_USER
DFLT_ITERATIONS
DFLT_RUNSTATEDIR
VERSION

Public Class Methods

bind_mount(path, user, group) click to toggle source

Bind mount a path under the configured runstatedir.

# File lib/frr-cli-fuzzer.rb, line 70
def bind_mount(path, user, group)
  source = "#{@runstatedir}/#{path}"
  FileUtils.mkdir_p(path)
  FileUtils.mkdir_p(source)
  FileUtils.chown_R(user, group, source)
  system("#{@ns.nsenter} mount --bind #{source} #{path}")
end
daemon_alive?(daemon) click to toggle source

Check if a FRR daemon is still alive.

# File lib/frr-cli-fuzzer.rb, line 128
def daemon_alive?(daemon)
  `#{@ns.nsenter} ps aux | grep #{daemon} | grep -E -v "defunct|grep"` != ""
end
filter_blacklist(command, blacklist) click to toggle source

Check if a command should be black-list filtered.

# File lib/frr-cli-fuzzer.rb, line 145
def filter_blacklist(command, blacklist)
  blacklist += @global_blacklist

  blacklist.each do |regexp|
    return true if command =~ /#{regexp}/
  end
  false
end
filter_whitelist(command, whitelist) click to toggle source

Check if a command should be white-list filtered.

# File lib/frr-cli-fuzzer.rb, line 133
def filter_whitelist(command, whitelist)
  whitelist += @global_whitelist

  return false if whitelist.empty?

  whitelist.each do |regexp|
    return false if command =~ /#{regexp}/
  end
  true
end
gen_config(daemon) click to toggle source

Generate FRR configuration file.

# File lib/frr-cli-fuzzer.rb, line 85
def gen_config(daemon)
  config = @configs["all"] || ""
  config += @configs[daemon] || ""

  # Replace variables.
  config.gsub!("%(daemon)", daemon)
  config.gsub!("%(logfile)", "#{@runstatedir}/#{daemon}.log")

  save_config(daemon, config)
end
gen_configs() click to toggle source

Generate FRR configuration files.

# File lib/frr-cli-fuzzer.rb, line 97
def gen_configs
  save_config("vtysh", "")
  @daemons.keys.each do |daemon|
    gen_config(daemon)
  end
end
init(iterations: nil, random_order: nil, runstatedir: nil, frr_build_parameters: nil, daemons: nil, configs: nil, nodes: nil, regexps: nil, global_whitelist: nil, global_blacklist: nil) click to toggle source
# File lib/frr-cli-fuzzer.rb, line 16
def init(iterations: nil,
         random_order: nil,
         runstatedir: nil,
         frr_build_parameters: nil,
         daemons: nil,
         configs: nil,
         nodes: nil,
         regexps: nil,
         global_whitelist: nil,
         global_blacklist: nil)
  # Load configuration and default values if necessary.
  @iterations = iterations || DFLT_ITERATIONS
  @random_order = random_order || false
  @runstatedir = runstatedir || DFLT_RUNSTATEDIR
  @frr = frr_build_parameters || []
  @frr["sysconfdir"] ||= DFLT_FRR_SYSCONFDIR
  @frr["localstatedir"] ||= DFLT_FRR_LOCALSTATE_DIR
  @frr["user"] ||= DFLT_FRR_USER
  @frr["group"] ||= DFLT_FRR_GROUP
  daemons ||= []
  @daemons = Hash[daemons.collect { |daemon| [daemon, nil] }]
  @configs = configs || []
  @nodes = nodes || []
  @regexps = regexps || []
  @global_whitelist = global_whitelist || []
  @global_blacklist = global_blacklist || []

  # Initialize counters.
  @counters = {}
  @counters["non-filtered-cmds"] = 0
  @counters["filtered-blacklist"] = 0
  @counters["filtered-whitelist"] = 0
  @counters["tested-cmds"] = 0
  @counters["segfaults"] = 0
  @segfaults = {}

  # Security check to prevent accidental deletion of data.
  unless @runstatedir.include?("frr-cli-fuzzer")
    abort("The runstatedir configuration parameter must contain "\
          "\"frr-cli-fuzzer\" somewhere in the path.")
  end
  FileUtils.rm_rf(@runstatedir)
  FileUtils.mkdir_p(@runstatedir)
  FileUtils.chown_R(@frr["user"], @frr["group"], @runstatedir)

  # Create a new process on a new pid, mount and network namespace.
  @ns = LinuxNamespace.new

  # Bind mount FRR directories.
  bind_mount(@frr["sysconfdir"], @frr["user"], @frr["group"])
  bind_mount(@frr["localstatedir"], @frr["user"], @frr["group"])
end
log_segfault(daemon, command) click to toggle source

Log a segfault to both the standard output and to the fuzzer output file.

# File lib/frr-cli-fuzzer.rb, line 243
def log_segfault(daemon, command)
  msg = "#{daemon} aborted: #{command}"
  pid = @daemons[daemon]

  @counters["segfaults"] += 1
  @segfaults[msg] ||= []
  @segfaults[msg].push(pid)
  msg << " (PID: #{pid})"
  puts msg
  File.open("#{@runstatedir}/segfaults.txt", "a") { |f| f.puts msg }
end
prepare_command(command) click to toggle source

Prepare command to be used by the CLI fuzzing tester.

# File lib/frr-cli-fuzzer.rb, line 155
def prepare_command(command)
  new_command = ""

  command.split.each do |word|
    # Custom regexps.
    @regexps.each_pair do |input, option|
      word.sub!(input, option)
    end

    # Handle intervals.
    if word =~ /(\d+\-\d+)/
      interval = word.scanf("(%d-%d)")
      new_command << interval[1].to_s
    else
      new_command << word
    end

    # Append whitespace after each word.
    new_command << " "
  end

  new_command.rstrip
end
prepare_commmands() click to toggle source

Obtain array of the commands we want to test.

# File lib/frr-cli-fuzzer.rb, line 180
def prepare_commmands
  commands = []

  @nodes.each do |node|
    hierarchy = node["hierarchy"]
    whitelist = node["whitelist"] || []
    blacklist = node["blacklist"] || []

    permutations = `#{@ns.nsenter} vtysh #{hierarchy} -c \"list permutations\"`
    permutations.each_line do |command|
      command = command.strip

      # Check whitelist and blacklist.
      if filter_whitelist(command, whitelist)
        puts "filtering (whitelist): #{command}"
        @counters["filtered-whitelist"] += 1
        next
      end
      if filter_blacklist(command, blacklist)
        puts "filtering (blacklist): #{command}"
        @counters["filtered-blacklist"] += 1
        next
      end

      @counters["non-filtered-cmds"] += 1

      commands.push("vtysh #{hierarchy} -c \"#{prepare_command(command)}\"")
    end
  end
  puts "non-filtered commands: #{@counters['non-filtered-cmds']}"

  commands
end
print_results() click to toggle source

Print the results of the fuzzing tests.

rename_log_files(daemon) click to toggle source

Append PID of the aborted daemon to its log files.

# File lib/frr-cli-fuzzer.rb, line 256
def rename_log_files(daemon)
  pid = @daemons[daemon]

  ["log", "stdout", "stderr"].each do |suffix|
    log_file = "#{@runstatedir}/#{daemon}.#{suffix}"
    FileUtils.mv(log_file, "#{log_file}.#{pid}")
  end
end
save_config(daemon, config) click to toggle source

Save configuration in the file system.

# File lib/frr-cli-fuzzer.rb, line 79
def save_config(daemon, config)
  path = "#{@runstatedir}/#{@frr['sysconfdir']}/#{daemon}.conf"
  File.open(path, "w") { |file| file.write(config) }
end
send_command(command) click to toggle source

Send command to all running FRR daemons.

# File lib/frr-cli-fuzzer.rb, line 215
def send_command(command)
  puts "testing: #{command}"

  ["stdout", "stderr"].each do |suffix|
    File.open("#{@runstatedir}/vtysh.#{suffix}", "a") { |f| f.puts command }
  end
  Kernel.system("#{@ns.nsenter} #{command}",
                out: ["#{@runstatedir}/vtysh.stdout", "a"],
                err: ["#{@runstatedir}/vtysh.stderr", "a"])
end
start_daemon(daemon) click to toggle source

Start a FRR daemon.

# File lib/frr-cli-fuzzer.rb, line 105
def start_daemon(daemon)
  # Remove old pid file if it exists.
  FileUtils.rm_f("#{@runstatedir}/#{@frr['localstatedir']}/#{daemon}.pid")

  # Spawn new process.
  pid = Process.spawn("#{@ns.nsenter} #{daemon} --log=stdout -d",
                      out: "#{@runstatedir}/#{daemon}.stdout",
                      err: "#{@runstatedir}/#{daemon}.stderr")
  Process.detach(pid)

  # Obtain the PID of the daemon as seen in the PID namespace where
  # it resides.
  @daemons[daemon] = `#{@ns.nsenter} pidof -s #{daemon}`.rstrip
end
start_daemons() click to toggle source

Start all FRR daemons.

# File lib/frr-cli-fuzzer.rb, line 121
def start_daemons
  @daemons.keys.each do |daemon|
    start_daemon(daemon)
  end
end
test_fuzzing() click to toggle source

Start fuzzing tests.

# File lib/frr-cli-fuzzer.rb, line 266
def test_fuzzing
  iteration = 0
  commands = prepare_commmands
  return if commands.empty?

  loop do
    iteration += 1
    puts "\nfuzz iteration: ##{iteration}"
    commands.shuffle! if @random_order

    # Iterate over all commands.
    commands.each do |command|
      @counters["tested-cmds"] += 1
      send_command(command)

      # Check if all daemons are still alive.
      @daemons.keys.each do |daemon|
        next if daemon_alive?(daemon)

        log_segfault(daemon, command)
        rename_log_files(daemon)
        start_daemon(daemon)
      end
    end

    # Check if this is the last iteration.
    break if @iterations > 0 && iteration == @iterations
  end
end