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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -0
- data/LICENSE +21 -0
- data/README.md +267 -46
- data/completions/_seccomp-tools +83 -0
- data/completions/seccomp-tools.bash +59 -0
- data/completions/seccomp-tools.fish +52 -0
- data/ext/ptrace/ptrace.c +2 -2
- data/lib/seccomp-tools/asm/asm.rb +9 -4
- data/lib/seccomp-tools/asm/compiler.rb +39 -9
- data/lib/seccomp-tools/asm/sasm.tab.rb +31 -21
- data/lib/seccomp-tools/asm/sasm.y +15 -7
- data/lib/seccomp-tools/asm/scalar.rb +50 -7
- data/lib/seccomp-tools/asm/scanner.rb +40 -8
- data/lib/seccomp-tools/asm/statement.rb +14 -5
- data/lib/seccomp-tools/asm/token.rb +19 -1
- data/lib/seccomp-tools/audit/catalog.rb +66 -0
- data/lib/seccomp-tools/audit/checks/arch_unchecked.rb +41 -0
- data/lib/seccomp-tools/audit/checks/dangerous_allow.rb +34 -0
- data/lib/seccomp-tools/audit/checks/orw_chain.rb +41 -0
- data/lib/seccomp-tools/audit/checks/permissive_default.rb +29 -0
- data/lib/seccomp-tools/audit/checks/syscall_alt_gap.rb +44 -0
- data/lib/seccomp-tools/audit/checks/x32_guard.rb +46 -0
- data/lib/seccomp-tools/audit/checks.rb +42 -0
- data/lib/seccomp-tools/audit/finding.rb +25 -0
- data/lib/seccomp-tools/audit/policy.rb +126 -0
- data/lib/seccomp-tools/audit/report.rb +98 -0
- data/lib/seccomp-tools/audit.rb +48 -0
- data/lib/seccomp-tools/bpf.rb +27 -17
- data/lib/seccomp-tools/cli/asm.rb +7 -3
- data/lib/seccomp-tools/cli/audit.rb +86 -0
- data/lib/seccomp-tools/cli/base.rb +51 -8
- data/lib/seccomp-tools/cli/cli.rb +10 -6
- data/lib/seccomp-tools/cli/completion.rb +40 -0
- data/lib/seccomp-tools/cli/disasm.rb +10 -6
- data/lib/seccomp-tools/cli/dump.rb +37 -52
- data/lib/seccomp-tools/cli/dumpable.rb +79 -0
- data/lib/seccomp-tools/cli/emu.rb +32 -9
- data/lib/seccomp-tools/cli/explain.rb +51 -0
- data/lib/seccomp-tools/cli/filter_input.rb +130 -0
- data/lib/seccomp-tools/const.rb +98 -15
- data/lib/seccomp-tools/consts/sys_nr/amd64.rb +1 -1
- data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
- data/lib/seccomp-tools/disasm/disasm.rb +31 -13
- data/lib/seccomp-tools/dumper.rb +67 -35
- data/lib/seccomp-tools/emulator.rb +41 -16
- data/lib/seccomp-tools/error.rb +4 -2
- data/lib/seccomp-tools/explain/analysis.rb +67 -0
- data/lib/seccomp-tools/explain/path_facts.rb +110 -0
- data/lib/seccomp-tools/explain/qword.rb +204 -0
- data/lib/seccomp-tools/explain/renderer.rb +128 -0
- data/lib/seccomp-tools/explain/summary.rb +218 -0
- data/lib/seccomp-tools/explain/verdict.rb +44 -0
- data/lib/seccomp-tools/explain.rb +38 -0
- data/lib/seccomp-tools/instruction/alu.rb +14 -9
- data/lib/seccomp-tools/instruction/base.rb +39 -10
- data/lib/seccomp-tools/instruction/jmp.rb +50 -23
- data/lib/seccomp-tools/instruction/ld.rb +46 -21
- data/lib/seccomp-tools/instruction/ldx.rb +4 -3
- data/lib/seccomp-tools/instruction/misc.rb +11 -9
- data/lib/seccomp-tools/instruction/ret.rb +12 -6
- data/lib/seccomp-tools/instruction/st.rb +15 -6
- data/lib/seccomp-tools/instruction/stx.rb +4 -3
- data/lib/seccomp-tools/logger.rb +15 -2
- data/lib/seccomp-tools/symbolic/constraint.rb +80 -0
- data/lib/seccomp-tools/symbolic/executor.rb +210 -0
- data/lib/seccomp-tools/symbolic/expr.rb +185 -0
- data/lib/seccomp-tools/symbolic/state.rb +82 -0
- data/lib/seccomp-tools/syscall.rb +74 -22
- data/lib/seccomp-tools/util.rb +70 -11
- data/lib/seccomp-tools/version.rb +1 -1
- data/lib/seccomp-tools.rb +10 -1
- metadata +83 -11
- 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
|
-
#
|
|
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
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
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(
|
|
35
|
-
# #=> [[11,
|
|
36
|
-
def branch(
|
|
47
|
+
# inst.branch(state)
|
|
48
|
+
# #=> [[11, state]]
|
|
49
|
+
def branch(_state); raise NotImplementedError
|
|
37
50
|
end
|
|
38
51
|
|
|
39
|
-
#
|
|
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
|
|
61
|
+
def symbolize; raise NotImplementedError
|
|
46
62
|
end
|
|
47
63
|
|
|
48
64
|
private
|
|
49
65
|
|
|
50
|
-
|
|
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
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
#
|
|
61
|
-
a =
|
|
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.
|
|
101
|
+
return k.to_s unless a.plain_data?
|
|
66
102
|
|
|
67
103
|
hex = "0x#{k.to_s(16)}"
|
|
68
|
-
case a.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
28
|
-
# @return [
|
|
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 [
|
|
35
|
-
# Current
|
|
36
|
-
# @return [Array<(Integer,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
69
|
-
when
|
|
70
|
-
|
|
71
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
name = a == arch ? sys : "#{a}.#{sys}"
|
|
116
|
+
comment = "# #{name}(#{args.join(', ')})"
|
|
92
117
|
arg_name = Util.colorize(args[idx / 2], t: :args)
|
|
93
|
-
"#{
|
|
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
|
-
#
|
|
10
|
-
# @return [
|
|
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 [
|
|
25
|
-
# Current
|
|
26
|
-
# @return [Array<(Integer,
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
when :tax then
|
|
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
|
-
#
|
|
22
|
-
#
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
#
|
|
10
|
-
# @return [
|
|
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
|
data/lib/seccomp-tools/logger.rb
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|