seccomp-tools 1.6.2 → 1.7.1

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/README.md +280 -42
  4. data/completions/_seccomp-tools +83 -0
  5. data/completions/seccomp-tools.bash +59 -0
  6. data/completions/seccomp-tools.fish +52 -0
  7. data/ext/ptrace/ptrace.c +2 -2
  8. data/lib/seccomp-tools/asm/asm.rb +9 -4
  9. data/lib/seccomp-tools/asm/compiler.rb +32 -2
  10. data/lib/seccomp-tools/asm/sasm.tab.rb +27 -19
  11. data/lib/seccomp-tools/asm/sasm.y +15 -7
  12. data/lib/seccomp-tools/asm/scalar.rb +50 -7
  13. data/lib/seccomp-tools/asm/scanner.rb +33 -1
  14. data/lib/seccomp-tools/asm/statement.rb +14 -5
  15. data/lib/seccomp-tools/asm/token.rb +19 -1
  16. data/lib/seccomp-tools/audit/catalog.rb +69 -0
  17. data/lib/seccomp-tools/audit/checks/arch_unchecked.rb +41 -0
  18. data/lib/seccomp-tools/audit/checks/dangerous_allow.rb +34 -0
  19. data/lib/seccomp-tools/audit/checks/orw_chain.rb +41 -0
  20. data/lib/seccomp-tools/audit/checks/permissive_default.rb +29 -0
  21. data/lib/seccomp-tools/audit/checks/syscall_alt_gap.rb +44 -0
  22. data/lib/seccomp-tools/audit/checks/x32_guard.rb +46 -0
  23. data/lib/seccomp-tools/audit/checks.rb +42 -0
  24. data/lib/seccomp-tools/audit/finding.rb +25 -0
  25. data/lib/seccomp-tools/audit/policy.rb +126 -0
  26. data/lib/seccomp-tools/audit/report.rb +98 -0
  27. data/lib/seccomp-tools/audit.rb +48 -0
  28. data/lib/seccomp-tools/bpf.rb +27 -17
  29. data/lib/seccomp-tools/cli/asm.rb +6 -2
  30. data/lib/seccomp-tools/cli/audit.rb +86 -0
  31. data/lib/seccomp-tools/cli/base.rb +51 -8
  32. data/lib/seccomp-tools/cli/cli.rb +9 -3
  33. data/lib/seccomp-tools/cli/completion.rb +40 -0
  34. data/lib/seccomp-tools/cli/disasm.rb +9 -5
  35. data/lib/seccomp-tools/cli/dump.rb +37 -52
  36. data/lib/seccomp-tools/cli/dumpable.rb +79 -0
  37. data/lib/seccomp-tools/cli/emu.rb +20 -5
  38. data/lib/seccomp-tools/cli/explain.rb +51 -0
  39. data/lib/seccomp-tools/cli/filter_input.rb +130 -0
  40. data/lib/seccomp-tools/const.rb +98 -15
  41. data/lib/seccomp-tools/consts/sys_arg.rb +43 -5
  42. data/lib/seccomp-tools/consts/sys_nr/aarch64.rb +52 -1
  43. data/lib/seccomp-tools/consts/sys_nr/amd64.rb +53 -1
  44. data/lib/seccomp-tools/consts/sys_nr/i386.rb +83 -2
  45. data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
  46. data/lib/seccomp-tools/consts/sys_nr/s390x.rb +30 -1
  47. data/lib/seccomp-tools/disasm/disasm.rb +30 -12
  48. data/lib/seccomp-tools/dumper.rb +67 -36
  49. data/lib/seccomp-tools/emulator.rb +40 -15
  50. data/lib/seccomp-tools/error.rb +4 -2
  51. data/lib/seccomp-tools/explain/analysis.rb +67 -0
  52. data/lib/seccomp-tools/explain/path_facts.rb +110 -0
  53. data/lib/seccomp-tools/explain/qword.rb +204 -0
  54. data/lib/seccomp-tools/explain/renderer.rb +128 -0
  55. data/lib/seccomp-tools/explain/summary.rb +218 -0
  56. data/lib/seccomp-tools/explain/verdict.rb +44 -0
  57. data/lib/seccomp-tools/explain.rb +38 -0
  58. data/lib/seccomp-tools/instruction/alu.rb +14 -9
  59. data/lib/seccomp-tools/instruction/base.rb +39 -10
  60. data/lib/seccomp-tools/instruction/jmp.rb +50 -23
  61. data/lib/seccomp-tools/instruction/ld.rb +46 -21
  62. data/lib/seccomp-tools/instruction/ldx.rb +4 -3
  63. data/lib/seccomp-tools/instruction/misc.rb +11 -9
  64. data/lib/seccomp-tools/instruction/ret.rb +12 -6
  65. data/lib/seccomp-tools/instruction/st.rb +15 -6
  66. data/lib/seccomp-tools/instruction/stx.rb +4 -3
  67. data/lib/seccomp-tools/logger.rb +14 -1
  68. data/lib/seccomp-tools/symbolic/constraint.rb +80 -0
  69. data/lib/seccomp-tools/symbolic/executor.rb +210 -0
  70. data/lib/seccomp-tools/symbolic/expr.rb +185 -0
  71. data/lib/seccomp-tools/symbolic/state.rb +82 -0
  72. data/lib/seccomp-tools/syscall.rb +73 -23
  73. data/lib/seccomp-tools/util.rb +76 -11
  74. data/lib/seccomp-tools/version.rb +1 -1
  75. data/lib/seccomp-tools.rb +10 -1
  76. metadata +36 -24
  77. data/lib/seccomp-tools/disasm/context.rb +0 -171
