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
@@ -361,5 +361,34 @@
361
361
  faccessat2: 439,
362
362
  process_madvise: 440,
363
363
  epoll_pwait2: 441,
364
- mount_setattr: 442
364
+ mount_setattr: 442,
365
+ quotactl_fd: 443,
366
+ landlock_create_ruleset: 444,
367
+ landlock_add_rule: 445,
368
+ landlock_restrict_self: 446,
369
+ memfd_secret: 447,
370
+ process_mrelease: 448,
371
+ futex_waitv: 449,
372
+ set_mempolicy_home_node: 450,
373
+ cachestat: 451,
374
+ fchmodat2: 452,
375
+ map_shadow_stack: 453,
376
+ futex_wake: 454,
377
+ futex_wait: 455,
378
+ futex_requeue: 456,
379
+ statmount: 457,
380
+ listmount: 458,
381
+ lsm_get_self_attr: 459,
382
+ lsm_set_self_attr: 460,
383
+ lsm_list_modules: 461,
384
+ mseal: 462,
385
+ setxattrat: 463,
386
+ getxattrat: 464,
387
+ listxattrat: 465,
388
+ removexattrat: 466,
389
+ open_tree_attr: 467,
390
+ file_getattr: 468,
391
+ file_setattr: 469,
392
+ listns: 470,
393
+ rseq_slice_yield: 471
365
394
  }
@@ -3,7 +3,8 @@
3
3
  require 'set'
4
4
 
5
5
  require 'seccomp-tools/bpf'
6
- require 'seccomp-tools/disasm/context'
6
+ require 'seccomp-tools/symbolic/constraint'
7
+ require 'seccomp-tools/symbolic/state'
7
8
  require 'seccomp-tools/util'
8
9
 
9
10
  module SeccompTools
@@ -12,24 +13,38 @@ module SeccompTools
12
13
  module_function
13
14
 
14
15
  # Disassemble BPF codes.
16
+ #
17
+ # Emulates the filter to track what each register holds, so syscall names and argument
18
+ # positions can be inferred and shown as comments.
15
19
  # @param [String] raw
16
20
  # The raw BPF bytes.
17
- # @param [Symbol] arch
18
- # Architecture.
21
+ # @param [Symbol?] arch
22
+ # Target architecture, must be one of {SeccompTools::Util.supported_archs}.
23
+ # Defaults to {SeccompTools::Util.system_arch} when +nil+.
19
24
  # @param [Boolean] display_bpf
25
+ # Whether to prepend each line with its raw +code+, +jt+, +jf+ and +k+ fields.
20
26
  # @param [Boolean] arg_infer
27
+ # Whether to annotate lines with the inferred syscall name and argument.
28
+ # @return [String]
29
+ # The disassembly result, ready to be printed.
30
+ # @example
31
+ # SeccompTools::Disasm.disasm(raw, arch: :amd64, display_bpf: false)
32
+ # #=> "0000: A = sys_number\n0001: if (A == read) goto 0003\n0002: return KILL\n0003: return ALLOW\n"
21
33
  def disasm(raw, arch: nil, display_bpf: true, arg_infer: true)
22
34
  codes = to_bpf(raw, arch)
23
- contexts = Array.new(codes.size) { Set.new }
24
- contexts[0].add(Context.new)
25
- # all we care is whether A is data[*]
26
- dis = codes.zip(contexts).map do |code, ctxs|
27
- ctxs.each do |ctx|
28
- code.branch(ctx) do |pc, c|
29
- contexts[pc].add(c) unless pc >= contexts.size
35
+ states = Array.new(codes.size) { Set.new }
36
+ states[0].add(Symbolic::State.initial)
37
+ # A forward pass (jumps only go forward) tracking, per line, the states that can reach it, so
38
+ # syscall names and argument positions can be inferred from what each register holds. Unlike
39
+ # the Executor, this over-approximates - it forks every conditional instead of folding - so
40
+ # dead lines are still rendered.
41
+ dis = codes.zip(states).map do |code, sts|
42
+ sts.each do |st|
43
+ code.branch(st) do |pc, s|
44
+ states[pc].add(s) unless pc >= states.size
30
45
  end
31
46
  end
