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
@@ -3,9 +3,15 @@
3
3
  require 'seccomp-tools/const'
4
4
 
5
5
  module SeccompTools
6
- # For instructions' class.
6
+ # Classes of BPF instructions, one per opcode class.
7
+ #
8
+ # Each instruction wraps a {SeccompTools::BPF} and knows how to render itself as assembly
9
+ # (+decompile+), as tokens ({Base#symbolize}), and how it moves the disassembler's state
10
+ # forward ({Base#branch}).
7
11
  module Instruction
8
12
  # Base class of instructions.
13
+ #
14
+ # Subclasses must implement {#branch} and {#symbolize}.
9
15
  class Base
10
16
  include SeccompTools::Const::BPF
11
17
 
@@ -25,29 +31,52 @@ module SeccompTools
25
31
  end
26
32
 
27
33
  # Returns the possible branches after executing this instruction.
28
- # @param [Context] _context
29
- # Current context.
30
- # @return [Array<(Integer, Context)>]
34
+ #
35
+ # Each branch is the line number to be executed next, paired with the {Symbolic::State} that
36
+ # reaching that line implies. Non-jump instructions have exactly one branch, the following
37
+ # line.
38
+ # @param [Symbolic::State] _state
39
+ # Current state.
40
+ # @return [Array<(Integer, Symbolic::State)>]
41
+ # Pairs of the next line number and the state at that line.
42
+ # @raise [NotImplementedError]
43
+ # Always, subclasses must override this method.
31
44
  # @example
32
45
  # # For ALU, LD, LDX, ST, STX
33
46
  # inst.line #=> 10
34
- # inst.branch(ctx)
35
- # #=> [[11, ctx]]
36
- def branch(_context); raise NotImplmentedError
47
+ # inst.branch(state)
48
+ # #=> [[11, state]]
49
+ def branch(_state); raise NotImplementedError
37
50
  end
38
51
 
39
- # Return tokens stand for this instruction.
52
+ # Returns tokens that represent this instruction.
40
53
  # @return [Array<Symbol, Integer>]
54
+ # The instruction as a tuple, the exact shape depends on the instruction class.
55
+ # @raise [NotImplementedError]
56
+ # Always, subclasses must override this method.
41
57
  # @example
42
58
  # ret_a.symbolize #=> [:ret, :a]
43
59
  # ret_k.symbolize #=> [:ret, 0x7fff0000]
44
60
  # jeq.symbolize #=> [:cmp, :==, 0, 0, 1]
45
- def symbolize; raise NotImplmentedError
61
+ def symbolize; raise NotImplementedError
46
62
  end
47
63
 
48
64
  private
49
65
 
50
- %i(code jt jf k arch line contexts show_arg_infer?).each do |sym|
66
+ # The architecture this instruction's operands should be read as: the one the filter has
67
+ # branched on (+arch == AUDIT_ARCH_*+) when the reaching states pin a single value, else +nil+
68
+ # so callers fall back to the declared {#arch}. Lets syscall/argument names stay correct even
69
+ # when the filter is disassembled under a different +--arch+ than it targets.
70
+ # @return [Symbol?]
71
+ def infer_arch
72
+ arches = states.map { |st| st.pinned(Const::BPF::SeccompData::ARCH) }.uniq
73
+ return nil unless arches.size == 1 && !arches.first.nil?
74
+
75
+ Const::Audit.arch_symbol(arches.first)
76
+ end
77
+
78
+ # Delegate the accessors of the wrapped {SeccompTools::BPF} so subclasses can use them directly.
79
+ %i(code jt jf k arch line states show_arg_infer?).each do |sym|
51
80
  define_method(sym) do
52
81
  @bpf.__send__(sym)
53
82
  end
@@ -5,9 +5,13 @@ require 'seccomp-tools/instruction/base'
5
5
 
6
6
  module SeccompTools
7
7
  module Instruction
8
- # Instruction jmp.
8
+ # Instruction jmp, an unconditional jump or a comparison of A against X or an immediate.
9
+ #
10
+ # Jumps are always forward, +jt+ and +jf+ being offsets relative to the following line.
9
11
  class JMP < Base
10
12
  # Decompile instruction.
13
+ # @return [String]
14
+ # The jump as assembly, e.g. +"if (A == read) goto 0003"+.
11
15
  def decompile
12
16
  return goto(k) if jop == :none
13
17
  # if jt == 0 && jf == 0 => no-op # should not happen
