seccomp-tools 1.6.1 → 1.7.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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/LICENSE +21 -0
  4. data/README.md +267 -46
  5. data/completions/_seccomp-tools +83 -0
  6. data/completions/seccomp-tools.bash +59 -0
  7. data/completions/seccomp-tools.fish +52 -0
  8. data/ext/ptrace/ptrace.c +2 -2
  9. data/lib/seccomp-tools/asm/asm.rb +9 -4
  10. data/lib/seccomp-tools/asm/compiler.rb +39 -9
  11. data/lib/seccomp-tools/asm/sasm.tab.rb +31 -21
  12. data/lib/seccomp-tools/asm/sasm.y +15 -7
  13. data/lib/seccomp-tools/asm/scalar.rb +50 -7
  14. data/lib/seccomp-tools/asm/scanner.rb +40 -8
  15. data/lib/seccomp-tools/asm/statement.rb +14 -5
  16. data/lib/seccomp-tools/asm/token.rb +19 -1
  17. data/lib/seccomp-tools/audit/catalog.rb +66 -0
  18. data/lib/seccomp-tools/audit/checks/arch_unchecked.rb +41 -0
  19. data/lib/seccomp-tools/audit/checks/dangerous_allow.rb +34 -0
  20. data/lib/seccomp-tools/audit/checks/orw_chain.rb +41 -0
  21. data/lib/seccomp-tools/audit/checks/permissive_default.rb +29 -0
  22. data/lib/seccomp-tools/audit/checks/syscall_alt_gap.rb +44 -0
  23. data/lib/seccomp-tools/audit/checks/x32_guard.rb +46 -0
  24. data/lib/seccomp-tools/audit/checks.rb +42 -0
  25. data/lib/seccomp-tools/audit/finding.rb +25 -0
  26. data/lib/seccomp-tools/audit/policy.rb +126 -0
  27. data/lib/seccomp-tools/audit/report.rb +98 -0
  28. data/lib/seccomp-tools/audit.rb +48 -0
  29. data/lib/seccomp-tools/bpf.rb +27 -17
  30. data/lib/seccomp-tools/cli/asm.rb +7 -3
  31. data/lib/seccomp-tools/cli/audit.rb +86 -0
  32. data/lib/seccomp-tools/cli/base.rb +51 -8
  33. data/lib/seccomp-tools/cli/cli.rb +10 -6
  34. data/lib/seccomp-tools/cli/completion.rb +40 -0
  35. data/lib/seccomp-tools/cli/disasm.rb +10 -6
  36. data/lib/seccomp-tools/cli/dump.rb +37 -52
  37. data/lib/seccomp-tools/cli/dumpable.rb +79 -0
  38. data/lib/seccomp-tools/cli/emu.rb +32 -9
  39. data/lib/seccomp-tools/cli/explain.rb +51 -0
  40. data/lib/seccomp-tools/cli/filter_input.rb +130 -0
  41. data/lib/seccomp-tools/const.rb +98 -15
  42. data/lib/seccomp-tools/consts/sys_nr/amd64.rb +1 -1
  43. data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
  44. data/lib/seccomp-tools/disasm/disasm.rb +31 -13
  45. data/lib/seccomp-tools/dumper.rb +67 -35
  46. data/lib/seccomp-tools/emulator.rb +41 -16
  47. data/lib/seccomp-tools/error.rb +4 -2
  48. data/lib/seccomp-tools/explain/analysis.rb +67 -0
  49. data/lib/seccomp-tools/explain/path_facts.rb +110 -0
  50. data/lib/seccomp-tools/explain/qword.rb +204 -0
  51. data/lib/seccomp-tools/explain/renderer.rb +128 -0
  52. data/lib/seccomp-tools/explain/summary.rb +218 -0
  53. data/lib/seccomp-tools/explain/verdict.rb +44 -0
  54. data/lib/seccomp-tools/explain.rb +38 -0
  55. data/lib/seccomp-tools/instruction/alu.rb +14 -9
  56. data/lib/seccomp-tools/instruction/base.rb +39 -10
  57. data/lib/seccomp-tools/instruction/jmp.rb +50 -23
  58. data/lib/seccomp-tools/instruction/ld.rb +46 -21
  59. data/lib/seccomp-tools/instruction/ldx.rb +4 -3
  60. data/lib/seccomp-tools/instruction/misc.rb +11 -9
  61. data/lib/seccomp-tools/instruction/ret.rb +12 -6
  62. data/lib/seccomp-tools/instruction/st.rb +15 -6
  63. data/lib/seccomp-tools/instruction/stx.rb +4 -3
  64. data/lib/seccomp-tools/logger.rb +15 -2
  65. data/lib/seccomp-tools/symbolic/constraint.rb +80 -0
  66. data/lib/seccomp-tools/symbolic/executor.rb +210 -0
  67. data/lib/seccomp-tools/symbolic/expr.rb +185 -0
  68. data/lib/seccomp-tools/symbolic/state.rb +82 -0
  69. data/lib/seccomp-tools/syscall.rb +74 -22
  70. data/lib/seccomp-tools/util.rb +70 -11
  71. data/lib/seccomp-tools/version.rb +1 -1
  72. data/lib/seccomp-tools.rb +10 -1
  73. metadata +83 -11
  74. data/lib/seccomp-tools/disasm/context.rb +0 -171
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'seccomp-tools/cli/asm'
4
+ require 'seccomp-tools/cli/audit'
5
+ require 'seccomp-tools/cli/completion'
4
6
  require 'seccomp-tools/cli/disasm'