32
- code.contexts = ctxs
47
+ code.states = sts
33
48
  code.disasm(code: display_bpf, arg_infer:)
34
49
  end.join("\n")
35
50
  if display_bpf
@@ -45,8 +60,11 @@ module SeccompTools
45
60
 
46
61
  # Convert raw BPF string to array of {BPF}.
47
62
  # @param [String] raw
48
- # @param [Symbol] arch
63
+ # The raw BPF bytes, each instruction being 8 bytes long.
64
+ # @param [Symbol?] arch
65
+ # Target architecture, defaults to {SeccompTools::Util.system_arch} when +nil+.
49
66
  # @return [Array<BPF>]
67
+ # One {BPF} per instruction, in order.
50
68
  def to_bpf(raw, arch)
51
69
  arch ||= Util.system_arch
52
70
  raw.scan(/.{8}/m).map.with_index { |b, i| BPF.new(b, arch, i) }
@@ -1,57 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'os'
3
+ require 'timeout'
4
4
 
5
5
  require 'seccomp-tools/logger'
6
- require 'seccomp-tools/ptrace' if OS.linux?
6
+ require 'seccomp-tools/util'
7
+ require 'seccomp-tools/ptrace' if SeccompTools::Util.linux?
7
8
  require 'seccomp-tools/syscall'
8
9
 
9
10
  module SeccompTools
10
11
  # Dump seccomp-bpf using ptrace of binary.
11
12
  module Dumper
12
13
  # Whether the dumper is supported.
13
- # Dumper works based on ptrace, so we need the platform be Linux.
14
- SUPPORTED = OS.linux?
14
+ # Dumper works based on ptrace, so we need the platform to be Linux.
15
+ SUPPORTED = Util.linux?
15
16
 
16
17
  module_function
17
18
 
18
19
  # Main bpf dump function.
19
- # Yield seccomp bpf whenever find a +prctl(SET_SECCOMP)+ call.
20
+ # Yields seccomp bpf whenever a +prctl(SET_SECCOMP)+ call is found.
20
21
  #
21
22
  # @param [Array<String>] args
22
- # The arguments for target execution file.
23
+ # The command to be executed, i.e. the target executable followed by its arguments.
23
24
  # @param [Integer] limit
24
25
  # By default, +dump+ will only dump the first +SET_SECCOMP+ call.
25
26
  # Set +limit+ to the number of calling +prctl(SET_SECCOMP)+ then the child process will be killed when number of
26
27
  # calling +prctl+ reaches +limit+.
27
28
  #
28
29
  # Negative number for unlimited.
30
+ # @param [Float?] timeout
31
+ # Number of seconds to wait for the target process. When the timeout is reached, the target
32
+ # process is killed and the filters dumped so far are returned. +nil+ for no timeout.
29
33
  # @yieldparam [String] bpf
30
34
  # Seccomp bpf in raw bytes.
31
- # @yieldparam [Symbol] arch
32
- # Architecture of the target process.
35
+ # @yieldparam [Symbol?] arch
36
+ # Architecture of the target process, +nil+ when it cannot be determined.
37
+ # See {SeccompTools::Util.process_arch}.
33
38
  # @return [Array<Object>, Array<String>]
34
- # Return the block returned. If block is not given, array of raw bytes will be returned.
39
+ # One entry per dumped filter: the block's return values when a block is given, otherwise the
40
+ # raw bytes. Empty on a non-Linux platform, where dumping is unsupported.
35
41
  # @example
36
42
  # dump('ls', '-l', '-a')
37
43
  # #=> []
38
44
  # dump('spec/binary/twctf-2016-diary') { |c| c[0, 10] }
39
45
  # #=> [" \x00\x00\x00\x00\x00\x00\x00\x15\x00"]
40
- # @todo
41
- # +timeout+ option.
42
- def dump(*args, limit: 1, &block)
46
+ def dump(*args, limit: 1, timeout: nil, &block)
43
47
  return [] unless SUPPORTED
44
48
 
45
49
  pid = fork { handle_child(*args) }
46
- Handler.new(pid).handle(limit, &block)
50
+ Handler.new(pid).handle(limit, timeout: timeout, &block)
47
51
  end
48
52
 
49
- # Do the tracer things.
53
+ # Traces a forked child, single-stepping it through its syscalls and capturing the seccomp
54
+ # filters it installs.
50
55
  class Handler