@@ -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]".freeze
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:) }
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
@@ -17,6 +17,9 @@ module SeccompTools
17
17
  # Usage of this command.
18
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
@@ -40,7 +44,7 @@ module SeccompTools
40
44
  end
41
45
  end
42
46
 
43
- # Handle options.
47
+ # Emulates the filter against the given syscall and shows the resulting action.
44
48
  # @return [void]
45
49
  def handle
46
50
  return unless super
@@ -75,17 +79,28 @@ module SeccompTools
75
79
 
76
80
  private
77
81
 
82
+ # Resolves a syscall given on the command line, by name or by number.
83
+ #
78
84
  # @param [String] str
85
+ # A syscall name valid for +option[:arch]+, or an integer literal.
79
86
  # @return [Integer]
87
+ # The syscall number.
88
+ # @raise [ArgumentError]
89
+ # If +str+ is neither a known syscall name nor a valid integer.
80
90
  def evaluate_sys_nr(str)
81
91
  consts = SeccompTools::Const::Syscall.const_get(option[:arch].to_s.upcase)
82
92
  consts[str.to_sym] || Integer(str)
83
93
  end
84
94
 
85
- # output the path during emulation
95
+ # Outputs the disassembly, highlighting the lines that were executed during emulation.
96
+ #
86
97
  # @param [Array<String>] disasm
87
- # @param [Set] trace
88
- # @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]
89
104
  def output_emulate_path(disasm, trace, result)
90
105
  output { disasm.shift }
91
106
  output { disasm.shift }
@@ -95,7 +110,7 @@ module SeccompTools
95
110
 
96
111
  Util.colorize(line, t: :gray)
97
112
  end
98
- # Too much remain, omit them.
113
+ # Too many lines remain; omit them.
99
114
  rem = disasm.size - idx - 1
100
115
  break output { Util.colorize("... (omitting #{rem} lines)\n", t: :gray) } if rem > 3 && idx > result[:pc] + 4
101
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
@@ -5,15 +5,48 @@ module SeccompTools
5
5
  module Const
6
6
  # For BPF / seccomp.
7
7
  module BPF