5
7
  require 'seccomp-tools/cli/dump'
6
8
  require 'seccomp-tools/cli/emu'
9
+ require 'seccomp-tools/cli/explain'
7
10
  require 'seccomp-tools/version'
8
11
 
9
12
  module SeccompTools
@@ -12,9 +15,12 @@ module SeccompTools
12
15
  # Handled commands
13
16
  COMMANDS = {
14
17
  'asm' => SeccompTools::CLI::Asm,
18
+ 'audit' => SeccompTools::CLI::Audit,
19
+ 'completion' => SeccompTools::CLI::Completion,
15
20
  'disasm' => SeccompTools::CLI::Disasm,
16
21
  'dump' => SeccompTools::CLI::Dump,
17
- 'emu' => SeccompTools::CLI::Emu
22
+ 'emu' => SeccompTools::CLI::Emu,
23
+ 'explain' => SeccompTools::CLI::Explain
18
24
  }.freeze
19
25
 
20
26
  # Main usage message.
@@ -57,14 +63,12 @@ EOS
57
63
  COMMANDS[cmd].new(argv).handle
58
64
  end
59
65
 
60
- # Just write message to stdout.
66
+ # Writes a message to stdout, followed by a newline.
61
67
  # @param [String] msg
62
- # The message.
63
- # @return [false]
64
- # Always return +false+.
68
+ # The message to be written.
69
+ # @return [void]
65
70
  def show(msg)
66
71
  puts msg
67
- false
68
72
  end
69
73
 
70
74
  class << self
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/cli/base'
4
+ require 'seccomp-tools/logger'
5
+
6
+ module SeccompTools
7
+ module CLI
8
+ # Handle 'completion' command.
9
+ class Completion < Base
10
+ # Summary of this command.
11
+ SUMMARY = 'Print a shell completion script.'
12
+ # Usage of this command.
13
+ USAGE = "completion - #{SUMMARY}\n\nUsage: seccomp-tools completion <bash|zsh|fish>".freeze
14
+
15
+ # Maps a shell name to the completion script that ships with the gem.
16
+ SCRIPTS = { 'bash' => 'seccomp-tools.bash', 'zsh' => '_seccomp-tools', 'fish' => 'seccomp-tools.fish' }.freeze
17
+
18
+ # Define option parser.
19
+ # @return [OptionParser]
20
+ # The parser of this command's options.
21
+ def parser
22
+ @parser ||= OptionParser.new do |opt|
23
+ opt.banner = usage
24
+ end
25
+ end
26
+
27
+ # Writes the completion script for the requested shell to stdout.
28
+ # @return [void]
29
+ def handle
30
+ return unless super
31
+
32
+ shell = argv.shift
33
+ file = SCRIPTS[shell]
34
+ return CLI.show(parser.help) if file.nil?
35
+
36
+ output { File.read(File.join(__dir__, '..', '..', '..', 'completions', file)) }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -10,8 +10,11 @@ module SeccompTools
10
10
  # Summary of this command.