51
56
  # Instantiate a {Handler} object.
52
57
  # @param [Integer] pid
53
58
  # The process id after fork.
54
59
  def initialize(pid)
60
+ @pids = [pid]
55
61
  Process.waitpid(pid)
56
62
  opt = Ptrace::O_TRACESYSGOOD | Ptrace::O_TRACECLONE | Ptrace::O_TRACEFORK | Ptrace::O_TRACEVFORK
57
63
  Ptrace.setoptions(pid, 0, opt)
@@ -62,45 +68,59 @@ module SeccompTools
62
68
  #
63
69
  # @param [Integer] limit
64
70
  # Child will be killed when number of calling +prctl(SET_SECCOMP)+ reaches +limit+.
71
+ # @param [Float?] timeout
72
+ # Kill the child processes when +timeout+ seconds have elapsed. +nil+ for no timeout.
65
73
  # @yieldparam [String] bpf
66
74
  # Seccomp bpf in raw bytes.
67
75
  # @yieldparam [Symbol] arch
68
76
  # Architecture. See {SeccompTools::Syscall::ABI} for supported architectures.
69
77
  # @return [Array<Object>, Array<String>]
70
- # Return the block returned. If block is not given, array of raw bytes will be returned.
71
- def handle(limit, &block)
78
+ # One entry per dumped filter: the block's return values when a block is given, otherwise
79
+ # the raw bytes.
80
+ def handle(limit, timeout: nil, &block)
72
81
  collect = []
73
82
  syscalls = {} # record last syscall
74
- loop while wait_syscall do |child|
75
- if syscalls[child].nil? # invoke syscall
76
- syscalls[child] = syscall(child)
77
- next true
83
+ begin
84
+ Timeout.timeout(timeout) do
85
+ loop while wait_syscall do |child|
86
+ if syscalls[child].nil? # invoke syscall
87
+ syscalls[child] = syscall(child)
88
+ next true
89
+ end
90
+ # syscall finished
91
+ sys = syscalls[child]
92
+ syscalls[child] = nil
93
+ if sys.set_seccomp? && syscall(child).ret.zero? # consider successful call only
94
+ bpf = sys.dump_bpf
95
+ collect << (block.nil? ? bpf : yield(bpf, sys.arch))
96
+ limit -= 1
97
+ end
98
+ !limit.zero?
99
+ end
78
100
  end
79
- # syscall finished
80
- sys = syscalls[child]
81
- syscalls[child] = nil
82
- if sys.set_seccomp? && syscall(child).ret.zero? # consider successful call only
83
- bpf = sys.dump_bpf
84
- collect << (block.nil? ? bpf : yield(bpf, sys.arch))
85
- limit -= 1
86
- end
87
- !limit.zero?
101
+ rescue Timeout::Error
102
+ # keep the filters dumped so far; fall through to kill the children
88
103
  end
89
- syscalls.each_key { |cpid| Process.kill('KILL', cpid) if alive?(cpid) }
104
+ @pids.each { |cpid| Process.kill('KILL', cpid) if alive?(cpid) }
90
105
  Process.waitall
91
106
  collect
92
107
  end
93
108
 
94
109
  private
95
110
 
111
+ # Waits until a traced child enters or leaves a syscall, then resumes it.
112
+ #
96
113
  # @yieldparam [Integer] pid
114
+ # Id of the child that stopped.
115
+ # @yieldreturn [Boolean]
116
+ # Whether tracing should continue.
97
117
  # @return [Boolean]
98
118
  # +true+ for continue,
99
- # +false+ for break.
119
+ # +false+ for break. Also +false+ once no children are left.
100
120
  def wait_syscall
101
121
  child, status = Process.wait2
122
+ @pids << child unless @pids.include?(child)
102
123
  cont = true
103
- # TODO: Test if clone / vfork works
104
124
  if [Ptrace::EVENT_CLONE, Ptrace::EVENT_FORK, Ptrace::EVENT_VFORK].include?(status.to_i >> 16)
105
125
  # New child launched!
106
126
  # newpid = SeccompTools::Ptrace.geteventmsg(child)
@@ -113,11 +133,20 @@ module SeccompTools
113
133
  false