@@ -23,6 +27,8 @@ module SeccompTools
23
27
 
24
28
  # See {Instruction::Base#symbolize}.
25
29
  # @return [[:cmp, Symbol, (:x, Integer), Integer, Integer], [:jmp, Integer]]
30
+ # +[:jmp, offset]+ for an unconditional jump, otherwise +[:cmp, operator, right operand,
31
+ # jt, jf]+.
26
32
  def symbolize
27
33
  return [:jmp, k] if jop == :none
28
34
 
@@ -30,19 +36,49 @@ module SeccompTools
30
36
  end
31
37
 
32
38
  # See {Base#branch}.
33
- # @param [Context] context
34
- # Current context.
35
- # @return [Array<(Integer, Context)>]
36
- def branch(context)
37
- return [[at(k), context]] if jop == :none
38
- return [[at(jt), context]] if jt == jf
39
- return [[at(jt), context.dup.eql!(src)], [at(jf), context]] if jop == :==
40
-
41
- [[at(jt), context], [at(jf), context]]
39
+ #
40
+ # Unlike the other instructions, a conditional jump has two possible successors. On the
41
+ # taken branch of an equality test the state is narrowed, recording that A is known to equal
42
+ # the compared value.
43
+ # @param [Symbolic::State] state
44
+ # Current state.
45
+ # @return [Array<(Integer, Symbolic::State)>]
46
+ # One pair for an unconditional jump, two otherwise.
47
+ # @example
48
+ # # 0000: if (A == 0) goto 0002 else goto 0003
49
+ # jeq.branch(state) #=> [[2, narrowed_state], [3, state]]
50
+ def branch(state)
51
+ return [[at(k), state]] if jop == :none
52
+ return [[at(jt), state]] if jt == jf
53
+ return [[at(jt), narrow(state)], [at(jf), state]] if jop == :==
54
+
55
+ [[at(jt), state], [at(jf), state]]
42
56
  end
43
57
 
44
58
  private
45
59
 
60
+ # The taken branch of +A == src+ learns +A == value+. When A holds a plain data word and the
61
+ # compared side is (or resolves to) a constant, that pins the word - recorded as a
62
+ # {Symbolic::Constraint} on the path. Anything else leaves the state unchanged.
63
+ # @param [Symbolic::State] state
64
+ # @return [Symbolic::State]
65
+ def narrow(state)
66
+ return state unless state.a.plain_data?
67
+
68
+ rhs = src == :x ? resolve(state, state.x) : Symbolic::Expr.imm(k)
69
+ return state unless rhs.imm?
70
+
71
+ state.with(path: state.path + [Symbolic::Constraint.new(state.a, :==, rhs)])
72
+ end
73
+
74
+ # Replaces a data-word +expr+ with the constant it is pinned to on +state+'s path, if any;
75
+ # otherwise returns it unchanged.
76
+ def resolve(state, expr)
77
+ return expr unless expr.plain_data?
78
+
79
+ state.path.find { |c| c.plain_data_eq?(expr.offset) }&.rhs || expr
80
+ end
81
+
46
82
  def jop
47
83
  case Const::BPF::JMP.invert[code & 0x70]
48
84
  when :ja then :none
@@ -57,15 +93,15 @@ module SeccompTools
57
93
  def src_str
58
94
  return 'X' if src == :x
59
95
 
60
- # if A in all contexts are same
61
- a = contexts.map(&:a).uniq
96
+ # only when A holds the same data word across every reaching state
97
+ a = states.map(&:a).uniq
62
98
  return k.to_s if a.size != 1
63
99
 
64
100
  a = a[0]
65
- return k.to_s unless a.data?
101
+ return k.to_s unless a.plain_data?
66
102
 
67
103
  hex = "0x#{k.to_s(16)}"
68
- case a.val
104
+ case a.offset
69
105
  # interpret as syscalls only if it's an equality test
70
106
  when 0 then Util.colorize(jop == :== ? sysname_by_k || hex : hex, t: :syscall)
71
107
  when 4 then Util.colorize(Const::Audit::ARCH.invert[k] || hex, t: :arch)
@@ -81,15 +117,6 @@ module SeccompTools
81
117
  a == arch ? name : "#{a}.#{name}"
82
118
  end
83
119
 