8
+ # Byte offsets of the fields of the filter's input buffer:
9
+ # struct seccomp_data {
10
+ # int nr; // SYS_NUMBER
11
+ # __u32 arch; // ARCH
12
+ # __u64 instruction_pointer; // INSTRUCTION_POINTER
13
+ # __u64 args[6]; // ARGS
14
+ # };
15
+ # The 64-bit fields are each two 32-bit words a filter loads separately (see {QWORD_BASES}).
16
+ module SeccompData
17
+ # Byte offset of the syscall number.
18
+ SYS_NUMBER = 0
19
+ # Byte offset of the architecture.
20
+ ARCH = 4
21
+ # Byte offset of the instruction pointer.
22
+ INSTRUCTION_POINTER = 8
23
+ # Byte offset of the first 64-bit argument.
24
+ ARGS = 16
25
+ # Total size in bytes; the +len+ load returns this.
26
+ SIZE = 64
27
+ # Byte offsets of the 64-bit fields (+instruction_pointer+ and the six arguments), each a
28
+ # pair of 32-bit words.
29
+ QWORD_BASES = [INSTRUCTION_POINTER, *(ARGS...SIZE).step(8)].freeze
30
+ # Display names of the fixed (non-argument) fields, keyed by byte offset. For the 64-bit
31
+ # +instruction_pointer+ this is the field's base name; a high-word load appends +>> 32+.
32
+ NAMES = { SYS_NUMBER => 'sys_number', ARCH => 'arch', INSTRUCTION_POINTER => 'instruction_pointer' }.freeze
33
+ end
34
+
8
35
  # sizeof(struct seccomp_data)
9
- SIZEOF_SECCOMP_DATA = 64
36
+ SIZEOF_SECCOMP_DATA = SeccompData::SIZE
10
37
 
11
38
  # option set seccomp
12
39
  PR_SET_SECCOMP = 22
13
40
 
41
+ # strict mode
42
+ SECCOMP_MODE_STRICT = 1
43
+
14
44
  # filter mode
15
45
  SECCOMP_MODE_FILTER = 2
16
46
 
47
+ # For syscall +seccomp+
48
+ SECCOMP_SET_MODE_STRICT = 0
49
+
17
50
  # For syscall +seccomp+
18
51
  SECCOMP_SET_MODE_FILTER = 1
19
52
 
@@ -86,7 +119,8 @@ module SeccompTools
86
119
  lsh: 0x60,
87
120
  rsh: 0x70,
88
121
  neg: 0x80,
89
- # mod: 0x90, # not support
122
+ # mod (0x90) is intentionally omitted: the kernel's seccomp_check_filter() allowlist rejects
123
+ # BPF_MOD (EINVAL), so it can never appear in a loadable seccomp filter.
90
124
  xor: 0xa0
91
125
  }.freeze
92
126
 
@@ -95,6 +129,24 @@ module SeccompTools
95
129
  tax: 0x00,
96
130
  txa: 0x80
97
131
  }.freeze
132
+
133
+ # The action a seccomp return +value+ names, e.g. +"ALLOW"+ or +"ERRNO(5)"+, or +nil+ when
134
+ # the action bits are not a value the kernel defines. The data part is shown for the actions
135
+ # that consume it: +ERRNO+ always, and +TRACE+/+TRAP+ when non-zero (0 is their idle default).
136
+ # The result is both human-readable and re-assemblable.
137
+ # @param [Integer] value
138
+ # @return [String?]
139
+ def self.action_label(value)
140
+ action = ACTION.invert[value & SECCOMP_RET_ACTION_FULL]
141
+ return if action.nil?
142
+
143
+ data = value & SECCOMP_RET_DATA
144
+ case action
145
+ when :ERRNO then "ERRNO(#{data})"
146
+ when :TRACE, :TRAP then data.zero? ? action.to_s : "#{action}(#{data})"
147
+ else action.to_s
148
+ end
149
+ end
98
150
  end
99
151
 
100
152
  # Define syscall numbers for all architectures.
@@ -104,17 +156,21 @@ module SeccompTools
104
156
 
105
157
  # To dynamically fetch constants from files.
106
158
  # @param [Symbol] cons
107
- # Name of const.
108
- # @return [Object]
109
- # Value of that +cons+.
159
+ # Name of const, an upcased architecture name such as +:AMD64+.
160
+ # @return [{Symbol => Integer}]
161
+ # The syscall table of that architecture, mapping name to number.
162
+ # @raise [NameError]
163
+ # If no syscall table exists for +cons+.
110
164
  def const_missing(cons)