114
134
  end
115
135
 
136
+ # Reads the syscall the stopped child is currently invoking.
137
+ #
138
+ # @param [Integer] pid
139
+ # Id of the stopped child.
116
140
  # @return [SeccompTools::Syscall]
117
141
  def syscall(pid)
118
142
  SeccompTools::Syscall.new(pid)
119
143
  end
120
144
 
145
+ # Is the process still running?
146
+ #
147
+ # @param [Integer] pid
148
+ # Id of the process to be checked.
149
+ # @return [Boolean]
121
150
  def alive?(pid)
122
151
  Process.getpgid(pid)
123
152
  true
@@ -149,10 +178,11 @@ module SeccompTools
149
178
  # Number of filters to dump. Negative number for unlimited.
150
179
  # @yieldparam [String] bpf
151
180
  # Seccomp bpf in raw bytes.
152
- # @yieldparam [Symbol] arch
153
- # Architecture of the target process (always nil right now).
181
+ # @yieldparam [Symbol?] arch
182
+ # Architecture of the target process, +nil+ when it cannot be determined.
183
+ # See {SeccompTools::Util.process_arch}.
154
184
  # @return [Array<Object>, Array<String>]
155
- # Return the block returned. If block is not given, array of raw bytes will be returned.
185
+ # Returns what the block returned. If a block is not given, an array of raw bytes will be returned.
156
186
  # @raise [Errno::ESRCH]
157
187
  # Raises when the target process does not exist.
158
188
  # @raise [Errno::EPERM]
@@ -174,6 +204,7 @@ module SeccompTools
174
204
  return [] unless SUPPORTED
175
205
 
176
206
  collect = []
207
+ arch = Util.process_arch(pid)
177
208
  Ptrace.attach_and_wait(pid)
178
209
  begin
179
210
  i = 0
@@ -183,7 +214,7 @@ module SeccompTools
183
214
  rescue Errno::ENOENT, Errno::EINVAL
184
215
  break
185
216
  end
186
- collect << (block.nil? ? bpf : yield(bpf, nil))
217
+ collect << (block.nil? ? bpf : yield(bpf, arch))
187
218
  i += 1
188
219
  end
189
220
  ensure
@@ -3,20 +3,21 @@
3
3
  require 'seccomp-tools/const'
4
4
 
5
5
  module SeccompTools
6
- # For emulating seccomp.
6
+ # Runs a seccomp filter against a hypothetical syscall to find out which action it returns.
7
7
  class Emulator
8
8
  # Instantiate a {Emulator} object.
9
9
  #
10
10
  # All parameters except +instructions+ are optional. A warning is shown when uninitialized data is accessed.
11
11
  # @param [Array<Instruction::Base>] instructions
12
- # @param [Integer] sys_nr
12
+ # The filter to be emulated, as returned by +SeccompTools::Disasm.to_bpf(raw, arch).map(&:inst)+.
13
+ # @param [Integer?] sys_nr
13
14
  # Syscall number.
14
15
  # @param [Array<Integer>] args
15
- # Syscall arguments
16
- # @param [Integer] instruction_pointer
17
- # Program counter address when this syscall invoked.
16
+ # Syscall arguments.
17
+ # @param [Integer?] instruction_pointer
18
+ # Program counter address when this syscall is invoked.
18
19
  # @param [Symbol?] arch
19
- # System architecture is used when this parameter is not provided.
20
+ # Defaults to the system architecture when not provided.
20
21
  #
21
22
  # See {SeccompTools::Util.supported_archs} for list of supported architectures.
22
23
  def initialize(instructions, sys_nr: nil, args: [], instruction_pointer: nil, arch: nil)
@@ -24,16 +25,27 @@ module SeccompTools
24
25
  @sys_nr = sys_nr
25
26
  @args = args
26
27
  @ip = instruction_pointer
27
- @arch = audit(arch || Util.system_arch)
28
+ arch ||= Util.system_arch
29
+ @arch = audit(arch)
30
+ # On a big-endian architecture the high 32-bit word of a 64-bit field comes first.
31
+ @big_endian = Const::Endian.big?(arch)
28
32
  end
29
33
 
30
34
  # Run emulation!