84
- # Infers the architecture from context.
85
- # @return [Symbol?]
86
- def infer_arch
87
- arches = contexts.map { |ctx| ctx.known_data[4] }.uniq
88
- return nil unless arches.size == 1 && !arches.first.nil?
89
-
90
- Const::Audit::ARCH_NAME.invert[Const::Audit::ARCH.invert[arches.first]]
91
- end
92
-
93
120
  def src
94
121
  SRC.invert[code & 8] == :x ? :x : k
95
122
  end
@@ -6,9 +6,14 @@ require 'seccomp-tools/util'
6
6
 
7
7
  module SeccompTools
8
8
  module Instruction
9
- # Instruction ld.
9
+ # Instruction ld, loads a value into the accumulator register A.
10
+ #
11
+ # The value can be an immediate, a word of +struct seccomp_data+, or a slot of the scratch
12
+ # memory. {LDX} inherits from this class and targets the X register instead.
10
13
  class LD < Base
11
14
  # Decompile instruction.
15
+ # @return [String]
16
+ # The assignment as assembly, e.g. +"A = sys_number"+.
12
17
  def decompile
13
18
  ret = "#{reg} = "
14
19
  _, _reg, type = symbolize
@@ -18,26 +23,35 @@ module SeccompTools
18
23
  ret + seccomp_data_str
19
24
  end
20
25
 
21
- # @return [void]
26
+ # See {Instruction::Base#symbolize}.
27
+ # @return [[:ld, Symbol, {rel: Symbol, val: Integer}]]
28
+ # The target register and the value being loaded, whose +:rel+ is one of +:immi+, +:mem+
29
+ # or +:data+.
22
30
  def symbolize
23
31
  type = load_val
24
32
  [:ld, reg.downcase.to_sym, type]
25
33
  end
26
34
 
27
- # Accumulator register.
28
- # @return ['A']
35
+ # Name of the register being loaded into.
36
+ # @return [String]
37
+ # The accumulator register, +"A"+.
29
38
  def reg
30
39
  'A'
31
40
  end
32
41
 
33
42
  # See {Base#branch}.
34
- # @param [Context] context
35
- # Current context.
36
- # @return [Array<(Integer, Context)>]
37
- def branch(context)
38
- ctx = context.dup
39
- ctx.load(reg, **load_val)
40
- [[line + 1, ctx]]
43
+ # @param [Symbolic::State] state
44
+ # Current state.
45
+ # @return [Array<(Integer, Symbolic::State)>]
46
+ # Always the next line, with the loaded value recorded in the register.
47
+ def branch(state)
48
+ v = load_val
49
+ val = case v[:rel]
50
+ when :immi then Symbolic::Expr.imm(v[:val])
51
+ when :mem then state.mem[v[:val]]
52
+ when :data then Symbolic::Expr.data(v[:val])
53
+ end
54
+ [[line + 1, reg == 'X' ? state.with(x: val) : state.with(a: val)]]
41
55
  end
42
56
 
43
57
  private
@@ -64,33 +78,44 @@ module SeccompTools
64
78
  # __u64 args[6];
65
79
  # };
66
80
  def seccomp_data_str
81
+ data = Const::BPF::SeccompData
67
82
  case k
68
- when 0 then 'sys_number'
69
- when 4 then 'arch'
70
- when 8 then 'instruction_pointer'
71
- when 12 then 'instruction_pointer >> 32'
83
+ when data::SYS_NUMBER, data::ARCH then data::NAMES[k]
84
+ when data::INSTRUCTION_POINTER, data::INSTRUCTION_POINTER + 4
85
+ ip = data::NAMES[data::INSTRUCTION_POINTER]
86
+ hi_word?(k) ? "#{ip} >> 32" : ip
72
87
  else
73
- idx = Array.new(12) { |i| i * 4 + 16 }.index(k)
88
+ idx = (data::ARGS...data::SIZE).step(4).to_a.index(k)
74
89
  return 'INVALID' if idx.nil?
75
90
 
76
91
  args_name(idx)
77
92
  end
78
93
  end
79
94
 
95
+ # Is the 32-bit word at byte offset +k+ the high half of its 64-bit +seccomp_data+ field?
96
+ # The high word comes second on little-endian architectures but first on big-endian ones
97
+ # (s390x); see +arch_arg_offset_hi+ in libseccomp.
98
+ def hi_word?(k)
99
+ (k % 8 == 4) ^ Const::Endian.big?(arch)
100
+ end
101
+
80
102
  def args_name(idx)