11
11
  SUMMARY = 'Disassemble seccomp bpf.'
12
12
  # Usage of this command.
13
- USAGE = "disasm - #{SUMMARY}\n\nUsage: seccomp-tools disasm BPF_FILE [options]"
13
+ USAGE = "disasm - #{SUMMARY}\n\nUsage: seccomp-tools disasm BPF_FILE [options]".freeze
14
14
 
15
+ # Instantiate a {Disasm} object, showing raw BPF and inferred arguments by default.
16
+ #
17
+ # Takes the same arguments as {Base#initialize}.
15
18
  def initialize(*)
16
19
  super
17
20
  option[:bpf] = true
@@ -20,10 +23,11 @@ module SeccompTools
20
23
 
21
24
  # Define option parser.
22
25
  # @return [OptionParser]
26
+ # The parser of this command's options.
23
27
  def parser
24
28
  @parser ||= OptionParser.new do |opt|
25
29
  opt.banner = usage
26
- opt.on('-o', '--output FILE', 'Output result into FILE instead of stdout.') do |o|
30
+ opt.on('-o', '--output FILE', 'Write output to FILE instead of stdout.') do |o|
27
31
  option[:ofile] = o
28
32
  end
29
33
  option_arch(opt)
@@ -35,9 +39,9 @@ module SeccompTools
35
39
  'Default: true') do |f|
36
40
  option[:arg_infer] = f
37
41
  end