35
+ #
36
+ # Executes the filter until it returns, then reports the final machine state.
37
+ # @yieldparam [{Symbol, Integer => Integer}] values
38
+ # If a block is given, it is invoked before each instruction with the current machine state.
31
39
  # @return [{Symbol, Integer => Integer}]
40
+ # The final state: +:ret+ is the action the filter returned, +:pc+ the line it returned from,
41
+ # +:a+ and +:x+ the registers, and Integer keys the scratch memory slots.
42
+ # @example
43
+ # insts = SeccompTools::Disasm.to_bpf(raw, :amd64).map(&:inst)
44
+ # SeccompTools::Emulator.new(insts, sys_nr: 0).run[:ret]
45
+ # #=> 2147418112 # SECCOMP_RET_ALLOW
32
46
  def run
33
47
  @values = { pc: 0, a: 0, x: 0 }
34
48
  loop do
35
- break if @values[:ret] # break when returned
36
-
37
49
  yield(@values) if block_given?
38
50
  inst = @instructions[pc]
39
51
  op, *args = inst.symbolize
@@ -46,6 +58,8 @@ module SeccompTools
46
58
  when :alu then alu(args[0], args[1]) # alu
47
59
  when :misc then misc(args[0]) # misc: txa/tax
48
60
  end
61
+ break if @values[:ret] # break when returned
62
+
49
63
  set(:pc, get(:pc) + 1) if %i[ld st alu misc].include?(op)
50
64
  end
51
65
  @values
@@ -101,6 +115,9 @@ module SeccompTools
101
115
  set(:a, (2**32) - get(:a))
102
116
  else
103
117
  src = get(:x) if src == :x
118
+ # Classic BPF aborts the whole program on division by zero.
119
+ return set(:ret, Const::BPF::ACTION[:KILL_THREAD]) if op == :/ && src.zero?
120
+
104
121
  set(:a, get(:a).__send__(op, src))
105
122
  end
106
123
  end
@@ -140,20 +157,28 @@ module SeccompTools
140
157
  end
141
158
 
142
159
  def data_of(index)
143
- raise IndexError, "Invalid index: #{index}" unless index.nobits?(3) && index.between?(0, 63)
160
+ data = Const::BPF::SeccompData
161
+ raise IndexError, "Invalid index: #{index}" unless index.nobits?(3) && index.between?(0, data::SIZE - 1)
144
162
 
145
163
  index /= 4
146
164
  case index
147
- when 0 then @sys_nr || undefined('sys_number')
148
- when 1 then @arch || undefined('arch')
149
- when 2 then (@ip & 0xffffffff) || undefined('instruction_pointer')
150
- when 3 then (@ip >> 32) || undefined('instruction_pointer')
165
+ when 0 then @sys_nr || undefined(data::NAMES[data::SYS_NUMBER])
166
+ when 1 then @arch || undefined(data::NAMES[data::ARCH])
167
+ when 2, 3 then word_of(@ip || undefined(data::NAMES[data::INSTRUCTION_POINTER]), index)
151
168
  else
152
169
  val = @args[(index - 4) / 2] || undefined("args[#{(index - 4) / 2}]")
153
- (val >> (index.even? ? 0 : 32)) & 0xffffffff
170
+ word_of(val, index)
154
171
  end
155
172
  end
156
173
 
174
+ # The 32-bit word of the 64-bit value +val+ that lives at word-index +index+ of
175
+ # +seccomp_data+: the high word comes second on little-endian architectures but first on
176
+ # big-endian ones (s390x).
177
+ def word_of(val, index)
178
+ hi = index.odd? ^ @big_endian
179
+ (val >> (hi ? 32 : 0)) & 0xffffffff
180
+ end
181
+
157
182
  def undefined(var)
158
183
  raise format("Undefined Variable\n\t%04d: %s <- `%s` is undefined", pc, @instructions[pc].decompile, var)
159
184
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SeccompTools
4
- # Base error class.
4
+ # Base class of all errors raised by this library.
5
+ #
6
+ # Rescue this class to catch every assembler error at once.
5
7
  class Error < StandardError
6
8
  end
7
9
 
@@ -9,7 +11,7 @@ module SeccompTools
9
11
  class UnrecognizedTokenError < Error
10
12
  end
11
13
 
