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
data/lib/seccomp-tools/dumper.rb
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'os'
|
|
4
|
+
require 'timeout'
|
|
4
5
|
|
|
5
6
|
require 'seccomp-tools/logger'
|
|
6
7
|
require 'seccomp-tools/ptrace' if OS.linux?
|
|
7
8
|
require 'seccomp-tools/syscall'
|
|
9
|
+
require 'seccomp-tools/util'
|
|
8
10
|
|
|
9
11
|
module SeccompTools
|
|
10
12
|
# Dump seccomp-bpf using ptrace of binary.
|
|
11
13
|
module Dumper
|
|
12
14
|
# Whether the dumper is supported.
|
|
13
|
-
# Dumper works based on ptrace, so we need the platform be Linux.
|
|
15
|
+
# Dumper works based on ptrace, so we need the platform to be Linux.
|
|
14
16
|
SUPPORTED = OS.linux?
|
|
15
17
|
|
|
16
18
|
module_function
|
|
17
19
|
|
|
18
20
|
# Main bpf dump function.
|
|
19
|
-
#
|
|
21
|
+
# Yields seccomp bpf whenever a +prctl(SET_SECCOMP)+ call is found.
|
|
20
22
|
#
|
|
21
23
|
# @param [Array<String>] args
|
|
22
|
-
# The
|
|
24
|
+
# The command to be executed, i.e. the target executable followed by its arguments.
|
|
23
25
|
# @param [Integer] limit
|
|
24
26
|
# By default, +dump+ will only dump the first +SET_SECCOMP+ call.
|
|
25
27
|
# Set +limit+ to the number of calling +prctl(SET_SECCOMP)+ then the child process will be killed when number of
|
|
26
28
|
# calling +prctl+ reaches +limit+.
|
|
27
29
|
#
|
|
28
30
|
# Negative number for unlimited.
|
|
31
|
+
# @param [Float?] timeout
|
|
32
|
+
# Number of seconds to wait for the target process. When the timeout is reached, the target
|
|
33
|
+
# process is killed and the filters dumped so far are returned. +nil+ for no timeout.
|
|
29
34
|
# @yieldparam [String] bpf
|
|
30
35
|
# Seccomp bpf in raw bytes.
|
|
31
|
-
# @yieldparam [Symbol] arch
|
|
32
|
-
# Architecture of the target process.
|
|
36
|
+
# @yieldparam [Symbol?] arch
|
|
37
|
+
# Architecture of the target process, +nil+ when it cannot be determined.
|
|
38
|
+
# See {SeccompTools::Util.process_arch}.
|
|
33
39
|
# @return [Array<Object>, Array<String>]
|
|
34
|
-
#
|
|
40
|
+
# One entry per dumped filter: the block's return values when a block is given, otherwise the
|
|
41
|
+
# raw bytes. Empty on a non-Linux platform, where dumping is unsupported.
|
|
35
42
|
# @example
|
|
36
43
|
# dump('ls', '-l', '-a')
|
|
37
44
|
# #=> []
|
|
38
45
|
# dump('spec/binary/twctf-2016-diary') { |c| c[0, 10] }
|
|
39
46
|
# #=> [" \x00\x00\x00\x00\x00\x00\x00\x15\x00"]
|
|
40
|
-
|
|
41
|
-
# +timeout+ option.
|
|
42
|
-
def dump(*args, limit: 1, &block)
|
|
47
|
+
def dump(*args, limit: 1, timeout: nil, &block)
|
|
43
48
|
return [] unless SUPPORTED
|
|
44
49
|
|
|
45
50
|
pid = fork { handle_child(*args) }
|
|
46
|
-
Handler.new(pid).handle(limit, &block)
|
|
51
|
+
Handler.new(pid).handle(limit, timeout: timeout, &block)
|
|
47
52
|
end
|
|
48
53
|
|
|
49
|
-
#
|
|
54
|
+
# Traces a forked child, single-stepping it through its syscalls and capturing the seccomp
|
|
55
|
+
# filters it installs.
|
|
50
56
|
class Handler
|
|
51
57
|
# Instantiate a {Handler} object.
|
|
52
58
|
# @param [Integer] pid
|
|
53
59
|
# The process id after fork.
|
|
54
60
|
def initialize(pid)
|
|
61
|
+
@pids = [pid]
|
|
55
62
|
Process.waitpid(pid)
|
|
56
63
|
opt = Ptrace::O_TRACESYSGOOD | Ptrace::O_TRACECLONE | Ptrace::O_TRACEFORK | Ptrace::O_TRACEVFORK
|
|
57
64
|
Ptrace.setoptions(pid, 0, opt)
|
|
@@ -62,46 +69,60 @@ module SeccompTools
|
|
|
62
69
|
#
|
|
63
70
|
# @param [Integer] limit
|
|
64
71
|
# Child will be killed when number of calling +prctl(SET_SECCOMP)+ reaches +limit+.
|
|
72
|
+
# @param [Float?] timeout
|
|
73
|
+
# Kill the child processes when +timeout+ seconds have elapsed. +nil+ for no timeout.
|
|
65
74
|
# @yieldparam [String] bpf
|
|
66
75
|
# Seccomp bpf in raw bytes.
|
|
67
76
|
# @yieldparam [Symbol] arch
|
|
68
77
|
# Architecture. See {SeccompTools::Syscall::ABI} for supported architectures.
|
|
69
78
|
# @return [Array<Object>, Array<String>]
|
|
70
|
-
#
|
|
71
|
-
|
|
79
|
+
# One entry per dumped filter: the block's return values when a block is given, otherwise
|
|
80
|
+
# the raw bytes.
|
|
81
|
+
def handle(limit, timeout: nil, &block)
|
|
72
82
|
collect = []
|
|
73
83
|
syscalls = {} # record last syscall
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
begin
|
|
85
|
+
Timeout.timeout(timeout) do
|
|
86
|
+
loop while wait_syscall do |child|
|
|
87
|
+
if syscalls[child].nil? # invoke syscall
|
|
88
|
+
syscalls[child] = syscall(child)
|
|
89
|
+
next true
|
|
90
|
+
end
|
|
91
|
+
# syscall finished
|
|
92
|
+
sys = syscalls[child]
|
|
93
|
+
syscalls[child] = nil
|
|
94
|
+
if sys.set_seccomp? && syscall(child).ret.zero? # consider successful call only
|
|
95
|
+
bpf = sys.dump_bpf
|
|
96
|
+
collect << (block.nil? ? bpf : yield(bpf, sys.arch))
|
|
97
|
+
limit -= 1
|
|
98
|
+
end
|
|
99
|
+
!limit.zero?
|
|
100
|
+
end
|
|
78
101
|
end
|
|
79
|
-
|
|
80
|
-
|
|
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?
|
|
102
|
+
rescue Timeout::Error
|
|
103
|
+
# keep the filters dumped so far; fall through to kill the children
|
|
88
104
|
end
|
|
89
|
-
|
|
105
|
+
@pids.each { |cpid| Process.kill('KILL', cpid) if alive?(cpid) }
|
|
90
106
|
Process.waitall
|
|
91
107
|
collect
|
|
92
108
|
end
|
|
93
109
|
|
|
94
110
|
private
|
|
95
111
|
|
|
112
|
+
# Waits until a traced child enters or leaves a syscall, then resumes it.
|
|
113
|
+
#
|
|
96
114
|
# @yieldparam [Integer] pid
|
|
115
|
+
# Id of the child that stopped.
|
|
116
|
+
# @yieldreturn [Boolean]
|
|
117
|
+
# Whether tracing should continue.
|
|
97
118
|
# @return [Boolean]
|
|
98
119
|
# +true+ for continue,
|
|
99
|
-
# +false+ for break.
|
|
120
|
+
# +false+ for break. Also +false+ once no children are left.
|
|
100
121
|
def wait_syscall
|
|
101
122
|
child, status = Process.wait2
|
|
123
|
+
@pids << child unless @pids.include?(child)
|
|
102
124
|
cont = true
|
|
103
|
-
|
|
104
|
-
if [Ptrace::EVENT_CLONE, Ptrace::EVENT_FORK, Ptrace::EVENT_VFORK].include?(status >> 16)
|
|
125
|
+
if [Ptrace::EVENT_CLONE, Ptrace::EVENT_FORK, Ptrace::EVENT_VFORK].include?(status.to_i >> 16)
|
|
105
126
|
# New child launched!
|
|
106
127
|
# newpid = SeccompTools::Ptrace.geteventmsg(child)
|
|
107
128
|
elsif status.stopped? && status.stopsig & 0x80 != 0
|
|
@@ -113,11 +134,20 @@ module SeccompTools
|
|
|
113
134
|
false
|
|
114
135
|
end
|
|
115
136
|
|
|
137
|
+
# Reads the syscall the stopped child is currently invoking.
|
|
138
|
+
#
|
|
139
|
+
# @param [Integer] pid
|
|
140
|
+
# Id of the stopped child.
|
|
116
141
|
# @return [SeccompTools::Syscall]
|
|
117
142
|
def syscall(pid)
|
|
118
143
|
SeccompTools::Syscall.new(pid)
|
|
119
144
|
end
|
|
120
145
|
|
|
146
|
+
# Is the process still running?
|
|
147
|
+
#
|
|
148
|
+
# @param [Integer] pid
|
|
149
|
+
# Id of the process to be checked.
|
|
150
|
+
# @return [Boolean]
|
|
121
151
|
def alive?(pid)
|
|
122
152
|
Process.getpgid(pid)
|
|
123
153
|
true
|
|
@@ -132,7 +162,7 @@ module SeccompTools
|
|
|
132
162
|
def handle_child(*args)
|
|
133
163
|
Ptrace.traceme_and_stop
|
|
134
164
|
exec(*args)
|
|
135
|
-
rescue # rubocop:disable Style/RescueStandardError
|
|
165
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
136
166
|
Logger.error("Failed to execute #{args.join(' ')}")
|
|
137
167
|
exit(1)
|
|
138
168
|
end
|
|
@@ -149,10 +179,11 @@ module SeccompTools
|
|
|
149
179
|
# Number of filters to dump. Negative number for unlimited.
|
|
150
180
|
# @yieldparam [String] bpf
|
|
151
181
|
# Seccomp bpf in raw bytes.
|
|
152
|
-
# @yieldparam [Symbol] arch
|
|
153
|
-
# Architecture of the target process
|
|
182
|
+
# @yieldparam [Symbol?] arch
|
|
183
|
+
# Architecture of the target process, +nil+ when it cannot be determined.
|
|
184
|
+
# See {SeccompTools::Util.process_arch}.
|
|
154
185
|
# @return [Array<Object>, Array<String>]
|
|
155
|
-
#
|
|
186
|
+
# Returns what the block returned. If a block is not given, an array of raw bytes will be returned.
|
|
156
187
|
# @raise [Errno::ESRCH]
|
|
157
188
|
# Raises when the target process does not exist.
|
|
158
189
|
# @raise [Errno::EPERM]
|
|
@@ -174,6 +205,7 @@ module SeccompTools
|
|
|
174
205
|
return [] unless SUPPORTED
|
|
175
206
|
|
|
176
207
|
collect = []
|
|
208
|
+
arch = Util.process_arch(pid)
|
|
177
209
|
Ptrace.attach_and_wait(pid)
|
|
178
210
|
begin
|
|
179
211
|
i = 0
|
|
@@ -183,7 +215,7 @@ module SeccompTools
|
|
|
183
215
|
rescue Errno::ENOENT, Errno::EINVAL
|
|
184
216
|
break
|
|
185
217
|
end
|
|
186
|
-
collect << (block.nil? ? bpf : yield(bpf,
|
|
218
|
+
collect << (block.nil? ? bpf : yield(bpf, arch))
|
|
187
219
|
i += 1
|
|
188
220
|
end
|
|
189
221
|
ensure
|
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
require 'seccomp-tools/const'
|
|
4
4
|
|
|
5
5
|
module SeccompTools
|
|
6
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
@@ -98,9 +112,12 @@ module SeccompTools
|
|
|
98
112
|
|
|
99
113
|
def alu(op, src)
|
|
100
114
|
if op == :neg
|
|
101
|
-
set(:a, 2**32 - get(:a))
|
|
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
|
-
|
|
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(
|
|
148
|
-
when 1 then @arch || undefined(
|
|
149
|
-
when 2 then @ip
|
|
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
|
|
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
|
data/lib/seccomp-tools/error.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SeccompTools
|
|
4
|
-
# Base
|
|
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
|
|
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
|