landlock 0.1.1 → 0.2
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 +8 -0
- data/README.md +72 -0
- data/ext/landlock/bin/safe_exec_helper.c +369 -0
- data/ext/landlock/extconf.rb +30 -0
- data/ext/landlock/landlock.c +6 -164
- data/ext/landlock/landlock_native.h +167 -0
- data/lib/landlock/safe_exec.rb +522 -0
- data/lib/landlock/version.rb +1 -1
- data/lib/landlock.rb +1 -0
- metadata +4 -1
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require_relative "landlock"
|
|
7
|
+
|
|
8
|
+
module Landlock
|
|
9
|
+
class SafeExec
|
|
10
|
+
Error = Class.new(StandardError)
|
|
11
|
+
OutputTooLargeError = Class.new(Error)
|
|
12
|
+
|
|
13
|
+
class CommandError < Error
|
|
14
|
+
attr_reader :stdout, :stderr, :status, :result
|
|
15
|
+
|
|
16
|
+
def initialize(message, stdout: "", stderr: "", status: nil, result: nil)
|
|
17
|
+
@stdout = stdout
|
|
18
|
+
@stderr = stderr
|
|
19
|
+
@status = status
|
|
20
|
+
@result = result
|
|
21
|
+
super(message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Result
|
|
26
|
+
attr_reader :stdout, :stderr, :status
|
|
27
|
+
|
|
28
|
+
def initialize(stdout:, stderr:, status:, output_truncated: false, timed_out: false)
|
|
29
|
+
@stdout = stdout
|
|
30
|
+
@stderr = stderr
|
|
31
|
+
@status = status
|
|
32
|
+
@output_truncated = output_truncated
|
|
33
|
+
@timed_out = timed_out
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def success?
|
|
37
|
+
!timed_out? && status&.success?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output_truncated?
|
|
41
|
+
@output_truncated
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def timed_out?
|
|
45
|
+
@timed_out
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_ary
|
|
49
|
+
[stdout, stderr, status]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
stdout.to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect
|
|
57
|
+
"#<#{self.class} status=#{status.inspect} timed_out=#{timed_out?} output_truncated=#{output_truncated?} stdout=#{stdout.inspect} stderr=#{stderr.inspect}>"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
DEFAULT_READ_PATHS = %w[/bin /etc /lib /lib64 /usr].freeze
|
|
62
|
+
DEFAULT_EXECUTE_PATHS = %w[/bin /lib /lib64 /usr].freeze
|
|
63
|
+
READ_CHUNK_BYTES = 16 * 1024
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
def capture(*command, **options)
|
|
67
|
+
perform_capture(*command, raise_on_failure: false, **options)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def capture!(*command, **options)
|
|
71
|
+
perform_capture(*command, raise_on_failure: true, **options)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def perform_capture(
|
|
75
|
+
*command,
|
|
76
|
+
read: [],
|
|
77
|
+
write: [],
|
|
78
|
+
execute: [],
|
|
79
|
+
timeout: nil,
|
|
80
|
+
failure_message: "",
|
|
81
|
+
success_status_codes: [0],
|
|
82
|
+
env: {},
|
|
83
|
+
inherit_env: false,
|
|
84
|
+
chdir: nil,
|
|
85
|
+
stdin: nil,
|
|
86
|
+
connect_tcp: nil,
|
|
87
|
+
bind_tcp: [],
|
|
88
|
+
rlimits: {},
|
|
89
|
+
seccomp_deny_network: false,
|
|
90
|
+
max_output_bytes: nil,
|
|
91
|
+
truncate_output: false,
|
|
92
|
+
allow_all_known: true,
|
|
93
|
+
raise_on_failure:
|
|
94
|
+
)
|
|
95
|
+
validate_sandbox_option_values!(connect_tcp: connect_tcp, bind_tcp: bind_tcp)
|
|
96
|
+
|
|
97
|
+
unsupported_options = unsupported_sandbox_options(
|
|
98
|
+
read: read,
|
|
99
|
+
write: write,
|
|
100
|
+
execute: execute,
|
|
101
|
+
connect_tcp: connect_tcp,
|
|
102
|
+
bind_tcp: bind_tcp,
|
|
103
|
+
seccomp_deny_network: seccomp_deny_network
|
|
104
|
+
)
|
|
105
|
+
use_helper = helper_available?
|
|
106
|
+
warn_unsupported_platform_once(unsupported_options) if !use_helper && unsupported_options.any?
|
|
107
|
+
|
|
108
|
+
stdout, stderr, status, output_truncated, timed_out = if use_helper
|
|
109
|
+
max_output_bytes = validate_output_limit!(max_output_bytes)
|
|
110
|
+
capture_process(
|
|
111
|
+
command,
|
|
112
|
+
read: read,
|
|
113
|
+
write: write,
|
|
114
|
+
execute: execute,
|
|
115
|
+
timeout: timeout,
|
|
116
|
+
env: env,
|
|
117
|
+
inherit_env: inherit_env,
|
|
118
|
+
chdir: chdir,
|
|
119
|
+
stdin: stdin,
|
|
120
|
+
connect_tcp: connect_tcp,
|
|
121
|
+
bind_tcp: bind_tcp,
|
|
122
|
+
rlimits: rlimits,
|
|
123
|
+
seccomp_deny_network: seccomp_deny_network,
|
|
124
|
+
max_output_bytes: max_output_bytes,
|
|
125
|
+
truncate_output: truncate_output,
|
|
126
|
+
allow_all_known: allow_all_known
|
|
127
|
+
)
|
|
128
|
+
else
|
|
129
|
+
max_output_bytes = validate_output_limit!(max_output_bytes)
|
|
130
|
+
capture_process_without_helper(
|
|
131
|
+
command,
|
|
132
|
+
timeout: timeout,
|
|
133
|
+
env: env,
|
|
134
|
+
inherit_env: inherit_env,
|
|
135
|
+
chdir: chdir,
|
|
136
|
+
stdin: stdin,
|
|
137
|
+
rlimits: rlimits,
|
|
138
|
+
max_output_bytes: max_output_bytes,
|
|
139
|
+
truncate_output: truncate_output
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
result = Result.new(stdout: stdout, stderr: stderr, status: status, output_truncated: output_truncated, timed_out: timed_out)
|
|
144
|
+
|
|
145
|
+
if raise_on_failure && (!status.exited? || !success_status_codes.include?(status.exitstatus))
|
|
146
|
+
message = [command.join(" "), failure_message, stderr].filter { |part| part.to_s != "" }.join("\n")
|
|
147
|
+
raise CommandError.new(message, stdout: stdout, stderr: stderr, status: status, result: result)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
result
|
|
151
|
+
rescue OutputTooLargeError => e
|
|
152
|
+
message = [command.join(" "), failure_message, e.message].filter { |part| part.to_s != "" }.join("\n")
|
|
153
|
+
raise CommandError.new(message)
|
|
154
|
+
end
|
|
155
|
+
private :perform_capture
|
|
156
|
+
|
|
157
|
+
def supported?
|
|
158
|
+
helper_available? && Landlock.supported?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def sandboxing?
|
|
162
|
+
supported?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def helper_path
|
|
166
|
+
candidates = [
|
|
167
|
+
File.expand_path("landlock-safe-exec", __dir__),
|
|
168
|
+
File.expand_path("../../tmp/#{RbConfig::CONFIG.fetch("arch")}/landlock/#{RUBY_VERSION}/landlock-safe-exec", __dir__),
|
|
169
|
+
File.expand_path("../../ext/landlock/landlock-safe-exec", __dir__)
|
|
170
|
+
]
|
|
171
|
+
candidates.find { |path| File.executable?(path) } || candidates.first
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def default_read_paths
|
|
175
|
+
existing_paths(DEFAULT_READ_PATHS)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def default_execute_paths
|
|
179
|
+
existing_paths(DEFAULT_EXECUTE_PATHS)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def existing_paths(paths)
|
|
183
|
+
Array(paths).filter { |path| path.to_s != "" && File.exist?(path) }.uniq
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def helper_available?
|
|
189
|
+
RUBY_PLATFORM.include?("linux") && File.executable?(helper_path)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def validate_sandbox_option_values!(connect_tcp:, bind_tcp:)
|
|
193
|
+
normalized_ports(connect_tcp, :connect_tcp) if !connect_tcp.nil?
|
|
194
|
+
normalized_ports(bind_tcp, :bind_tcp)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def unsupported_sandbox_options(read:, write:, execute:, connect_tcp:, bind_tcp:, seccomp_deny_network:)
|
|
198
|
+
options = []
|
|
199
|
+
options << :read if Array(read).any?
|
|
200
|
+
options << :write if Array(write).any?
|
|
201
|
+
options << :execute if Array(execute).any?
|
|
202
|
+
options << :connect_tcp if !connect_tcp.nil?
|
|
203
|
+
options << :bind_tcp if Array(bind_tcp).any?
|
|
204
|
+
options << :seccomp_deny_network if seccomp_deny_network
|
|
205
|
+
options
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def warn_unsupported_platform_once(options)
|
|
209
|
+
return if @warned_unsupported_sandbox
|
|
210
|
+
|
|
211
|
+
@warned_unsupported_sandbox = true
|
|
212
|
+
warn(
|
|
213
|
+
"Landlock::SafeExec sandbox options #{options.join(", ")} are unavailable without the Linux " \
|
|
214
|
+
"landlock-safe-exec helper; running command as a pass-through with those restrictions ignored"
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def validate_output_limit!(max_output_bytes)
|
|
219
|
+
return if max_output_bytes.nil?
|
|
220
|
+
|
|
221
|
+
Integer(max_output_bytes).tap do |value|
|
|
222
|
+
raise ArgumentError, "max_output_bytes must be non-negative" if value.negative?
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def capture_process_without_helper(
|
|
227
|
+
command,
|
|
228
|
+
timeout:,
|
|
229
|
+
env:,
|
|
230
|
+
inherit_env:,
|
|
231
|
+
chdir:,
|
|
232
|
+
stdin:,
|
|
233
|
+
rlimits:,
|
|
234
|
+
max_output_bytes:,
|
|
235
|
+
truncate_output:
|
|
236
|
+
)
|
|
237
|
+
argv = normalize_command(command)
|
|
238
|
+
spawn_options = fallback_spawn_options(
|
|
239
|
+
inherit_env: inherit_env,
|
|
240
|
+
chdir: chdir,
|
|
241
|
+
rlimits: rlimits
|
|
242
|
+
)
|
|
243
|
+
popen_args = [env || {}, *argv, spawn_options]
|
|
244
|
+
|
|
245
|
+
output_state = { bytes: 0, truncated: false }
|
|
246
|
+
output_mutex = Mutex.new
|
|
247
|
+
stdout = stderr = status = nil
|
|
248
|
+
timed_out = false
|
|
249
|
+
|
|
250
|
+
Open3.popen3(*popen_args) do |stdin_io, stdout_io, stderr_io, wait_thread|
|
|
251
|
+
stdin_thread = write_process_input(stdin_io, stdin)
|
|
252
|
+
stdout_thread = Thread.new do
|
|
253
|
+
Thread.current.report_on_exception = false
|
|
254
|
+
read_process_output(stdout_io, max_output_bytes, truncate_output, output_state, output_mutex, wait_thread.pid)
|
|
255
|
+
end
|
|
256
|
+
stderr_thread = Thread.new do
|
|
257
|
+
Thread.current.report_on_exception = false
|
|
258
|
+
read_process_output(stderr_io, max_output_bytes, truncate_output, output_state, output_mutex, wait_thread.pid)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
status, timed_out = wait_for_process(wait_thread, timeout)
|
|
262
|
+
stdin_thread&.value
|
|
263
|
+
stdout = stdout_thread.value
|
|
264
|
+
stderr = stderr_thread.value
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
[stdout, stderr, status, output_state[:truncated], timed_out]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def fallback_spawn_options(inherit_env:, chdir:, rlimits:)
|
|
271
|
+
options = { close_others: true, pgroup: true }
|
|
272
|
+
options[:unsetenv_others] = true if !inherit_env
|
|
273
|
+
options[:chdir] = chdir if chdir
|
|
274
|
+
options.merge!(rlimit_spawn_options(rlimits))
|
|
275
|
+
options
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def rlimit_spawn_options(rlimits)
|
|
279
|
+
normalized_rlimits(rlimits).to_h do |key, value|
|
|
280
|
+
[rlimit_spawn_key(key), [value, value]]
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def rlimit_spawn_key(name)
|
|
285
|
+
case name
|
|
286
|
+
when :cpu_seconds
|
|
287
|
+
:rlimit_cpu
|
|
288
|
+
when :memory_bytes
|
|
289
|
+
:rlimit_as
|
|
290
|
+
when :file_size_bytes
|
|
291
|
+
:rlimit_fsize
|
|
292
|
+
when :open_files
|
|
293
|
+
:rlimit_nofile
|
|
294
|
+
when :processes
|
|
295
|
+
:rlimit_nproc
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def normalize_command(command)
|
|
300
|
+
raise ArgumentError, "command must not be empty" if command.empty?
|
|
301
|
+
|
|
302
|
+
command.map(&:to_s)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def capture_process(
|
|
306
|
+
command,
|
|
307
|
+
read:,
|
|
308
|
+
write:,
|
|
309
|
+
execute:,
|
|
310
|
+
timeout:,
|
|
311
|
+
env:,
|
|
312
|
+
inherit_env:,
|
|
313
|
+
chdir:,
|
|
314
|
+
stdin:,
|
|
315
|
+
connect_tcp:,
|
|
316
|
+
bind_tcp:,
|
|
317
|
+
rlimits:,
|
|
318
|
+
seccomp_deny_network:,
|
|
319
|
+
max_output_bytes:,
|
|
320
|
+
truncate_output:,
|
|
321
|
+
allow_all_known:
|
|
322
|
+
)
|
|
323
|
+
argv = helper_argv(
|
|
324
|
+
command,
|
|
325
|
+
read: read,
|
|
326
|
+
write: write,
|
|
327
|
+
execute: execute,
|
|
328
|
+
env: env,
|
|
329
|
+
inherit_env: inherit_env,
|
|
330
|
+
chdir: chdir,
|
|
331
|
+
connect_tcp: connect_tcp,
|
|
332
|
+
bind_tcp: bind_tcp,
|
|
333
|
+
rlimits: rlimits,
|
|
334
|
+
seccomp_deny_network: seccomp_deny_network,
|
|
335
|
+
allow_all_known: allow_all_known
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
output_state = { bytes: 0, truncated: false }
|
|
339
|
+
output_mutex = Mutex.new
|
|
340
|
+
stdout = stderr = status = nil
|
|
341
|
+
timed_out = false
|
|
342
|
+
|
|
343
|
+
Open3.popen3(*argv, pgroup: true) do |stdin_io, stdout_io, stderr_io, wait_thread|
|
|
344
|
+
stdin_thread = write_process_input(stdin_io, stdin)
|
|
345
|
+
stdout_thread = Thread.new do
|
|
346
|
+
Thread.current.report_on_exception = false
|
|
347
|
+
read_process_output(stdout_io, max_output_bytes, truncate_output, output_state, output_mutex, wait_thread.pid)
|
|
348
|
+
end
|
|
349
|
+
stderr_thread = Thread.new do
|
|
350
|
+
Thread.current.report_on_exception = false
|
|
351
|
+
read_process_output(stderr_io, max_output_bytes, truncate_output, output_state, output_mutex, wait_thread.pid)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
status, timed_out = wait_for_process(wait_thread, timeout)
|
|
355
|
+
stdin_thread&.value
|
|
356
|
+
stdout = stdout_thread.value
|
|
357
|
+
stderr = stderr_thread.value
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
[stdout, stderr, status, output_state[:truncated], timed_out]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def helper_argv(
|
|
364
|
+
command,
|
|
365
|
+
read:,
|
|
366
|
+
write:,
|
|
367
|
+
execute:,
|
|
368
|
+
env:,
|
|
369
|
+
inherit_env:,
|
|
370
|
+
chdir:,
|
|
371
|
+
connect_tcp:,
|
|
372
|
+
bind_tcp:,
|
|
373
|
+
rlimits:,
|
|
374
|
+
seccomp_deny_network:,
|
|
375
|
+
allow_all_known:
|
|
376
|
+
)
|
|
377
|
+
normalize_command(command)
|
|
378
|
+
read_paths = validate_existing_paths(read, :read)
|
|
379
|
+
write_paths = validate_existing_paths(write, :write)
|
|
380
|
+
execute_paths = validate_existing_paths(execute, :execute)
|
|
381
|
+
filesystem_policy_requested = read_paths.any? || write_paths.any? || execute_paths.any?
|
|
382
|
+
|
|
383
|
+
argv = [helper_path]
|
|
384
|
+
read_paths.each { |path| argv << "--read" << path }
|
|
385
|
+
write_paths.each { |path| argv << "--write" << path }
|
|
386
|
+
execute_paths.each { |path| argv << "--execute" << path }
|
|
387
|
+
sandbox_connect_tcp_ports(connect_tcp).each { |port| argv << "--connect-tcp" << port.to_s }
|
|
388
|
+
normalized_ports(bind_tcp, :bind_tcp).each { |port| argv << "--bind-tcp" << port.to_s }
|
|
389
|
+
argv << "--chdir" << chdir if chdir
|
|
390
|
+
Array(env).each { |key, value| argv << "--env" << "#{key}=#{value}" }
|
|
391
|
+
argv << "--unsetenv-others" if !inherit_env
|
|
392
|
+
normalized_rlimits(rlimits).each { |key, value| argv << "--rlimit" << "#{key}=#{value}" }
|
|
393
|
+
argv << "--seccomp-deny-network" if seccomp_deny_network
|
|
394
|
+
argv << "--allow-all-known" if allow_all_known && filesystem_policy_requested
|
|
395
|
+
argv << "--"
|
|
396
|
+
argv.concat(command.map(&:to_s))
|
|
397
|
+
argv
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def sandbox_connect_tcp_ports(connect_tcp)
|
|
401
|
+
return normalized_ports(connect_tcp, :connect_tcp) if !connect_tcp.nil?
|
|
402
|
+
return [] if !Landlock.supported? || Landlock.abi_version < 4
|
|
403
|
+
|
|
404
|
+
[0]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def normalized_ports(ports, name)
|
|
408
|
+
Array(ports).map do |port|
|
|
409
|
+
integer = Integer(port)
|
|
410
|
+
raise ArgumentError, "#{name} port must be between 0 and 65535" if integer.negative? || integer > 65_535
|
|
411
|
+
|
|
412
|
+
integer
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def validate_existing_paths(paths, name)
|
|
417
|
+
Array(paths).map do |path|
|
|
418
|
+
string = path.to_s
|
|
419
|
+
raise ArgumentError, "#{name} path must not be empty" if string.empty?
|
|
420
|
+
raise ArgumentError, "#{name} path does not exist: #{string}" if !File.exist?(string)
|
|
421
|
+
|
|
422
|
+
string
|
|
423
|
+
end.uniq
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def normalized_rlimits(rlimits)
|
|
427
|
+
Array(rlimits).filter_map do |name, value|
|
|
428
|
+
next if value.nil?
|
|
429
|
+
|
|
430
|
+
key = name.to_sym
|
|
431
|
+
unless %i[cpu_seconds memory_bytes file_size_bytes open_files processes].include?(key)
|
|
432
|
+
raise ArgumentError, "Unknown rlimit: #{name}"
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
value = Integer(value)
|
|
436
|
+
raise ArgumentError, "rlimit #{name} must be non-negative" if value.negative?
|
|
437
|
+
|
|
438
|
+
[key, value]
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def wait_for_process(wait_thread, timeout)
|
|
443
|
+
if timeout
|
|
444
|
+
[Timeout.timeout(timeout) { wait_thread.value }, false]
|
|
445
|
+
else
|
|
446
|
+
[wait_thread.value, false]
|
|
447
|
+
end
|
|
448
|
+
rescue Timeout::Error
|
|
449
|
+
terminate_process(wait_thread.pid)
|
|
450
|
+
[wait_thread.value, true]
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def write_process_input(io, input)
|
|
454
|
+
return io.close if input.nil?
|
|
455
|
+
|
|
456
|
+
Thread.new do
|
|
457
|
+
Thread.current.report_on_exception = false
|
|
458
|
+
begin
|
|
459
|
+
if input.respond_to?(:read)
|
|
460
|
+
while (chunk = input.read(READ_CHUNK_BYTES))
|
|
461
|
+
io.write(chunk)
|
|
462
|
+
end
|
|
463
|
+
else
|
|
464
|
+
io.write(input.to_s)
|
|
465
|
+
end
|
|
466
|
+
rescue Errno::EPIPE, IOError
|
|
467
|
+
ensure
|
|
468
|
+
io.close unless io.closed?
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def read_process_output(io, max_output_bytes, truncate_output, output_state, output_mutex, pid)
|
|
474
|
+
return io.read if max_output_bytes.nil?
|
|
475
|
+
|
|
476
|
+
output = +""
|
|
477
|
+
while (chunk = io.read(READ_CHUNK_BYTES))
|
|
478
|
+
chunk_to_append = chunk
|
|
479
|
+
over_limit = false
|
|
480
|
+
|
|
481
|
+
output_mutex.synchronize do
|
|
482
|
+
remaining_bytes = max_output_bytes - output_state[:bytes]
|
|
483
|
+
if remaining_bytes <= 0
|
|
484
|
+
chunk_to_append = ""
|
|
485
|
+
over_limit = true
|
|
486
|
+
elsif chunk.bytesize > remaining_bytes
|
|
487
|
+
chunk_to_append = chunk.byteslice(0, remaining_bytes)
|
|
488
|
+
over_limit = true
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
output_state[:bytes] += chunk.bytesize
|
|
492
|
+
output_state[:truncated] = true if over_limit
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
output << chunk_to_append
|
|
496
|
+
if over_limit
|
|
497
|
+
terminate_process(pid)
|
|
498
|
+
raise OutputTooLargeError, "Process output exceeded #{max_output_bytes} bytes" if !truncate_output
|
|
499
|
+
|
|
500
|
+
break
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
output
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def terminate_process(pid)
|
|
507
|
+
signal_process("TERM", pid)
|
|
508
|
+
sleep 0.5
|
|
509
|
+
signal_process("KILL", pid)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def signal_process(signal, pid)
|
|
513
|
+
Process.kill(signal, -pid)
|
|
514
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
515
|
+
begin
|
|
516
|
+
Process.kill(signal, pid)
|
|
517
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
data/lib/landlock/version.rb
CHANGED
data/lib/landlock.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: landlock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: '0.2'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
@@ -78,9 +78,12 @@ files:
|
|
|
78
78
|
- LICENSE.txt
|
|
79
79
|
- README.md
|
|
80
80
|
- benchmark/landlock_overhead.rb
|
|
81
|
+
- ext/landlock/bin/safe_exec_helper.c
|
|
81
82
|
- ext/landlock/extconf.rb
|
|
82
83
|
- ext/landlock/landlock.c
|
|
84
|
+
- ext/landlock/landlock_native.h
|
|
83
85
|
- lib/landlock.rb
|
|
86
|
+
- lib/landlock/safe_exec.rb
|
|
84
87
|
- lib/landlock/version.rb
|
|
85
88
|
homepage: https://github.com/discourse/ruby-landlock
|
|
86
89
|
licenses:
|