81
- default = idx.even? ? "args[#{idx / 2}]" : "args[#{idx / 2}] >> 32"
103
+ hi = hi_word?((idx * 4) + Const::BPF::SeccompData::ARGS)
104
+ default = hi ? "args[#{idx / 2}] >> 32" : "args[#{idx / 2}]"
82
105
  return default unless show_arg_infer?
83
106
 
84
- sys_nrs = contexts.map { |ctx| ctx.known_data[0] }.uniq
107
+ sys_nrs = states.map { |st| st.pinned(Const::BPF::SeccompData::SYS_NUMBER) }.uniq
85
108
  return default if sys_nrs.size != 1 || sys_nrs.first.nil?
86
109
 
87
- sys = Const::Syscall.const_get(arch.upcase.to_sym).invert[sys_nrs.first]
110
+ a = infer_arch || arch
111
+ sys = Const::Syscall.const_get(a.upcase.to_sym).invert[sys_nrs.first]
88
112
  args = Const::SYS_ARG[sys]
89
113
  return default if args.nil? || args[idx / 2].nil? # function prototype doesn't have that argument
90
114
 
91
- comment = "# #{sys}(#{args.join(', ')})"
115
+ name = a == arch ? sys : "#{a}.#{sys}"
116
+ comment = "# #{name}(#{args.join(', ')})"
92
117
  arg_name = Util.colorize(args[idx / 2], t: :args)
93
- "#{idx.even? ? arg_name : "#{arg_name} >> 32"} #{Util.colorize(comment, t: :gray)}"
118
+ "#{hi ? "#{arg_name} >> 32" : arg_name} #{Util.colorize(comment, t: :gray)}"
94
119
  end
95
120
  end
96
121
  end
@@ -4,10 +4,11 @@ require 'seccomp-tools/instruction/ld'
4
4
 
5
5
  module SeccompTools
6
6
  module Instruction
7
- # Instruction ldx.
7
+ # Instruction ldx, same as {LD} but loads into the index register X.
8
8
  class LDX < LD
9
- # Index register.
10
- # @return ['X']
9
+ # Name of the register being loaded into.
10
+ # @return [String]
11
+ # The index register, +"X"+.
11
12
  def reg
12
13
  'X'
13
14
  end
@@ -4,9 +4,11 @@ require 'seccomp-tools/instruction/base'
4
4
 
5
5
  module SeccompTools
6
6
  module Instruction
7
- # Instruction misc.
7
+ # Instruction misc, copies a value between the A and X registers.
8
8
  class MISC < Base
9
9
  # Decompile instruction.
10
+ # @return [String]
11
+ # Either +"A = X"+ or +"X = A"+.
10
12
  def decompile
11
13
  case op
12
14
  when :txa then 'A = X'
@@ -16,21 +18,21 @@ module SeccompTools
16
18
 
17
19
  # See {Instruction::Base#symbolize}.
18
20
  # @return [[:misc, (:tax, :txa)]]
21
+ # +:tax+ copies A into X, +:txa+ copies X into A.
19
22
  def symbolize
20
23
  [:misc, op]
21
24
  end
22
25
 
23
26
  # See {Base#branch}.
24
- # @param [Context] context
25
- # Current context.
26
- # @return [Array<(Integer, Context)>]
27
- def branch(context)
28
- ctx = context.dup
27
+ # @param [Symbolic::State] state
28
+ # Current state.
29
+ # @return [Array<(Integer, Symbolic::State)>]
30
+ # Always the next line, with the copied value recorded in the register.
31
+ def branch(state)
29
32
  case op
30
- when :txa then ctx['A'] = ctx['X']
31
- when :tax then ctx['X'] = ctx['A']
33
+ when :txa then [[line + 1, state.with(a: state.x)]]
34
+ when :tax then [[line + 1, state.with(x: state.a)]]
32
35
  end
33
- [[line + 1, ctx]]
34
36
  end
35
37
 
36
38
  private
@@ -4,22 +4,29 @@ require 'seccomp-tools/instruction/base'
4
4
 
5
5
  module SeccompTools
6
6
  module Instruction
7
- # Instruction ret.
7
+ # Instruction ret, terminates the filter with an action such as +ALLOW+ or +KILL+.
8
+ #
9
+ # The action comes from either the accumulator register A or the immediate +k+.
8
10
  class RET < Base
9
11
  # Decompile instruction.
12
+ # @return [String]
13
+ # The return as assembly, e.g. +"return ERRNO(1)"+.
10
14
  def decompile
11
15
  "return #{ret_str}"
