seccomp-notify 0.1.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 +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +82 -0
- data/Rakefile +8 -0
- data/examples/deny_network.rb +11 -0
- data/examples/emulate_open.rb +19 -0
- data/examples/strace_lite.rb +14 -0
- data/exe/seccomp-notify-supervisor +15 -0
- data/lib/seccomp/notify/bpf/builder.rb +61 -0
- data/lib/seccomp/notify/bpf/instruction.rb +13 -0
- data/lib/seccomp/notify/bpf/program.rb +30 -0
- data/lib/seccomp/notify/constants.rb +62 -0
- data/lib/seccomp/notify/errors.rb +13 -0
- data/lib/seccomp/notify/fd_passing.rb +28 -0
- data/lib/seccomp/notify/features.rb +106 -0
- data/lib/seccomp/notify/filter.rb +21 -0
- data/lib/seccomp/notify/ioctl.rb +34 -0
- data/lib/seccomp/notify/libc.rb +76 -0
- data/lib/seccomp/notify/policy.rb +84 -0
- data/lib/seccomp/notify/request.rb +165 -0
- data/lib/seccomp/notify/structs.rb +21 -0
- data/lib/seccomp/notify/supervisor.rb +198 -0
- data/lib/seccomp/notify/syscalls.rb +747 -0
- data/lib/seccomp/notify/target_memory.rb +78 -0
- data/lib/seccomp/notify/version.rb +7 -0
- data/lib/seccomp/notify.rb +221 -0
- data/spike/notify_min.c +65 -0
- data/tools/gen_syscall_table.rb +59 -0
- metadata +86 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seccomp
|
|
4
|
+
module Notify
|
|
5
|
+
class Policy
|
|
6
|
+
BOOTSTRAP_SYSCALLS = %i[fcntl sendmsg].freeze
|
|
7
|
+
ALLOWED_FLAGS = Constants::SECCOMP_FILTER_FLAG_LOG |
|
|
8
|
+
Constants::SECCOMP_FILTER_FLAG_SPEC_ALLOW |
|
|
9
|
+
Constants::SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
allow: Constants::SECCOMP_RET_ALLOW,
|
|
12
|
+
errno: Constants::SECCOMP_RET_ERRNO | Errno::EPERM::Errno,
|
|
13
|
+
kill: Constants::SECCOMP_RET_KILL_PROCESS
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :default_action
|
|
17
|
+
|
|
18
|
+
def initialize(default: :allow, deny_io_uring: true, flags: 0, &block)
|
|
19
|
+
@default_action = DEFAULTS.fetch(default) { raise InvalidPolicyError, "invalid default action: #{default.inspect}" }
|
|
20
|
+
raise ArgumentError, "flags must be an integer" unless flags.is_a?(Integer)
|
|
21
|
+
|
|
22
|
+
@flags = flags
|
|
23
|
+
@notifications = []
|
|
24
|
+
@denials = {}
|
|
25
|
+
deny_io_uring! if deny_io_uring
|
|
26
|
+
instance_eval(&block) if block
|
|
27
|
+
validate!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Marks syscalls for supervisor notification.
|
|
31
|
+
# @param names [Array<Symbol>]
|
|
32
|
+
# @return [void]
|
|
33
|
+
def notify(*names)
|
|
34
|
+
@notifications.concat(names.map(&:to_sym))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Rejects syscalls in the kernel without notifying the supervisor.
|
|
38
|
+
# @param names [Array<Symbol>]
|
|
39
|
+
# @param errno [Class, Integer] an Errno class or numeric errno
|
|
40
|
+
# @return [void]
|
|
41
|
+
def deny(*names, errno: Errno::EPERM)
|
|
42
|
+
number = Constants.errno_number(errno)
|
|
43
|
+
names.each { |name| @denials[name.to_sym] = number }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Denies io_uring setup so submitted I/O cannot bypass seccomp.
|
|
47
|
+
#
|
|
48
|
+
# @return [void]
|
|
49
|
+
def deny_io_uring!
|
|
50
|
+
deny(:io_uring_setup, errno: Errno::ENOSYS)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def flags
|
|
54
|
+
@flags
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def decisions(arch)
|
|
58
|
+
validate!
|
|
59
|
+
notified = @notifications.to_h { |name| [Syscalls.number(name, arch), Constants::SECCOMP_RET_USER_NOTIF] }
|
|
60
|
+
denied = @denials.to_h { |name, errno| [Syscalls.number(name, arch), Constants::SECCOMP_RET_ERRNO | errno] }
|
|
61
|
+
notified.merge(denied).tap do |decisions|
|
|
62
|
+
BOOTSTRAP_SYSCALLS.each do |name|
|
|
63
|
+
decisions[Syscalls.number(name, arch)] = Constants::SECCOMP_RET_ALLOW unless @default_action == Constants::SECCOMP_RET_ALLOW
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def notified?(name)
|
|
69
|
+
@notifications.include?(name.to_sym)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def validate!
|
|
75
|
+
invalid_bootstrap = BOOTSTRAP_SYSCALLS.find { |name| notified?(name) || @denials.key?(name) }
|
|
76
|
+
if invalid_bootstrap
|
|
77
|
+
raise InvalidPolicyError, "#{invalid_bootstrap} must be allowed while transferring the listener fd"
|
|
78
|
+
end
|
|
79
|
+
raise InvalidPolicyError, "TSYNC and NEW_LISTENER cannot be combined" if (@flags & Constants::SECCOMP_FILTER_FLAG_TSYNC).positive?
|
|
80
|
+
raise InvalidPolicyError, "unsupported filter flags: 0x#{@flags.to_s(16)}" unless (@flags & ~ALLOWED_FLAGS).zero?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seccomp
|
|
4
|
+
module Notify
|
|
5
|
+
class Request
|
|
6
|
+
attr_reader :id, :tid, :flags, :nr, :instruction_pointer, :args
|
|
7
|
+
alias_method :pid, :tid
|
|
8
|
+
|
|
9
|
+
def self.from_binary(binary, listener:, sizes:, memory_cache:, memory_cache_mutex:, features:)
|
|
10
|
+
id, tid, flags, nr, audit_arch, ip, *args = binary.unpack(Structs::NOTIF_FORMAT)
|
|
11
|
+
arch = case audit_arch
|
|
12
|
+
when Constants::AUDIT_ARCH_X86_64 then :x86_64
|
|
13
|
+
when Constants::AUDIT_ARCH_AARCH64 then :aarch64
|
|
14
|
+
else raise NotSupportedError, "unsupported audit architecture 0x#{audit_arch.to_s(16)}"
|
|
15
|
+
end
|
|
16
|
+
new(id:, tid:, flags:, nr:, arch:, instruction_pointer: ip, args:, listener:, sizes:, memory_cache:, memory_cache_mutex:, features:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(id:, tid:, flags:, nr:, arch:, instruction_pointer:, args:, listener:, sizes:, memory_cache:, memory_cache_mutex:, features:)
|
|
20
|
+
@id = id
|
|
21
|
+
@tid = tid
|
|
22
|
+
@flags = flags
|
|
23
|
+
@nr = nr
|
|
24
|
+
@arch = arch
|
|
25
|
+
@instruction_pointer = instruction_pointer
|
|
26
|
+
@args = args.freeze
|
|
27
|
+
@listener = listener
|
|
28
|
+
@sizes = sizes
|
|
29
|
+
@features = features
|
|
30
|
+
@memory = TargetMemory.new(listener, self, memory_cache, memory_cache_mutex)
|
|
31
|
+
@responded = false
|
|
32
|
+
@pointer_read = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :arch
|
|
36
|
+
|
|
37
|
+
def syscall
|
|
38
|
+
Syscalls.name(@nr, @arch)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def responded?
|
|
42
|
+
@responded
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def valid?
|
|
46
|
+
buffer = [@id].pack("Q<")
|
|
47
|
+
validate_id(Ioctl::NOTIF_ID_VALID, buffer)
|
|
48
|
+
true
|
|
49
|
+
rescue Errno::EINVAL
|
|
50
|
+
begin
|
|
51
|
+
validate_id(Ioctl::NOTIF_ID_VALID_OLD, buffer)
|
|
52
|
+
true
|
|
53
|
+
rescue Errno::ENOENT
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
rescue Errno::ENOENT
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Reads bytes from target memory and revalidates the notification afterward.
|
|
61
|
+
# @return [String]
|
|
62
|
+
def read(address, length)
|
|
63
|
+
@memory.read(address, length)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Reads a NUL-terminated string from target memory.
|
|
67
|
+
# @return [String]
|
|
68
|
+
def read_string(address, max: 4096)
|
|
69
|
+
@memory.read_string(address, max:)
|
|
70
|
+
end
|
|
71
|
+
alias_method :read_cstring, :read_string
|
|
72
|
+
|
|
73
|
+
# Decodes an AF_INET, AF_INET6, or AF_UNIX socket address.
|
|
74
|
+
# @return [Addrinfo]
|
|
75
|
+
def read_sockaddr(address, length)
|
|
76
|
+
@memory.read_sockaddr(address, length)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Emulates a successful syscall return.
|
|
80
|
+
# @return [void]
|
|
81
|
+
def allow!(value = 0)
|
|
82
|
+
respond!(value:, error: 0, flags: 0)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Emulates a failed syscall return.
|
|
86
|
+
# @return [void]
|
|
87
|
+
def error!(error = Errno::EPERM)
|
|
88
|
+
errno = Constants.errno_number(error)
|
|
89
|
+
respond!(value: 0, error: -errno, flags: 0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Lets the kernel execute the original syscall.
|
|
93
|
+
# @param unsafe [Boolean] acknowledges pointer-read TOCTOU risk
|
|
94
|
+
# @return [void]
|
|
95
|
+
def continue!(unsafe: false)
|
|
96
|
+
raise NotSupportedError, "SECCOMP_USER_NOTIF_FLAG_CONTINUE is unavailable" unless @features[:continue]
|
|
97
|
+
warn("seccomp-notify: continuing after reading target memory is subject to TOCTOU; pass unsafe: true to acknowledge") if @pointer_read && !unsafe
|
|
98
|
+
respond!(value: 0, error: 0, flags: Constants::SECCOMP_USER_NOTIF_FLAG_CONTINUE)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Injects an open file descriptor into the target.
|
|
102
|
+
# @return [Integer] the target file descriptor number
|
|
103
|
+
def add_fd!(io, flags: 0, newfd: 0, newfd_flags: 0)
|
|
104
|
+
raise NotSupportedError, "SECCOMP_IOCTL_NOTIF_ADDFD is unavailable" unless @features[:addfd]
|
|
105
|
+
ensure_unresponded!
|
|
106
|
+
unless [flags, newfd, newfd_flags].all? { |value| value.is_a?(Integer) } && (0..0xffff_ffff).cover?(newfd)
|
|
107
|
+
raise ArgumentError, "flags and fd values must be unsigned integers"
|
|
108
|
+
end
|
|
109
|
+
unless (flags & ~Constants::SECCOMP_ADDFD_FLAG_SETFD).zero?
|
|
110
|
+
raise ArgumentError, "flags may only contain SECCOMP_ADDFD_FLAG_SETFD"
|
|
111
|
+
end
|
|
112
|
+
unless (newfd_flags & ~Constants::O_CLOEXEC).zero?
|
|
113
|
+
raise ArgumentError, "newfd_flags may only contain O_CLOEXEC"
|
|
114
|
+
end
|
|
115
|
+
if (flags & Constants::SECCOMP_ADDFD_FLAG_SETFD).zero? && !newfd.zero?
|
|
116
|
+
raise ArgumentError, "newfd requires SECCOMP_ADDFD_FLAG_SETFD"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
addfd_flags = flags
|
|
120
|
+
addfd_flags |= Constants::SECCOMP_ADDFD_FLAG_SEND if @features[:addfd_send]
|
|
121
|
+
buffer = [@id, addfd_flags, io.fileno, newfd, newfd_flags].pack(Structs::ADDFD_FORMAT)
|
|
122
|
+
injected = Ioctl.call(@listener, Ioctl::NOTIF_ADDFD, buffer)
|
|
123
|
+
if @features[:addfd_send]
|
|
124
|
+
@responded = true
|
|
125
|
+
else
|
|
126
|
+
allow!(injected)
|
|
127
|
+
end
|
|
128
|
+
injected
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def kill!(signal = "KILL")
|
|
132
|
+
ensure_unresponded!
|
|
133
|
+
Process.kill(signal, @tid)
|
|
134
|
+
@responded = true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def mark_pointer_read!
|
|
138
|
+
@pointer_read = true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def validate_id(request, buffer)
|
|
144
|
+
Ioctl.call(@listener, request, buffer)
|
|
145
|
+
rescue Errno::EINTR
|
|
146
|
+
retry
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def respond!(value:, error:, flags:)
|
|
150
|
+
ensure_unresponded!
|
|
151
|
+
buffer = [@id, value, error, flags].pack(Structs::RESPONSE_FORMAT).ljust(@sizes[:resp], "\0")
|
|
152
|
+
Ioctl.call(@listener, Ioctl::NOTIF_SEND, buffer)
|
|
153
|
+
@responded = true
|
|
154
|
+
rescue Errno::EINTR
|
|
155
|
+
retry
|
|
156
|
+
rescue Errno::ENOENT
|
|
157
|
+
@responded = true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ensure_unresponded!
|
|
161
|
+
raise AlreadyRespondedError, "notification #{@id} has already been answered" if @responded
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seccomp
|
|
4
|
+
module Notify
|
|
5
|
+
module Structs
|
|
6
|
+
NOTIF_FORMAT = "Q<L<L<l<L<Q<Q<6"
|
|
7
|
+
RESPONSE_FORMAT = "Q<q<l<L<"
|
|
8
|
+
ADDFD_FORMAT = "Q<L<L<L<L<"
|
|
9
|
+
SIZES_FORMAT = "S<S<S<"
|
|
10
|
+
INSTRUCTION_FORMAT = "S<CCL<"
|
|
11
|
+
PROGRAM_FORMAT = "S<x6J<"
|
|
12
|
+
|
|
13
|
+
NOTIF_SIZE = 80
|
|
14
|
+
RESPONSE_SIZE = 24
|
|
15
|
+
ADDFD_SIZE = 24
|
|
16
|
+
SIZES_SIZE = 6
|
|
17
|
+
INSTRUCTION_SIZE = 8
|
|
18
|
+
PROGRAM_SIZE = 16
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Seccomp
|
|
6
|
+
module Notify
|
|
7
|
+
class Supervisor
|
|
8
|
+
USER_OPTIONS = %i[poll_interval default_errno timeout concurrency features].freeze
|
|
9
|
+
EXPECTED_SIZES = {notif: Structs::NOTIF_SIZE, resp: Structs::RESPONSE_SIZE, data: 64}.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :listener, :target_pid
|
|
12
|
+
|
|
13
|
+
def self.validate_options!(options)
|
|
14
|
+
unknown = options.keys - USER_OPTIONS
|
|
15
|
+
raise ArgumentError, "unknown supervisor options: #{unknown.join(", ")}" unless unknown.empty?
|
|
16
|
+
if options.key?(:concurrency) && (!options[:concurrency].is_a?(Integer) || !options[:concurrency].positive?)
|
|
17
|
+
raise ArgumentError, "concurrency must be a positive integer"
|
|
18
|
+
end
|
|
19
|
+
if options.key?(:poll_interval) && !valid_duration?(options[:poll_interval], allow_zero: true)
|
|
20
|
+
raise ArgumentError, "poll_interval must be non-negative"
|
|
21
|
+
end
|
|
22
|
+
if options[:timeout] && !valid_duration?(options[:timeout], allow_zero: false)
|
|
23
|
+
raise ArgumentError, "timeout must be positive"
|
|
24
|
+
end
|
|
25
|
+
if options.key?(:features) && !options[:features].is_a?(Hash)
|
|
26
|
+
raise ArgumentError, "features must be a hash"
|
|
27
|
+
end
|
|
28
|
+
return unless options.key?(:default_errno)
|
|
29
|
+
|
|
30
|
+
Constants.errno_number(options[:default_errno])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.valid_duration?(value, allow_zero:)
|
|
34
|
+
value.is_a?(Numeric) && value.real? && value.finite? && (allow_zero ? !value.negative? : value.positive?)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(listener, target_pid: nil, target_child: nil, poll_interval: 0.1, default_errno: Errno::EPERM,
|
|
38
|
+
timeout: nil, concurrency: 1, features: Notify.features, health_writer: nil)
|
|
39
|
+
self.class.validate_options!(poll_interval:, default_errno:, timeout:, concurrency:, features:)
|
|
40
|
+
|
|
41
|
+
@listener = listener
|
|
42
|
+
@target_pid = target_pid
|
|
43
|
+
@target_child = target_child
|
|
44
|
+
@poll_interval = poll_interval
|
|
45
|
+
@default_errno = default_errno
|
|
46
|
+
@timeout = timeout
|
|
47
|
+
@concurrency = concurrency
|
|
48
|
+
@features = features
|
|
49
|
+
@health_writer = health_writer
|
|
50
|
+
@sizes = Notify.notif_sizes
|
|
51
|
+
raise NotSupportedError, "unsupported kernel notification sizes: #{@sizes.inspect}" unless @sizes == EXPECTED_SIZES
|
|
52
|
+
|
|
53
|
+
@handlers = {}
|
|
54
|
+
@unknown_handler = ->(request) { request.error!(@default_errno) }
|
|
55
|
+
@error_handler = ->(error, _request) { warn("seccomp-notify supervisor: #{error.full_message(highlight: false, order: :top)}") }
|
|
56
|
+
@memory_cache = {}
|
|
57
|
+
@memory_cache_mutex = Mutex.new
|
|
58
|
+
@receive_mutex = Mutex.new
|
|
59
|
+
@stop = false
|
|
60
|
+
@listener_closed = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Registers a handler for a notified syscall.
|
|
64
|
+
# @param syscall [Symbol]
|
|
65
|
+
# @yieldparam request [Request]
|
|
66
|
+
# @return [Supervisor]
|
|
67
|
+
def on(syscall, &handler)
|
|
68
|
+
raise ArgumentError, "handler block is required" unless handler
|
|
69
|
+
|
|
70
|
+
@handlers[syscall.to_sym] = handler
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Registers the fallback handler for unregistered syscalls.
|
|
75
|
+
# @yieldparam request [Request]
|
|
76
|
+
# @return [Supervisor]
|
|
77
|
+
def on_unknown(&handler)
|
|
78
|
+
raise ArgumentError, "handler block is required" unless handler
|
|
79
|
+
|
|
80
|
+
@unknown_handler = handler
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Registers an exception callback.
|
|
85
|
+
# @yieldparam error [Exception]
|
|
86
|
+
# @yieldparam request [Request]
|
|
87
|
+
# @return [Supervisor]
|
|
88
|
+
def on_error(&handler)
|
|
89
|
+
raise ArgumentError, "handler block is required" unless handler
|
|
90
|
+
|
|
91
|
+
@error_handler = handler
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Runs until the target exits and returns its process status when available.
|
|
96
|
+
# @return [Process::Status, nil]
|
|
97
|
+
def run
|
|
98
|
+
workers = Array.new(@concurrency) { Thread.new { event_loop } }
|
|
99
|
+
status = wait_for_target
|
|
100
|
+
wait_until_listener_closes if @target_pid && !@stop
|
|
101
|
+
@stop = true
|
|
102
|
+
workers.each(&:join)
|
|
103
|
+
status
|
|
104
|
+
ensure
|
|
105
|
+
@stop = true
|
|
106
|
+
@memory_cache.each_value { |io| io.close unless io.closed? }
|
|
107
|
+
@listener.close unless @listener.closed?
|
|
108
|
+
@health_writer&.close unless @health_writer&.closed?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def stop
|
|
112
|
+
@stop = true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def wait_for_target
|
|
118
|
+
return wait_until_listener_closes unless @target_pid
|
|
119
|
+
|
|
120
|
+
loop do
|
|
121
|
+
return if @stop
|
|
122
|
+
|
|
123
|
+
waited = Process.waitpid2(@target_pid, Process::WNOHANG)
|
|
124
|
+
return waited.last if waited
|
|
125
|
+
sleep(@poll_interval)
|
|
126
|
+
end
|
|
127
|
+
rescue Errno::ECHILD
|
|
128
|
+
return if @target_child
|
|
129
|
+
|
|
130
|
+
wait_for_non_child
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def wait_for_non_child
|
|
134
|
+
loop do
|
|
135
|
+
return if @stop
|
|
136
|
+
|
|
137
|
+
Process.kill(0, @target_pid)
|
|
138
|
+
sleep(@poll_interval)
|
|
139
|
+
end
|
|
140
|
+
rescue Errno::ESRCH
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def wait_until_listener_closes
|
|
145
|
+
sleep(@poll_interval) until @stop || @listener_closed
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def event_loop
|
|
149
|
+
process_one until @stop || @listener_closed
|
|
150
|
+
rescue IOError, Errno::EBADF, Errno::ENOTCONN
|
|
151
|
+
@stop = true
|
|
152
|
+
rescue StandardError => error
|
|
153
|
+
warn("seccomp-notify worker failed: #{error.message}")
|
|
154
|
+
@stop = true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def process_one
|
|
158
|
+
request = @receive_mutex.synchronize { receive_request }
|
|
159
|
+
return unless request
|
|
160
|
+
|
|
161
|
+
dispatch(request)
|
|
162
|
+
rescue Errno::EINTR
|
|
163
|
+
retry
|
|
164
|
+
rescue Errno::ENOENT
|
|
165
|
+
@listener_closed = true if Libc.poll_hup?(@listener.fileno)
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def receive_request
|
|
170
|
+
return unless IO.select([@listener], nil, nil, @poll_interval)
|
|
171
|
+
|
|
172
|
+
buffer = "\0" * @sizes[:notif]
|
|
173
|
+
Ioctl.call(@listener, Ioctl::NOTIF_RECV, buffer)
|
|
174
|
+
Request.from_binary(
|
|
175
|
+
buffer,
|
|
176
|
+
listener: @listener,
|
|
177
|
+
sizes: @sizes,
|
|
178
|
+
memory_cache: @memory_cache,
|
|
179
|
+
memory_cache_mutex: @memory_cache_mutex,
|
|
180
|
+
features: @features
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def dispatch(request)
|
|
185
|
+
handler = @handlers.fetch(request.syscall, @unknown_handler)
|
|
186
|
+
@timeout ? Timeout.timeout(@timeout) { handler.call(request) } : handler.call(request)
|
|
187
|
+
rescue StandardError => error
|
|
188
|
+
begin
|
|
189
|
+
@error_handler.call(error, request)
|
|
190
|
+
rescue Exception => callback_error # rubocop:disable Lint/RescueException
|
|
191
|
+
warn("seccomp-notify error callback failed: #{callback_error.message}")
|
|
192
|
+
end
|
|
193
|
+
ensure
|
|
194
|
+
request&.error!(@default_errno) unless request&.responded?
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|