12
- # Raised when a referred label is defined no where on compiling seccomp assembly.
14
+ # Raised when a referenced label is defined nowhere on compiling seccomp assembly.
13
15
  class UndefinedLabelError < Error
14
16
  end
15
17
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/const'
4
+ require 'seccomp-tools/explain/path_facts'
5
+ require 'seccomp-tools/explain/verdict'
6
+
7
+ module SeccompTools
8
+ class Explain
9
+ # What a walk of one filter amounts to: the reachable returns, each one's facts read once, and
10
+ # the split into the architectures the filter distinguishes.
11
+ #
12
+ # Everything that reads a walk needs the same groundwork - {Summary} to render a per-architecture
13
+ # policy, {Audit} to assess each architecture's reachable syscalls - so it is derived here once
14
+ # rather than in each of them.
15
+ class Analysis
16
+ # @param [Array<Symbolic::Executor::Leaf>] leaves
17
+ def initialize(leaves)
18
+ @leaves = leaves
19
+ @facts = Hash.new { |h, leaf| h[leaf] = PathFacts.new(leaf.path) }
20
+ end
21
+
22
+ # The {PathFacts} of +leaf+, computed once and shared across all consumers.
23
+ # @param [Symbolic::Executor::Leaf] leaf
24
+ # @return [PathFacts]
25
+ def facts(leaf)
26
+ @facts[leaf]
27
+ end
28
+
29
+ # The distinct architecture values (+AUDIT_ARCH_*+) the filter explicitly branches on.
30
+ # @return [Array<Integer>]
31
+ def arch_values
32
+ @arch_values ||= @leaves.filter_map { |l| facts(l).arch_eq }.uniq
33
+ end
34
+
35
+ # One entry per architecture section: its +AUDIT_ARCH+ value (+nil+ when the filter never
36
+ # branches on +arch+), the architecture symbol whose syscall names apply (+nil+ when the checked
37
+ # value is not one seccomp-tools knows), a display title, and the leaves reachable on it.
38
+ # @param [Symbol] declared_arch
39
+ # The architecture assumed when the filter itself does not branch on +arch+.
40
+ # @return [Array<Array(Integer?, Symbol?, Object, Array<Symbolic::Executor::Leaf>)>]
41
+ def sections(declared_arch)
42
+ vals = arch_values
43
+ return [[nil, declared_arch, declared_arch, @leaves]] if vals.empty?
44
+
45
+ vals.map do |v|
46
+ sym = Const::Audit.arch_symbol(v)
47
+ [v, sym, sym || format('0x%x (unknown)', v), @leaves.select { |l| facts(l).arch_consistent?(v) }]
48
+ end
49
+ end
50
+
51
+ # Leaves reachable when +arch+ is none of the explicitly-checked values.
52
+ # @return [Array<Symbolic::Executor::Leaf>]
53
+ def other_leaves
54
+ @leaves.reject { |l| facts(l).arch_eq }
55
+ end
56
+
57
+ # The catch-all action of +leaves+: the verdict of a leaf that matches no syscall, no range and
58
+ # no arguments (or the first leaf, if none is a pure catch-all), or +nil+ when +leaves+ is empty.
59
+ # @param [Array<Symbolic::Executor::Leaf>] leaves
60
+ # @return [String?]
61
+ def default_label(leaves)
62
+ catch_all = leaves.find { |l| facts(l).catch_all? }
63
+ (catch_all || leaves.first)&.then { |l| Verdict.label(l.ret) }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/const'
4
+ require 'seccomp-tools/symbolic/constraint'
5
+
6
+ module SeccompTools
7
+ class Explain
8
+ # The seccomp reading of one leaf's path condition: which syscall number it pins or bounds,
9
+ # which architecture value it pins, and which facts remain for the rule's +when+ clause. The
10
+ # path is immutable, so every query is derived once, eagerly.
11
+ class PathFacts
12
+ SYS = Const::BPF::SeccompData::SYS_NUMBER
13
+ ARCH = Const::BPF::SeccompData::ARCH
14
+ # Largest 32-bit value, the upper end of an unconstrained syscall-number range.
15
+ U32_MAX = 0xffffffff
16
+
17
+ # The syscall number the path pins with +==+, or +nil+.
18
+ # @return [Integer?]
19
+ attr_reader :sys_eq
20
+ # The architecture value the path pins with +==+, or +nil+.
21
+ # @return [Integer?]
22
+ attr_reader :arch_eq
23
+ # The +[lo, hi]+ range (inclusive; +hi+ is +nil+ when unbounded) all bound facts restrict the
24
+ # syscall number to, or +nil+ when there is no lower bound. An upper bound alone does not
25
+ # make a range rule: it is the complement of one (e.g. the +sys < 0x40000000+ side of an x32
26
+ # guard) and reads naturally as part of the default bucket.
27
+ # @return [Array(Integer, Integer?)?]
28
+ attr_reader :sys_range
29
+ # Constraints not already conveyed by the syscall-number / architecture presentation.
30
+ #
31
+ # Consumed (dropped): +==+, +!=+ and range facts on +sys_number+ - the named/ranged buckets
32
+ # and the "any other syscall" default wording express them; +==+/+!=+ facts on +arch+ - the
33
+ # per-architecture sections and the "any other" fall-through express them; and any non-+==+
34
+ # fact on a word that some +==+ on the same path already pins (it is then redundant - a
35
+ # contradicting combination would have been pruned as infeasible).
36
+ #
37
+ # Everything else is kept so a kernel-valid check is never silently dropped: bit-tests on an
38
+ # unpinned +sys_number+ (e.g. an odd/even dispatch), bit-tests or ranges on +arch+ (e.g.
39
+ # testing the +__AUDIT_ARCH_64BIT+ flag instead of pinning one value), and any comparison
40
+ # against a register rather than a constant.
41
+ # @return [Array<Symbolic::Constraint>]
42
+ attr_reader :residual
43
+
44
+ # @param [Array<Symbolic::Constraint>] path
45
+ def initialize(path)
46
+ @path = path
47
+ @sys_eq = eq(SYS)
48
+ @arch_eq = eq(ARCH)
49
+ @sys_range = compute_sys_range
50
+ @residual = compute_residual
51
+ end
52
+
53
+ # Is the path consistent with the architecture being +val+? Every constant arch fact is
54
+ # evaluated against +val+.
55
+ # @param [Integer] val
56
+ # @return [Boolean]
57
+ def arch_consistent?(val)
58
+ @path.all? do |c|
59
+ next true unless c.plain_data_fact?(ARCH)
60
+
61
+ Symbolic::Constraint.evaluate(val, c.op, c.rhs.val)
62
+ end
63
+ end
64
+
65
+ # Does the path match no syscall, no range, and no arguments - i.e. describe the filter's
66
+ # catch-all behavior?
67
+ # @return [Boolean]
68
+ def catch_all?
69
+ sys_eq.nil? && sys_range.nil? && residual.empty?
70
+ end
71
+
72
+ private
73
+
74
+ # The value of the single +data[offset] == k+ fact, if any.
75
+ def eq(offset)
76
+ @path.find { |c| c.plain_data_eq?(offset) }&.rhs&.val
77
+ end
78
+
79
+ def compute_sys_range
80
+ lo = nil
81
+ hi = nil
82
+ @path.each do |c|
83
+ next unless c.plain_data_fact?(SYS)
84
+
85
+ case c.op
86
+ when :> then lo = [lo || 0, c.rhs.val + 1].max
87
+ when :>= then lo = [lo || 0, c.rhs.val].max
88
+ when :< then hi = [hi || U32_MAX, c.rhs.val - 1].min
89
+ when :<= then hi = [hi || U32_MAX, c.rhs.val].min
90
+ end
91
+ end
92
+ lo && [lo, hi]
93
+ end
94
+
95
+ def compute_residual
96
+ pinned = @path.filter_map { |c| c.lhs.offset if c.plain_data_eq? }
97
+ @path.reject do |c|
98
+ next false unless c.plain_data_fact?
99
+
100
+ redundant = c.op != :== && pinned.include?(c.lhs.offset)
101
+ case c.lhs.offset
102
+ when SYS then redundant || !%i[set unset].include?(c.op)
103
+ when ARCH then redundant || %i[== !=].include?(c.op)
104
+ else redundant
105
+ end
106
+ end.uniq(&:key)
107
+ end
108
+ end
109
+ end
110
+ end