111
165
  load_const(cons) || super
112
166
  end
113
167
 
114
168
  # Load from file and define const value.
115
169
  # @param [Symbol] cons
116
- # Name of const.
117
- # @return [Object]
170
+ # Name of const, an upcased architecture name such as +:AMD64+.
171
+ # @return [{Symbol => Integer}?]
172
+ # The syscall table of that architecture, or +nil+ if it has no file under
173
+ # +consts/sys_nr/+.
118
174
  def load_const(cons)
119
175
  arch = cons.to_s.downcase
120
176
  filename = File.join(__dir__, 'consts', 'sys_nr', "#{arch}.rb")
@@ -124,6 +180,10 @@ module SeccompTools
124
180
  end
125
181
 
126
182
  # Helper for loading syscall prototypes from generated sys_arg.rb.
183
+ #
184
+ # @return [{Symbol => Array<String>}]
185
+ # Syscall name to its argument names. Lookups of an +x32_+-prefixed name fall back to the
186
+ # unprefixed one, and unknown names give +nil+.
127
187
  def load_args
128
188
  hash = instance_eval(File.read(File.join(__dir__, 'consts', 'sys_arg.rb')))
129
189
  Hash.new do |_h, k|
@@ -145,6 +205,7 @@ module SeccompTools
145
205
  amd64: 'ARCH_X86_64',
146
206
  i386: 'ARCH_I386',
147
207
  aarch64: 'ARCH_AARCH64',
208
+ riscv64: 'ARCH_RISCV64',
148
209
  s390x: 'ARCH_S390X'
149
210
  }.freeze
150
211
 
@@ -153,19 +214,41 @@ module SeccompTools
153
214
  'ARCH_X86_64' => 0xc000003e,
154
215
  'ARCH_I386' => 0x40000003,
155
216
  'ARCH_AARCH64' => 0xc00000b7,
217
+ 'ARCH_RISCV64' => 0xc00000f3,
156
218
  'ARCH_S390X' => 0x80000016
157
219
  }.freeze
220
+
221
+ # The architecture symbol (e.g. +:amd64+) for an +AUDIT_ARCH_*+ value, or +nil+ when it is
222
+ # not one seccomp-tools knows.
223
+ # @param [Integer] audit_val
224
+ # @return [Symbol?]
225
+ def self.arch_symbol(audit_val)
226
+ name = ARCH.invert[audit_val]
227
+ name && ARCH_NAME.invert[name]
228
+ end
158
229
  end
159
230
 
160
- # Endianess constants.
231
+ # Endianness constants.
161
232
  module Endian
162
- # Defining default endianess of architectures.
163
- ENDIAN = {
164
- i386: '<',
165
- amd64: '<',
166
- aarch64: '<',
167
- s390x: '>'
168
- }.freeze
233
+ # +__AUDIT_ARCH_LE+ from +uapi/linux/audit.h+: the bit an +AUDIT_ARCH_*+ value carries iff
234
+ # the architecture is little-endian. The kernel encodes endianness in the arch token itself,
235
+ # so it never needs to be guessed from an architecture's name.
236
+ AUDIT_ARCH_LE = 0x40000000
237
+
238
+ # Endianness of each architecture as a pack/unpack format modifier, derived from the
239
+ # {Audit::ARCH} values instead of being maintained by hand.
240
+ ENDIAN = Audit::ARCH_NAME.transform_values do |name|
241
+ Audit::ARCH[name].anybits?(AUDIT_ARCH_LE) ? '<' : '>'
242
+ end.freeze
243
+
244
+ # Whether +arch+ is big-endian, i.e. stores the high 32-bit word of a 64-bit +seccomp_data+
245
+ # field (+instruction_pointer+ or an argument) first. See +arch_arg_offset_lo/hi+ in
246
+ # libseccomp and the +syscall_arg+ macro of the kernel's seccomp selftests.
247
+ # @param [Symbol] arch
248
+ # @return [Boolean]
249
+ def self.big?(arch)
250
+ ENDIAN[arch] == '>'
251
+ end
169
252
  end
170
253
  end
171
254
  end