12
16
  end
13
17
 
14
18
  # See {Instruction::Base#symbolize}.
15
19
  # @return [[:ret, (:a, Integer)]]
20
+ # +:a+ when the action is taken from the A register, otherwise the immediate action value.
16
21
  def symbolize
17
22
  [:ret, code & 0x18 == SRC[:a] ? :a : k]
18
23
  end
19
24
 
20
25
  # See {Base#branch}.
21
- # @return [[]]
22
- # Always return an empty array.
26
+ #
27
+ # Accepts and ignores any arguments, the state is irrelevant here.
28
+ # @return [Array]
29
+ # Always an empty array, a filter stops executing at a return.
23
30
  def branch(*)
24
31
  []
25
32
  end
@@ -30,9 +37,8 @@ module SeccompTools
30
37
  _, type = symbolize
31
38
  return 'A' if type == :a
32
39
 
33
- str = ACTION.invert[type & SECCOMP_RET_ACTION_FULL].to_s
34
- str << "(#{type & SECCOMP_RET_DATA})" if str == 'ERRNO'
35
- str
40
+ # fall back to the raw value (still re-assemblable) for an action the kernel does not define
41
+ Const::BPF.action_label(type) || "0x#{type.to_s(16)}"
36
42
  end
37
43
  end
38
44
  end
@@ -4,24 +4,33 @@ require 'seccomp-tools/instruction/ld'
4
4
 
5
5
  module SeccompTools
6
6
  module Instruction
7
- # Instruction st.
7
+ # Instruction st, stores the accumulator register A into a slot of the scratch memory.
8
+ #
9
+ # {STX} inherits from this class and stores the X register instead.
8
10
  class ST < LD
9
11
  # Decompile instruction.
12
+ # @return [String]
13
+ # The store as assembly, e.g. +"mem[0] = A"+.
10
14
  def decompile
11
15
  "mem[#{k}] = #{reg}"
12
16
  end
13
17
 
14
18
  # See {Instruction::Base#symbolize}.
15
19
  # @return [[:st, (:a, :x), Integer]]
20
+ # The source register and the index of the target memory slot.
16
21
  def symbolize
17
22
  [:st, reg.downcase.to_sym, k]
18
23
  end
19
24
 
20
- # @return [Array<(Integer, Context)>]
21
- def branch(context)
22
- ctx = context.dup
23
- ctx.store(k, reg)
24
- [[line + 1, ctx]]
25
+ # See {Base#branch}.
26
+ # @param [Symbolic::State] state
27
+ # Current state.
28
+ # @return [Array<(Integer, Symbolic::State)>]
29
+ # Always the next line, with the register's value recorded in the scratch slot.
30
+ def branch(state)
31
+ mem = state.mem.dup
32
+ mem[k] = reg == 'X' ? state.x : state.a
33
+ [[line + 1, state.with(mem:)]]
25
34
  end
26
35
  end
27
36
  end
@@ -4,10 +4,11 @@ require 'seccomp-tools/instruction/st'
4
4
 
5
5
  module SeccompTools
6
6
  module Instruction
7
- # Instruction stx.
7
+ # Instruction stx, same as {ST} but stores the index register X.
8
8
  class STX < ST
9
- # Index register.
10
- # @return ['X']
9
+ # Name of the register being stored.
10
+ # @return [String]
11
+ # The index register, +"X"+.
11
12
  def reg
12
13
  'X'
13
14
  end
@@ -13,6 +13,9 @@ module SeccompTools
13
13
 
14
14
  # Returns a +::Logger+ object for internal logging.
15
15
  #
16
+ # The returned logger writes to +$stdout+ with a formatter that prefixes each message with its
17
+ # colorized severity and indents continuation lines to align with the first.
18
+ #
16
19
  # @return [::Logger]
17
20
  def logger
18
21
  ::Logger.new($stdout).tap do |log|
@@ -24,14 +27,24 @@ module SeccompTools
24
27
  str.strip.empty? ? str : prep + str
25
28
  end
26
29
  color = severity.downcase.to_sym
27
- msg = +"[#{SeccompTools::Util.colorize(severity, t: color)}] #{message.join}"
30
+ msg = "[#{SeccompTools::Util.colorize(severity, t: color)}] #{message.join}"
28
31
  msg << "\n" unless msg.end_with?("\n")
29
32
  msg
30
33
  end
31
34
  end
32
35
  end
33
36
 
