landlock 0.1.1 → 0.3

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.
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "env"
4
+ require_relative "native"
5
+ require_relative "policy"
6
+ require_relative "result"
7
+ require_relative "rlimits"
8
+ require_relative "runner"
9
+ require_relative "validation"
10
+
11
+ module Landlock
12
+ module Execution
13
+ module_function
14
+
15
+ def exec(
16
+ argv,
17
+ read: [],
18
+ write: [],
19
+ execute: [],
20
+ connect_tcp: [],
21
+ bind_tcp: [],
22
+ paths: [],
23
+ scope: [],
24
+ chdir: nil,
25
+ env: nil,
26
+ unsetenv_others: false,
27
+ close_others: true,
28
+ allow_all_known: false
29
+ )
30
+ pid =
31
+ spawn(
32
+ argv,
33
+ read:,
34
+ write:,
35
+ execute:,
36
+ connect_tcp:,
37
+ bind_tcp:,
38
+ paths:,
39
+ scope:,
40
+ chdir:,
41
+ env:,
42
+ unsetenv_others:,
43
+ close_others:,
44
+ allow_all_known:
45
+ )
46
+ _, status = ::Process.wait2(pid)
47
+ status
48
+ end
49
+
50
+ def spawn(
51
+ argv,
52
+ read: [],
53
+ write: [],
54
+ execute: [],
55
+ connect_tcp: [],
56
+ bind_tcp: [],
57
+ paths: [],
58
+ scope: [],
59
+ chdir: nil,
60
+ env: nil,
61
+ unsetenv_others: false,
62
+ close_others: true,
63
+ allow_all_known: false
64
+ )
65
+ argv = Validation.normalize_argv(argv).map(&:to_s)
66
+ ensure_landlock_supported!
67
+ env = Env.normalize(env)
68
+ policy =
69
+ prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
70
+ validate_landlock_restriction!(**policy)
71
+
72
+ spawn_with_runner(argv, **policy, chdir:, env:, unsetenv_others:, close_others:)
73
+ end
74
+
75
+ def capture(argv, **options)
76
+ capture_with(argv, raise_on_failure: false, **options)
77
+ end
78
+
79
+ def capture!(argv, **options)
80
+ capture_with(argv, raise_on_failure: true, **options)
81
+ end
82
+
83
+ def capture_with(
84
+ argv,
85
+ read: [],
86
+ write: [],
87
+ execute: [],
88
+ connect_tcp: [],
89
+ bind_tcp: [],
90
+ paths: [],
91
+ scope: [],
92
+ chdir: nil,
93
+ env: nil,
94
+ unsetenv_others: false,
95
+ close_others: true,
96
+ allow_all_known: false,
97
+ timeout: nil,
98
+ stdin: nil,
99
+ rlimits: {},
100
+ seccomp_deny_network: false,
101
+ max_output_bytes: nil,
102
+ truncate_output: false,
103
+ success_status_codes: [0],
104
+ failure_message: "",
105
+ raise_on_failure:
106
+ )
107
+ argv = Validation.normalize_argv(argv).map(&:to_s)
108
+ ensure_landlock_supported!
109
+ max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
110
+ timeout = Validation.validate_timeout!(timeout)
111
+ normalized_rlimits = Rlimits.normalize(rlimits)
112
+ env = Env.normalize(env)
113
+ policy =
114
+ prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
115
+ validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits: normalized_rlimits)
116
+
117
+ result =
118
+ call_with_runner(
119
+ argv,
120
+ **policy,
121
+ chdir:,
122
+ env:,
123
+ unsetenv_others:,
124
+ close_others:,
125
+ timeout:,
126
+ stdin:,
127
+ rlimits: normalized_rlimits,
128
+ seccomp_deny_network:,
129
+ max_output_bytes:,
130
+ truncate_output:
131
+ )
132
+
133
+ if raise_on_failure &&
134
+ (result.timed_out? || !result.status.exited? || !success_status_codes.include?(result.status.exitstatus))
135
+ message = [argv.join(" "), failure_message, result.stderr].filter { |part| part.to_s != "" }.join("\n")
136
+ raise CommandError.new(message, stdout: result.stdout, stderr: result.stderr, status: result.status, result:)
137
+ end
138
+
139
+ result
140
+ rescue OutputTooLargeError => e
141
+ message = [argv&.join(" "), failure_message, e.message].filter { |part| part.to_s != "" }.join("\n")
142
+ result = e.result
143
+ raise CommandError.new(
144
+ message,
145
+ stdout: result&.stdout.to_s,
146
+ stderr: result&.stderr.to_s,
147
+ status: result&.status,
148
+ result:
149
+ )
150
+ end
151
+
152
+ def spawn_with_runner(argv, **options)
153
+ if Runner::Native.available?
154
+ begin
155
+ return Runner::Native.spawn(argv, **options)
156
+ rescue Errno::E2BIG
157
+ return Runner::Fork.spawn(argv, **options)
158
+ end
159
+ end
160
+
161
+ Runner::Fork.spawn(argv, **options)
162
+ end
163
+
164
+ def call_with_runner(argv, **options)
165
+ if Runner::Native.available?
166
+ begin
167
+ return Runner::Native.call(argv, **options)
168
+ rescue Errno::E2BIG
169
+ return Runner::Fork.call(argv, **options)
170
+ end
171
+ end
172
+
173
+ Runner::Fork.call(argv, **options)
174
+ end
175
+
176
+ def ensure_landlock_supported!
177
+ raise UnsupportedError, "Linux Landlock is unavailable" unless Native.abi_version.positive?
178
+ end
179
+
180
+ def prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
181
+ connect_tcp = Validation.normalize_ports(connect_tcp, :connect_tcp)
182
+ bind_tcp = Validation.normalize_ports(bind_tcp, :bind_tcp)
183
+ read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
184
+ { read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
185
+ end
186
+
187
+ def validate_landlock_restriction!(
188
+ read:,
189
+ write:,
190
+ execute:,
191
+ connect_tcp:,
192
+ bind_tcp:,
193
+ paths:,
194
+ scope:,
195
+ allow_all_known:
196
+ )
197
+ return if Policy.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
198
+
199
+ raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
200
+ end
201
+
202
+ def validate_capture_restriction!(
203
+ read:,
204
+ write:,
205
+ execute:,
206
+ connect_tcp:,
207
+ bind_tcp:,
208
+ paths:,
209
+ scope:,
210
+ allow_all_known:,
211
+ seccomp_deny_network:,
212
+ rlimits:
213
+ )
214
+ return if Policy.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
215
+ return if seccomp_deny_network
216
+ return if Array(rlimits).any?
217
+
218
+ raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, or rlimits"
219
+ end
220
+
221
+ def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
222
+ base = chdir ? File.expand_path(chdir) : Dir.pwd
223
+ abi = Native.abi_version
224
+ read = Validation.validate_existing_paths(read, :read, chdir:)
225
+ write = Validation.validate_existing_paths(write, :write, chdir:)
226
+ execute = Validation.validate_existing_paths(execute, :execute, chdir:)
227
+ paths =
228
+ Array(paths).map do |rule|
229
+ path, rights = Policy.normalize_path_rule(rule)
230
+ Validation.validate_existing_path!(path, :path, base)
231
+ Policy.path_rule_access_mask(File.expand_path(path, base), rights, abi)
232
+ { path: path.to_s, rights: }
233
+ end
234
+
235
+ [read, write, execute, paths]
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "landlock"
5
+
6
+ module Landlock
7
+ module Native
8
+ module_function
9
+
10
+ def abi_version
11
+ Landlock.abi_version
12
+ end
13
+
14
+ def create_ruleset(fs_handled, net_handled, scoped)
15
+ Landlock.__send__(:_create_ruleset, fs_handled, net_handled, scoped)
16
+ end
17
+
18
+ def add_path_rule(fd, path, access_mask)
19
+ Landlock.__send__(:_add_path_rule, fd, path, access_mask)
20
+ end
21
+
22
+ def add_net_rule(fd, port, access_mask)
23
+ Landlock.__send__(:_add_net_rule, fd, port, access_mask)
24
+ end
25
+
26
+ def restrict_self(fd)
27
+ Landlock.__send__(:_restrict_self, fd)
28
+ end
29
+
30
+ def close_fd(fd)
31
+ Landlock.__send__(:_close_fd, fd)
32
+ end
33
+
34
+ def seccomp_deny_network!
35
+ Landlock.seccomp_deny_network!
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "native"
4
+ require_relative "rights"
5
+
6
+ module Landlock
7
+ module Policy
8
+ module_function
9
+
10
+ def restrict!(
11
+ read: [],
12
+ write: [],
13
+ execute: [],
14
+ connect_tcp: [],
15
+ bind_tcp: [],
16
+ paths: [],
17
+ scope: [],
18
+ allow_all_known: false
19
+ )
20
+ abi = Native.abi_version
21
+ raise UnsupportedError, "Linux Landlock is unavailable" unless abi.positive?
22
+
23
+ fs_handled =
24
+ (
25
+ if allow_all_known
26
+ fs_rights_for_abi(abi)
27
+ else
28
+ handled_fs_for(read:, write:, execute:, paths:, abi:)
29
+ end
30
+ )
31
+ net_handled = handled_net_for(connect_tcp:, bind_tcp:, abi:)
32
+ scoped = scope_for(scope:, abi:)
33
+
34
+ if fs_handled.zero? && net_handled.zero? && scoped.zero?
35
+ raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
36
+ end
37
+
38
+ fd = Native.create_ruleset(fs_handled, net_handled, scoped)
39
+ begin
40
+ add_path_rules(fd, read, READ_RIGHTS, abi)
41
+ add_path_rules(fd, execute, EXEC_RIGHTS, abi)
42
+ add_path_rules(fd, write, WRITE_RIGHTS, abi)
43
+
44
+ Array(paths).each do |rule|
45
+ path, rights = normalize_path_rule(rule)
46
+ expanded_path = File.expand_path(path)
47
+ access_mask = path_rule_access_mask(expanded_path, rights, abi)
48
+
49
+ Native.add_path_rule(fd, expanded_path, access_mask)
50
+ end
51
+
52
+ add_net_rules(fd, connect_tcp, [:connect_tcp], abi)
53
+ add_net_rules(fd, bind_tcp, [:bind_tcp], abi)
54
+
55
+ Native.restrict_self(fd)
56
+ ensure
57
+ Native.close_fd(fd) if fd && fd >= 0
58
+ end
59
+
60
+ true
61
+ end
62
+
63
+ def requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
64
+ allow_all_known || Array(read).any? || Array(write).any? || Array(execute).any? || Array(connect_tcp).any? ||
65
+ Array(bind_tcp).any? || Array(paths).any? || Array(scope).any?
66
+ end
67
+
68
+ def path_rights(path, rights)
69
+ File.directory?(path) ? rights : Array(rights) & FILE_PATH_RIGHTS
70
+ end
71
+
72
+ def path_rule_access_mask(path, rights, abi)
73
+ mask(path_rights(path, rights), FS_RIGHTS, abi).tap do |access_mask|
74
+ raise ArgumentError, "path rule has no effective rights: #{path}" if access_mask.zero?
75
+ end
76
+ end
77
+
78
+ def add_path_rules(fd, paths, rights, abi)
79
+ Array(paths).each do |path|
80
+ expanded_path = File.expand_path(path)
81
+ access_mask = mask(path_rights(expanded_path, rights), FS_RIGHTS, abi)
82
+ next if access_mask.zero?
83
+
84
+ Native.add_path_rule(fd, expanded_path, access_mask)
85
+ end
86
+ end
87
+
88
+ def add_net_rules(fd, ports, rights, abi)
89
+ ports = Array(ports)
90
+ return if ports.empty?
91
+ raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
92
+
93
+ access_mask = mask(rights, NET_RIGHTS, abi)
94
+ return if access_mask.zero?
95
+
96
+ ports.each { |port| Native.add_net_rule(fd, Integer(port), access_mask) }
97
+ end
98
+
99
+ def normalize_path_rule(rule)
100
+ case rule
101
+ when Hash
102
+ [rule.fetch(:path), Array(rule.fetch(:rights))]
103
+ when Array
104
+ [rule.fetch(0), Array(rule.fetch(1))]
105
+ else
106
+ raise ArgumentError, "path rule must be {path:, rights:} or [path, rights]"
107
+ end
108
+ end
109
+
110
+ def mask(names, table, abi)
111
+ Array(names).reduce(0) do |bits, name|
112
+ bit = table.fetch(name.to_sym) { raise ArgumentError, "unknown Landlock right: #{name.inspect}" }
113
+ next bits if bit == ACCESS_FS_REFER && abi < 2
114
+ next bits if bit == ACCESS_FS_TRUNCATE && abi < 3
115
+ next bits if bit == ACCESS_FS_IOCTL_DEV && abi < 5
116
+
117
+ bits | bit
118
+ end
119
+ end
120
+
121
+ def fs_rights_for_abi(abi)
122
+ rights = FS_RIGHTS.values.reduce(0, :|)
123
+ rights &= ~ACCESS_FS_REFER if abi < 2
124
+ rights &= ~ACCESS_FS_TRUNCATE if abi < 3
125
+ rights &= ~ACCESS_FS_IOCTL_DEV if abi < 5
126
+ rights
127
+ end
128
+
129
+ def handled_fs_for(read:, write:, execute:, paths:, abi:)
130
+ bits = 0
131
+ bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless Array(read).empty?
132
+ bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless Array(execute).empty?
133
+ bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless Array(write).empty?
134
+ Array(paths).each do |rule|
135
+ path, rights = normalize_path_rule(rule)
136
+ bits |= path_rule_access_mask(File.expand_path(path), rights, abi)
137
+ end
138
+ bits
139
+ end
140
+
141
+ def handled_net_for(connect_tcp:, bind_tcp:, abi:)
142
+ bits = 0
143
+ bits |= ACCESS_NET_CONNECT_TCP unless Array(connect_tcp).empty?
144
+ bits |= ACCESS_NET_BIND_TCP unless Array(bind_tcp).empty?
145
+ return 0 if bits.zero?
146
+
147
+ raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
148
+
149
+ bits
150
+ end
151
+
152
+ def scope_for(scope:, abi:)
153
+ bits = mask(scope, SCOPE_FLAGS, abi)
154
+ return 0 if bits.zero?
155
+
156
+ raise UnsupportedError, "Landlock scopes require ABI v6+; running ABI v#{abi}" if abi < 6
157
+
158
+ bits
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "result"
5
+
6
+ module Landlock
7
+ READ_CHUNK_BYTES = 16 * 1024
8
+ PROCESS_POLL_SECONDS = 0.1
9
+ STDIN_THREAD_JOIN_SECONDS = 0.1
10
+ POST_TIMEOUT_DRAIN_SECONDS = 0.05
11
+
12
+ module ProcessIO
13
+ module_function
14
+
15
+ def complete_pipe_capture(
16
+ pid,
17
+ stdout_reader,
18
+ stderr_reader,
19
+ stdin_writer,
20
+ stdin,
21
+ timeout,
22
+ max_output_bytes,
23
+ truncate_output
24
+ )
25
+ stdin_thread = write_input(stdin_writer, stdin)
26
+
27
+ stdout = +"".b
28
+ stderr = +"".b
29
+ state = { bytes: 0, truncated: false }
30
+ begin
31
+ status, timed_out =
32
+ read_and_wait(
33
+ pid,
34
+ { stdout_reader => stdout, stderr_reader => stderr },
35
+ timeout,
36
+ max_output_bytes,
37
+ truncate_output,
38
+ state
39
+ )
40
+ rescue OutputTooLargeError => error
41
+ status ||= wait_for_pid(pid)
42
+ error.result = capture_result(stdout, stderr, status, output_truncated: true, timed_out:)
43
+ raise
44
+ ensure
45
+ finish_input_thread(stdin_thread, stdin_writer)
46
+ end
47
+
48
+ capture_result(stdout, stderr, status, output_truncated: state[:truncated], timed_out:)
49
+ end
50
+
51
+ def capture_result(stdout, stderr, status, output_truncated:, timed_out:)
52
+ stdout.force_encoding(Encoding.default_external)
53
+ stderr.force_encoding(Encoding.default_external)
54
+ CaptureResult.new(stdout:, stderr:, status:, output_truncated:, timed_out:)
55
+ end
56
+
57
+ def write_input(io, input)
58
+ return io.close if input.nil?
59
+
60
+ Thread.new do
61
+ Thread.current.report_on_exception = false
62
+ begin
63
+ if input.respond_to?(:read)
64
+ while (chunk = input.read(READ_CHUNK_BYTES))
65
+ io.write(chunk)
66
+ end
67
+ else
68
+ io.write(input.to_s)
69
+ end
70
+ rescue Errno::EPIPE, IOError
71
+ ensure
72
+ io.close unless io.closed?
73
+ end
74
+ end
75
+ end
76
+
77
+ def finish_input_thread(thread, io)
78
+ close_stream(io)
79
+ return unless thread
80
+
81
+ if thread.join(STDIN_THREAD_JOIN_SECONDS)
82
+ thread.value
83
+ else
84
+ thread.kill
85
+ thread.join(STDIN_THREAD_JOIN_SECONDS)
86
+ end
87
+ end
88
+
89
+ def read_and_wait(pid, streams, timeout, max_output_bytes, truncate_output, state)
90
+ deadline = timeout ? ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout : nil
91
+ timed_out = false
92
+ status = nil
93
+
94
+ until streams.empty? && status
95
+ if deadline
96
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
97
+ if remaining <= 0
98
+ timed_out = true
99
+ terminate_process(pid)
100
+ status = wait_for_pid(pid)
101
+ drain_streams_until(
102
+ streams,
103
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + POST_TIMEOUT_DRAIN_SECONDS,
104
+ max_output_bytes,
105
+ truncate_output,
106
+ state,
107
+ pid
108
+ )
109
+ close_streams(streams)
110
+ break
111
+ end
112
+ end
113
+
114
+ status ||= poll_pid(pid)
115
+
116
+ break if streams.empty? && status
117
+
118
+ wait =
119
+ (
120
+ if deadline
121
+ [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), PROCESS_POLL_SECONDS].min
122
+ else
123
+ PROCESS_POLL_SECONDS
124
+ end
125
+ )
126
+ wait = 0 if wait.negative?
127
+ if streams.empty?
128
+ sleep wait
129
+ next
130
+ end
131
+
132
+ readable, = IO.select(streams.keys, nil, nil, wait)
133
+ next unless readable
134
+
135
+ readable.each do |io|
136
+ begin
137
+ chunk = io.read_nonblock(READ_CHUNK_BYTES)
138
+ append_output_chunk(streams.fetch(io), chunk, state, max_output_bytes, truncate_output, pid)
139
+ rescue IO::WaitReadable
140
+ next
141
+ rescue EOFError
142
+ streams.delete(io)
143
+ io.close
144
+ end
145
+ end
146
+ end
147
+
148
+ status ||= wait_for_pid(pid)
149
+ [status, timed_out]
150
+ end
151
+
152
+ def poll_pid(pid)
153
+ result = ::Process.wait2(pid, ::Process::WNOHANG)
154
+ result&.last
155
+ rescue Errno::ECHILD
156
+ nil
157
+ end
158
+
159
+ def wait_for_pid(pid)
160
+ ::Process.wait2(pid).last
161
+ rescue Errno::ECHILD
162
+ nil
163
+ end
164
+
165
+ def close_stream(io)
166
+ io.close unless io.closed?
167
+ rescue IOError
168
+ end
169
+
170
+ def read_available_streams(streams, max_output_bytes, truncate_output, state, pid)
171
+ readable, = IO.select(streams.keys, nil, nil, 0)
172
+ return false unless readable
173
+
174
+ readable.each do |io|
175
+ begin
176
+ chunk = io.read_nonblock(READ_CHUNK_BYTES)
177
+ append_output_chunk(streams.fetch(io), chunk, state, max_output_bytes, truncate_output, pid)
178
+ rescue IO::WaitReadable
179
+ next
180
+ rescue EOFError
181
+ streams.delete(io)
182
+ io.close
183
+ end
184
+ end
185
+
186
+ true
187
+ end
188
+
189
+ def drain_streams_until(streams, drain_deadline, max_output_bytes, truncate_output, state, pid)
190
+ while streams.any? && ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) < drain_deadline
191
+ break unless read_available_streams(streams, max_output_bytes, truncate_output, state, pid)
192
+ end
193
+ end
194
+
195
+ def close_streams(streams)
196
+ streams.keys.each do |io|
197
+ streams.delete(io)
198
+ io.close unless io.closed?
199
+ rescue IOError
200
+ end
201
+ end
202
+
203
+ def append_output_chunk(
204
+ buffer,
205
+ chunk,
206
+ state,
207
+ max_output_bytes,
208
+ truncate_output,
209
+ pid,
210
+ output_too_large_error: Landlock::OutputTooLargeError
211
+ )
212
+ return buffer << chunk if max_output_bytes.nil?
213
+
214
+ chunk_to_append = chunk
215
+ over_limit = false
216
+ remaining_bytes = max_output_bytes - state[:bytes]
217
+ if remaining_bytes <= 0
218
+ chunk_to_append = ""
219
+ over_limit = true
220
+ elsif chunk.bytesize > remaining_bytes
221
+ chunk_to_append = chunk.byteslice(0, remaining_bytes)
222
+ over_limit = true
223
+ end
224
+
225
+ state[:bytes] += chunk.bytesize
226
+ state[:truncated] = true if over_limit
227
+ buffer << chunk_to_append
228
+ return unless over_limit
229
+
230
+ terminate_process(pid)
231
+ raise output_too_large_error, "Process output exceeded #{max_output_bytes} bytes" unless truncate_output
232
+ end
233
+
234
+ def terminate_process(pid)
235
+ signal_process("TERM", pid)
236
+ sleep 0.5
237
+ signal_process("KILL", pid)
238
+ end
239
+
240
+ def signal_process(signal, pid)
241
+ ::Process.kill(signal, -pid)
242
+ rescue Errno::ESRCH, Errno::EPERM
243
+ begin
244
+ ::Process.kill(signal, pid)
245
+ rescue Errno::ESRCH, Errno::EPERM
246
+ end
247
+ end
248
+ end
249
+ end