38
- opt.on('--asm-able', 'Output with this flag is a valid input of "seccomp-tools asm".',
39
- 'By default, "seccomp-tools disasm" is in a human-readable format that easy for analysis.',
40
- 'Passing this flag can have the output be simplified to a valid input for "seccomp-tools asm".',
42
+ opt.on('--asm-able', 'Make the output valid input for "seccomp-tools asm".',
43
+ 'By default, "seccomp-tools disasm" outputs a human-readable format meant for analysis.',
44
+ 'With this flag the output is simplified so it can be fed back to "seccomp-tools asm".',
41
45
  'This flag implies "--no-bpf --no-arg-infer".',
42
46
  'Default: false') do |_f|
43
47
  option[:bpf] = false
@@ -46,7 +50,7 @@ module SeccompTools
46
50
  end
47
51
  end
48
52
 
49
- # Handle options.
53
+ # Disassembles the input file and writes the result.
50
54
  # @return [void]
51
55
  def handle
52
56
  return unless super
@@ -1,39 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'shellwords'
4
-
5
3
  require 'seccomp-tools/cli/base'
4
+ require 'seccomp-tools/cli/filter_input'
6
5
  require 'seccomp-tools/disasm/disasm'
7
6
  require 'seccomp-tools/dumper'
8
- require 'seccomp-tools/logger'
9
7
 
10
8
  module SeccompTools
11
9
  module CLI
12
10
  # Handle 'dump' command.
13
11
  class Dump < Base
12
+ include FilterInput
13
+
14
14
  # Summary of this command.
15
- SUMMARY = 'Automatically dump seccomp bpf from execution file(s).'
15
+ SUMMARY = 'Automatically dump seccomp bpf from executable(s).'
16
16
  # Usage of this command.
17
- USAGE = "dump - #{SUMMARY}\nNOTE : This function is only available on Linux." \
18
- "\n\nUsage: seccomp-tools dump [exec] [options]"
17
+ USAGE = "dump - #{SUMMARY}\nNOTE: This command is only available on Linux." \
18
+ "\n\nUsage: seccomp-tools dump [EXEC] [options]".freeze
19
19
 
20
+ # Instantiate a {Dump} object, dumping the first filter as disassembly by default.
21
+ #
22
+ # Takes the same arguments as {Base#initialize}.
20
23
  def initialize(*)
21
24
  super
22
25
  option[:format] = :disasm
23
- option[:limit] = 1
24
- option[:pid] = nil
25
26
  end
26
27
 
27
28
  # Define option parser.
28
29
  # @return [OptionParser]
30
+ # The parser of this command's options.
29
31
  def parser
30
32
  @parser ||= OptionParser.new do |opt|
31
33
  opt.banner = usage
32
- opt.on('-c', '--sh-exec <command>', 'Executes the given command (via sh).',
33
- 'Use this option if want to pass arguments or do pipe things to the execution file.',
34
- 'e.g. use `-c "./bin > /dev/null"` to dump seccomp without being mixed with stdout.') do |command|
35
- option[:command] = command
36
- end
34
+ option_filter_source(opt, 'dump')
37
35
 
38
36
  opt.on('-f', '--format FORMAT', %i[disasm raw inspect],
39
37
  'Output format. FORMAT can only be one of <disasm|raw|inspect>.',
@@ -41,55 +39,42 @@ module SeccompTools
41
39
  option[:format] = f
42
40
  end
43
41
 
44
- opt.on('-l', '--limit LIMIT', 'Limit the number of calling "prctl(PR_SET_SECCOMP)".',
45
- 'The target process will be killed whenever its calling times reaches LIMIT.',
46
- 'Default: 1', Integer) do |l|
47
- option[:limit] = l
48
- end
49
-
50
- opt.on('-o', '--output FILE', 'Output result into FILE instead of stdout.',
42
+ opt.on('-o', '--output FILE', 'Write output to FILE instead of stdout.',
51
43
  'If multiple seccomp syscalls have been invoked (see --limit),',
52
- 'results will be written to FILE, FILE_1, FILE_2.. etc.',
53
- 'For example, "--output out.bpf" and the output files are out.bpf, out_1.bpf, ...') do |o|
44
+ 'results are written to FILE, FILE_1, FILE_2, etc.',
45
+ 'For example, with "--output out.bpf" the output files are out.bpf, out_1.bpf, ...') do |o|
54
46
  option[:ofile] = o
55
47
  end
56
-
57
- opt.on('-p', '--pid PID', 'Dump installed seccomp filters of the existing process.',
58
- 'You must have CAP_SYS_ADMIN (e.g. be root) in order to use this option.',
59
- Integer) do |p|
60
- option[:pid] = p
61
- end
62
48
  end
63
49
  end
64
50
 
65
- # Handle options.
51
+ # Traces the target process and writes out the seccomp filters it installs.
52
+ #
53
+ # Only available on Linux, logs an error and returns otherwise.
66
54
  # @return [void]
67
55
  def handle
68
- return Logger.error('Dump is only available on Linux.') unless Dumper::SUPPORTED
56
+ return unless dumping_supported?
69
57
  return unless super
70
58
 
71
- block = lambda do |bpf, arch|
72
- case option[:format]
73
- when :inspect then output { "\"#{bpf.bytes.map { |b| format('\\x%02X', b) }.join}\"\n" }
74
- when :raw then output { bpf }
75
- when :disasm then output { SeccompTools::Disasm.disasm(bpf, arch: arch) }
76
- end
77
- end
78
- if option[:pid].nil?
79
- option[:command] = argv.shift unless argv.empty?
80
- SeccompTools::Dumper.dump('/bin/sh', '-c', option[:command], limit: option[:limit], &block)
81
- else
82
- begin
83
- SeccompTools::Dumper.dump_by_pid(option[:pid], option[:limit], &block)
84
- rescue Errno::EPERM, Errno::EACCES => e
85
- Logger.error(<<~EOS)
86
- #{e}
87
- PTRACE_SECCOMP_GET_FILTER requires CAP_SYS_ADMIN
88
- Try:
89
- sudo env "PATH=$PATH" #{(%w[seccomp-tools] + ARGV).shelljoin}
90
- EOS
91
- exit(1)
92
- end
59
+ collect_filters.each { |bpf, arch| emit(bpf, arch) }
60
+ end
61
+
62
+ private
63
+
64
+ # A positional argument is always an executable to trace, never a raw BPF blob to read: that is
65
+ # +disasm+'s job, and a non-ELF executable (a shell script wrapping the target) must still run.
66
+ # @return [Boolean]
67
+ def accepts_raw_bpf?
68
+ false
69
+ end
70
+
71
+ # Writes one dumped filter in the requested format.
72
+ # @return [void]
73
+ def emit(bpf, arch)
74
+ case option[:format]
75
+ when :inspect then output { "\"#{bpf.bytes.map { |b| format('\\x%02X', b) }.join}\"\n" }
76
+ when :raw then output { bpf }
77
+ when :disasm then output { SeccompTools::Disasm.disasm(bpf, arch:) }
93
78
  end
94
79
  end
95
80
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ require 'seccomp-tools/dumper'
6
+ require 'seccomp-tools/logger'
7
+
8
+ module SeccompTools
9
+ module CLI
10
+ # Shared helpers for the commands that dump seccomp filters via ptrace ({Dump} and {Explain}).
11
+ module Dumpable
12
+ # Dumps the seccomp filters from a command run via +sh+, or from an existing process, yielding
13
+ # each installed filter.
14
+ #
15
+ # When +pid+ is given the process is traced (requiring +CAP_SYS_ADMIN+); otherwise +command+
16
+ # is executed. On a permission error while tracing a pid, {#dump_permission_error} exits.
17
+ # Warns when nothing was installed, so a caller that produces no output still tells the user why.
18
+ # @param [String?] command
19
+ # The command to run, when +pid+ is +nil+.
20
+ # @param [Integer?] pid
21
+ # The process to trace, or +nil+ to run +command+.
22
+ # @param [Integer] limit
23
+ # Stop after this many installed filters.
24
+ # @param [Float?] timeout
25
+ # Seconds to wait for +command+, ignored when tracing a pid.
26
+ # @yieldparam [String] bpf
27
+ # One installed filter, as raw bytes.
28
+ # @yieldparam [Symbol?] arch
29
+ # The architecture of the traced process, if known.
30
+ # @return [Array]
31
+ # One entry per filter: the block's return values. Empty when nothing was installed.
32
+ def dump_seccomp(command:, pid:, limit:, timeout:, &)
33
+ filters = if pid
34
+ dump_seccomp_by_pid(pid, limit, &)
35
+ else
36
+ SeccompTools::Dumper.dump('/bin/sh', '-c', command, limit:, timeout:, &)
37
+ end
38
+ Logger.warn('No seccomp filter was installed.') if filters.empty?
39
+ filters
40
+ end
41
+
42
+ # Whether tracer-based dumping is available on this platform (Linux only). Logs an error when
43
+ # it is not, so callers can guard with +return unless dumping_supported?+.
44
+ # @return [Boolean]
45
+ def dumping_supported?
46
+ return true if SeccompTools::Dumper::SUPPORTED
47
+
48
+ Logger.error('Dumping a filter from an executable or process is only available on Linux.')
49
+ false
50
+ end
51
+
52
+ private
53
+
54
+ # Traces +pid+, translating a permission error into the standard hint.
55
+ def dump_seccomp_by_pid(pid, limit, &)
56
+ SeccompTools::Dumper.dump_by_pid(pid, limit, &)
57
+ rescue Errno::EPERM, Errno::EACCES => e
58
+ dump_permission_error(e)
59
+ end
60
+
61
+ # Reports a permission error from tracing a process and exits.
62
+ #
63
+ # Dumping a filter by pid needs +CAP_SYS_ADMIN+ for +PTRACE_SECCOMP_GET_FILTER+.
64
+ # @param [SystemCallError] err
65
+ # The +Errno::EPERM+ or +Errno::EACCES+ that was raised.
66
+ # @raise [SystemExit]
67
+ # Always; the process is terminated with exit status 1.
68
+ def dump_permission_error(err)
69
+ Logger.error(<<~EOS)
70
+ #{err}
71
+ PTRACE_SECCOMP_GET_FILTER requires CAP_SYS_ADMIN
72
+ Try:
73
+ sudo env "PATH=$PATH" #{(%w[seccomp-tools] + ARGV).shelljoin}
74
+ EOS
75
+ exit(1)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -15,8 +15,11 @@ module SeccompTools
15
15
  # Summary of this command.
16
16
  SUMMARY = 'Emulate seccomp rules.'
17
17
  # Usage of this command.
18
- USAGE = "emu - #{SUMMARY}\n\nUsage: seccomp-tools emu [options] BPF_FILE [sys_nr [arg0 [arg1 ... arg5]]]"
18
+ USAGE = "emu - #{SUMMARY}\n\nUsage: seccomp-tools emu [options] BPF_FILE [sys_nr [arg0 [arg1 ... arg5]]]".freeze
19
19
 
20
+ # Instantiate an {Emu} object, defaulting to verbose output.
21
+ #
22
+ # Takes the same arguments as {Base#initialize}.
20
23
  def initialize(*)
21
24
  super
22
25
  option[:verbose] = 1
@@ -24,6 +27,7 @@ module SeccompTools
24
27
 
25
28
  # Define option parser.
26
29
  # @return [OptionParser]
30
+ # The parser of this command's options.
27
31
  def parser
28
32
  @parser ||= OptionParser.new do |opt|
29
33
  opt.banner = usage
@@ -33,10 +37,14 @@ module SeccompTools
33
37
  opt.on('-q', '--[no-]quiet', 'Run quietly, only show emulation result.') do |v|
34
38
  option[:verbose] = 0 if v
35
39
  end
40
+
41
+ opt.on('-i', '--ip=VAL', Integer, 'Set instruction pointer.') do |val|
42
+ option[:instruction_pointer] = val
43
+ end
36
44
  end
37
45
  end
38
46
 
39
- # Handle options.
47
+ # Emulates the filter against the given syscall and shows the resulting action.
40
48
  # @return [void]
41
49
  def handle
42
50
  return unless super
@@ -50,9 +58,13 @@ module SeccompTools
50
58
  sys = evaluate_sys_nr(sys) if sys
51
59
  args.map! { |v| Integer(v) }
52
60
  trace = Set.new
53
- res = SeccompTools::Emulator.new(insts, sys_nr: sys, args: args, arch: option[:arch]).run do |ctx|
54
- trace << ctx[:pc]
55
- end
61
+ res = SeccompTools::Emulator.new(
62
+ insts,
63
+ sys_nr: sys,
64
+ args:,
65
+ instruction_pointer: option[:instruction_pointer] && Integer(option[:instruction_pointer]),
66
+ arch: option[:arch]
67
+ ).run
56
68
 
57
69
  if option[:verbose] >= 1
58
70
  disasm = SeccompTools::Disasm.disasm(raw, arch: option[:arch]).lines
@@ -67,17 +79,28 @@ module SeccompTools
67
79
 
68
80
  private
69
81
 
82
+ # Resolves a syscall given on the command line, by name or by number.
83
+ #
70
84
  # @param [String] str
85
+ # A syscall name valid for +option[:arch]+, or an integer literal.
71
86
  # @return [Integer]
87
+ # The syscall number.
88
+ # @raise [ArgumentError]
89
+ # If +str+ is neither a known syscall name nor a valid integer.
72
90
  def evaluate_sys_nr(str)
73
91
  consts = SeccompTools::Const::Syscall.const_get(option[:arch].to_s.upcase)
74
92
  consts[str.to_sym] || Integer(str)
75
93
  end
76
94
 
77
- # output the path during emulation
95
+ # Outputs the disassembly, highlighting the lines that were executed during emulation.
96
+ #
78
97
  # @param [Array<String>] disasm
79
- # @param [Set] trace
80
- # @param [{Symbol => Object}] result
98
+ # Lines of the disassembly, including the two header lines.
99
+ # @param [Set<Integer>] trace
100
+ # Line numbers that were reached.
101
+ # @param [{Symbol => Integer}] result
102
+ # The emulation result, as returned by {SeccompTools::Emulator#run}.
103
+ # @return [void]
81
104
  def output_emulate_path(disasm, trace, result)
82
105
  output { disasm.shift }
83
106
  output { disasm.shift }
@@ -87,7 +110,7 @@ module SeccompTools
87
110
 
88
111
  Util.colorize(line, t: :gray)
89
112
  end
90
- # Too much remain, omit them.
113
+ # Too many lines remain; omit them.
91
114
  rem = disasm.size - idx - 1
92
115
  break output { Util.colorize("... (omitting #{rem} lines)\n", t: :gray) } if rem > 3 && idx > result[:pc] + 4
93
116
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/cli/base'
4
+ require 'seccomp-tools/cli/filter_input'
5
+ require 'seccomp-tools/disasm/disasm'
6
+ require 'seccomp-tools/explain'
7
+ require 'seccomp-tools/logger'
8
+
9
+ module SeccompTools
10
+ module CLI
11
+ # Handle 'explain' command.
12
+ class Explain < Base
13
+ include FilterInput
14
+
15
+ # Summary of this command.
16
+ SUMMARY = 'Summarize a seccomp filter as a per-action policy.'
17
+ # Usage of this command.
18
+ USAGE = "explain - #{SUMMARY}\n\nUsage: seccomp-tools explain [options] [BPF_FILE|EXEC]".freeze
19
+
20
+ # Define option parser.
21
+ # @return [OptionParser]
22
+ # The parser of this command's options.
23
+ def parser
24
+ @parser ||= OptionParser.new do |opt|
25
+ opt.banner = usage
26
+
27
+ option_filter_source(opt, 'explain')
28
+ option_arch(opt, 'With an executable or --pid the architecture is auto-detected instead.')
29
+ end
30
+ end
31
+
32
+ # Reads the filter(s) from a BPF file, an executable, or an existing process, then prints the
33
+ # policy of each.
34
+ # @return [void]
35
+ def handle
36
+ return unless super
37
+
38
+ filters = collect_filters
39
+ if filters.size > 1
40
+ Logger.warn("#{filters.size} filters are installed; they stack, so a syscall must pass every one " \
41
+ '(most restrictive wins). Each is explained separately below.')
42
+ end
43
+ filters.each_with_index do |(raw, arch, source), idx|
44
+ label = filters.size > 1 ? "#{source} (filter ##{idx})" : source
45
+ insts = SeccompTools::Disasm.to_bpf(raw, arch).map(&:inst)
46
+ output { SeccompTools::Explain.new(insts, arch:, source: label).summarize.to_s }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/cli/dumpable'
4
+ require 'seccomp-tools/logger'
5
+ require 'seccomp-tools/util'
6
+
7
+ module SeccompTools
8
+ module CLI
9
+ # Shared input handling for the commands that take a seccomp filter from a BPF file, stdin, an
10
+ # executable, or a running process ({Explain} and {Audit}): it resolves the positional argument
11
+ # and options into +[raw_bpf, arch, source]+ tuples, dumping via ptrace ({Dumpable}) when the
12
+ # input is a command or +--pid+. The including command must provide +option+, +argv+, +parser+,
13
+ # +input+ and +warn_ignored_arguments+ (all from {Base}).
14
+ module FilterInput
15
+ include Dumpable
16
+
17
+ private
18
+
19
+ # Registers the options every command taking its filter from a process shares - +-c/--sh-exec+,
20
+ # +-l/--limit+, +-p/--pid+ and +-t/--timeout+ - and their defaults, so the four stay described
21
+ # and parsed the same way wherever they appear. The counterpart of {Base#option_arch}.
22
+ #
23
+ # The descriptions hold for every including command, because the behaviour they describe lives
24
+ # in the shared code: {#collect_filters} gives +-c+ precedence, and {Dumpable#dump_seccomp}
25
+ # passes +--timeout+ only when running an executable, never when attaching to a +--pid+.
26
+ # @param [OptionParser] opt
27
+ # @param [String] action
28
+ # What the command does with each filter, woven into the descriptions.
29
+ # @return [void]
30
+ # @example
31
+ # option_filter_source(opt, 'explain')
32
+ def option_filter_source(opt, action)
33
+ option[:limit] = 1
34
+ opt.on('-c', '--sh-exec <command>', "Executes the given command (via sh) and #{action}s its seccomp.",
35
+ 'Use this to pass arguments or pipe things to the executable.',
36
+ 'e.g. use `-c "./bin > /dev/null"` to keep the program output out of the result.',
37
+ 'Takes precedence over the positional argument.') { |command| option[:command] = command }
38
+
39
+ opt.on('-l', '--limit LIMIT', Integer, "#{action.capitalize} only the first LIMIT installed filters.",
40
+ 'Only meaningful when the input is an executable or --pid. Default: 1',
41
+ 'An executable is killed once it reaches LIMIT.') { |l| option[:limit] = l }
42
+
43
+ opt.on('-p', '--pid PID', Integer, "#{action.capitalize} the seccomp filters installed on an existing process.",
44
+ 'You must have CAP_SYS_ADMIN (e.g. be root) to use this option.') { |p| option[:pid] = p }
45
+
46
+ opt.on('-t', '--timeout SEC', Float, 'Timeout (seconds) for the execution. Default: no timeout',
47
+ 'This option is ignored when --pid is given.') { |t| option[:timeout] = t }
48
+ end
49
+
50
+ # Resolves the input into an array of +[raw_bpf, arch, source]+ tuples, empty when there is
51
+ # nothing to process (help shown, or an error was logged).
52
+ #
53
+ # The input is one of three kinds:
54
+ # * a running process, when +--pid+ is given;
55
+ # * a raw BPF file (or stdin), when the positional argument is not an executable;
56
+ # * a command to run and trace - either +-c+, or a positional executable.
57
+ # @return [Array<Array(String, Symbol, String?)>]
58
+ def collect_filters
59
+ # -c/--sh-exec and --pid take precedence over a positional BPF file or executable.
60
+ option[:ifile] = argv.shift unless option[:command] || option[:pid]
61
+ warn_ignored_arguments
62
+
63
+ return dump_filters(command: nil, pid: option[:pid], source: "pid #{option[:pid]}") if option[:pid]
64
+
65
+ command = option[:command] || option[:ifile]
66
+ if command.nil? # nothing to process
67
+ CLI.show(parser.help)
68
+ return []
69
+ end
70
+ return read_raw_bpf if raw_bpf_file?
71
+
72
+ dump_filters(command:, pid: nil, source: command)
73
+ end
74
+
75
+ # Reads the positional file (or stdin) as a raw BPF blob, logging an error instead of
76
+ # crashing when it cannot be read.
77
+ # @return [Array<Array(String, Symbol, String?)>]
78
+ def read_raw_bpf
79
+ [[input, option[:arch], source_name(option[:ifile])]]
80
+ rescue SystemCallError => e
81
+ Logger.error(e.message)
82
+ []
83
+ end
84
+
85
+ # Should the input be read directly as a raw BPF blob, rather than run as a command? True when
86
+ # the command accepts blobs at all, no +-c+ was given, and the positional argument is not an
87
+ # executable (a plain file or stdin).
88
+ # @return [Boolean]
89
+ def raw_bpf_file?
90
+ accepts_raw_bpf? && !option[:command] && !executable?(option[:ifile])
91
+ end
92
+
93
+ # Does a positional argument that is not an ELF mean "read this filter"? True for the commands
94
+ # that analyze a filter ({Explain}, {Audit}); {Dump} overrides it to +false+, because its
95
+ # positional is always something to execute - a shell script is not an ELF but must still be
96
+ # run, not parsed as BPF.
97
+ # @return [Boolean]
98
+ def accepts_raw_bpf?
99
+ true
100
+ end
101
+
102
+ # Dumps filters from a command or pid and labels each with +source+.
103
+ # @return [Array<Array(String, Symbol, String?)>]
104
+ # The filter tuples, empty when dumping is unsupported or nothing was installed.
105
+ def dump_filters(command:, pid:, source:)
106
+ return [] unless dumping_supported?
107
+
108
+ dump_seccomp(command:, pid:, limit: option[:limit], timeout: option[:timeout]) do |bpf, arch|
109
+ [bpf, arch || option[:arch], source]
110
+ end
111
+ end
112
+
113
+ # Is +file+ an ELF executable to run, rather than a raw BPF blob or stdin to read?
114
+ # @param [String?] file
115
+ # The path to check. +nil+ (no argument) and +-+ (stdin) are not executables.
116
+ # @return [Boolean]
117
+ def executable?(file)
118
+ file && file != '-' && Util.elf?(file)
119
+ end
120
+
121
+ # The label shown in the header for +file+; +<STDIN>+ when reading from stdin.
122
+ # @param [String] file
123
+ # The input path, or +-+ for stdin.
124
+ # @return [String]
125
+ def source_name(file)
126
+ file == '-' ? '<STDIN>' : file
127
+ end
128
+ end
129
+ end
130
+ end