34
- %i[error].each do |sym|
37
+ # @!method error(msg)
38
+ # Logs +msg+ at the +error+ severity.
39
+ # @param [String] msg
40
+ # The message to be logged.
41
+ # @return [true]
42
+ # @!method warn(msg)
43
+ # Logs +msg+ at the +warn+ severity.
44
+ # @param [String] msg
45
+ # The message to be logged.
46
+ # @return [true]
47
+ %i[error warn].each do |sym|
35
48
  define_method(sym) do |msg|
36
49
  logger.__send__(sym, msg)
37
50
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/symbolic/expr'
4
+
5
+ module SeccompTools
6
+ module Symbolic
7
+ # One fact that must hold for the executor to be on a particular path, e.g. +A == 1+ or
8
+ # +data[16] & 0xffff != 0+. Every conditional jump adds one {Constraint} to each branch it
9
+ # takes; the accumulated list is the "path condition" carried in {State#path} and reported on a
10
+ # {Executor::Leaf}.
11
+ #
12
+ # A constraint keeps a lone constant on the right: BPF always compares the A register against X
13
+ # or +k+, but A itself may hold the constant (e.g. +A = 5+ compared against a data word +tax+'d
14
+ # into X earlier), and every consumer reads "expression op constant". So +Constraint.new(5, :>,
15
+ # word)+ normalizes to +word < 5+ at construction - the two spellings are one value.
16
+ class Constraint
17
+ # The same comparison with its two sides swapped: +5 > x+ is +x < 5+. The bit tests are
18
+ # symmetric (+&+ commutes).
19
+ MIRROR = {
20
+ :== => :==, :!= => :!=, :> => :<, :>= => :<=, :< => :>, :<= => :>=, set: :set, unset: :unset
21
+ }.freeze
22
+
23
+ # Applies one of the constraint operators to concrete operands: the +jset+ bit tests
24
+ # (+:set+/+:unset+) and the Integer comparisons. Stateless - used to evaluate a pinned
25
+ # constraint or a candidate value, without building a {Constraint}.
26
+ # @param [Integer] value
27
+ # @param [Symbol] op
28
+ # @param [Integer] k
29
+ # @return [Boolean]
30
+ def self.evaluate(value, op, k)
31
+ case op
32
+ when :set then !value.nobits?(k)
33
+ when :unset then value.nobits?(k)
34
+ else value.public_send(op, k) # the comparisons are all Integer methods
35
+ end
36
+ end
37
+
38
+ # @return [Expr] The left-hand side (what is being tested).
39
+ attr_reader :lhs
40
+ # @return [Symbol] The comparison, one of +:==, :!=, :>, :>=, :<, :<=, :set, :unset+.
41
+ # +:set+/+:unset+ mean "some/none of these bits are set" (from a +jset+ test).
42
+ attr_reader :op
43
+ # @return [Expr] The right-hand side (what it is compared against).
44
+ attr_reader :rhs
45
+
46
+ # @param [Expr] lhs
47
+ # @param [Symbol] op
48
+ # @param [Expr] rhs
49
+ def initialize(lhs, op, rhs)
50
+ lhs, op, rhs = rhs, MIRROR[op], lhs if lhs.imm? && !rhs.imm? # keep the constant on the right
51
+ @lhs = lhs
52
+ @op = op
53
+ @rhs = rhs
54
+ end
55
+
56
+ # Is this a fact about one plain data word compared against a constant - about the word at
57
+ # +offset+, when given? These are the facts rule-based consumers can reason about, e.g. the
58
+ # feasibility pruning in +Symbolic::Executor+.
59
+ # @param [Integer?] offset
60
+ # @return [Boolean]
61
+ def plain_data_fact?(offset = nil)
62
+ lhs.plain_data? && rhs.imm? && (offset.nil? || lhs.offset == offset)
63
+ end
64
+
65
+ # Is this a +word == constant+ fact - about the word at +offset+, when given?
66
+ # @param [Integer?] offset
67
+ # @return [Boolean]
68
+ def plain_data_eq?(offset = nil)
69
+ op == :== && plain_data_fact?(offset)
70
+ end
71
+
72
+ # A string that uniquely identifies this constraint, for both equality and hashing; the
73
+ # +expr op expr+ counterpart of {Expr#key}, injective for the same reason.
74
+ # @return [String]
75
+ def key
76
+ @key ||= "#{lhs.key}#{op}#{rhs.key}"
77
+ end
78
+ end
79
+ end
80
+ end