landlock 0.1.0 → 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.
@@ -0,0 +1,167 @@
1
+ #ifndef RB_LANDLOCK_NATIVE_H
2
+ #define RB_LANDLOCK_NATIVE_H
3
+
4
+ #include <errno.h>
5
+ #include <fcntl.h>
6
+ #include <stddef.h>
7
+ #include <stdint.h>
8
+ #include <unistd.h>
9
+
10
+ #ifdef __linux__
11
+ #include <sys/prctl.h>
12
+ #include <sys/syscall.h>
13
+ #ifdef HAVE_LINUX_LANDLOCK_H
14
+ #include <linux/landlock.h>
15
+ #endif
16
+ #endif
17
+
18
+ #ifndef SYS_landlock_create_ruleset
19
+ # if defined(__linux__) && defined(__NR_landlock_create_ruleset) && defined(__NR_landlock_add_rule) && defined(__NR_landlock_restrict_self)
20
+ # define SYS_landlock_create_ruleset __NR_landlock_create_ruleset
21
+ # define SYS_landlock_add_rule __NR_landlock_add_rule
22
+ # define SYS_landlock_restrict_self __NR_landlock_restrict_self
23
+ # elif defined(__linux__) && defined(__x86_64__) && defined(__ILP32__)
24
+ # ifndef __X32_SYSCALL_BIT
25
+ # define __X32_SYSCALL_BIT 0x40000000
26
+ # endif
27
+ # define SYS_landlock_create_ruleset (__X32_SYSCALL_BIT + 444)
28
+ # define SYS_landlock_add_rule (__X32_SYSCALL_BIT + 445)
29
+ # define SYS_landlock_restrict_self (__X32_SYSCALL_BIT + 446)
30
+ # elif defined(__linux__) && (defined(__x86_64__) || defined(__aarch64__) || defined(__i386__))
31
+ # define SYS_landlock_create_ruleset 444
32
+ # define SYS_landlock_add_rule 445
33
+ # define SYS_landlock_restrict_self 446
34
+ # endif
35
+ #endif
36
+
37
+ #ifndef LANDLOCK_CREATE_RULESET_VERSION
38
+ #define LANDLOCK_CREATE_RULESET_VERSION (1U << 0)
39
+ #endif
40
+
41
+ #ifndef LANDLOCK_RULE_PATH_BENEATH
42
+ #define LANDLOCK_RULE_PATH_BENEATH 1
43
+ #endif
44
+
45
+ #ifndef LANDLOCK_RULE_NET_PORT
46
+ #define LANDLOCK_RULE_NET_PORT 2
47
+ #endif
48
+
49
+ #ifndef LANDLOCK_ACCESS_FS_EXECUTE
50
+ #define LANDLOCK_ACCESS_FS_EXECUTE (1ULL << 0)
51
+ #endif
52
+ #ifndef LANDLOCK_ACCESS_FS_WRITE_FILE
53
+ #define LANDLOCK_ACCESS_FS_WRITE_FILE (1ULL << 1)
54
+ #endif
55
+ #ifndef LANDLOCK_ACCESS_FS_READ_FILE
56
+ #define LANDLOCK_ACCESS_FS_READ_FILE (1ULL << 2)
57
+ #endif
58
+ #ifndef LANDLOCK_ACCESS_FS_READ_DIR
59
+ #define LANDLOCK_ACCESS_FS_READ_DIR (1ULL << 3)
60
+ #endif
61
+ #ifndef LANDLOCK_ACCESS_FS_REMOVE_DIR
62
+ #define LANDLOCK_ACCESS_FS_REMOVE_DIR (1ULL << 4)
63
+ #endif
64
+ #ifndef LANDLOCK_ACCESS_FS_REMOVE_FILE
65
+ #define LANDLOCK_ACCESS_FS_REMOVE_FILE (1ULL << 5)
66
+ #endif
67
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_CHAR
68
+ #define LANDLOCK_ACCESS_FS_MAKE_CHAR (1ULL << 6)
69
+ #endif
70
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_DIR
71
+ #define LANDLOCK_ACCESS_FS_MAKE_DIR (1ULL << 7)
72
+ #endif
73
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_REG
74
+ #define LANDLOCK_ACCESS_FS_MAKE_REG (1ULL << 8)
75
+ #endif
76
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_SOCK
77
+ #define LANDLOCK_ACCESS_FS_MAKE_SOCK (1ULL << 9)
78
+ #endif
79
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_FIFO
80
+ #define LANDLOCK_ACCESS_FS_MAKE_FIFO (1ULL << 10)
81
+ #endif
82
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_BLOCK
83
+ #define LANDLOCK_ACCESS_FS_MAKE_BLOCK (1ULL << 11)
84
+ #endif
85
+ #ifndef LANDLOCK_ACCESS_FS_MAKE_SYM
86
+ #define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12)
87
+ #endif
88
+ #ifndef LANDLOCK_ACCESS_FS_REFER
89
+ #define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
90
+ #endif
91
+ #ifndef LANDLOCK_ACCESS_FS_TRUNCATE
92
+ #define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
93
+ #endif
94
+ #ifndef LANDLOCK_ACCESS_FS_IOCTL_DEV
95
+ #define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15)
96
+ #endif
97
+
98
+ #ifndef LANDLOCK_ACCESS_NET_BIND_TCP
99
+ #define LANDLOCK_ACCESS_NET_BIND_TCP (1ULL << 0)
100
+ #endif
101
+ #ifndef LANDLOCK_ACCESS_NET_CONNECT_TCP
102
+ #define LANDLOCK_ACCESS_NET_CONNECT_TCP (1ULL << 1)
103
+ #endif
104
+
105
+ #ifndef LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
106
+ #define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)
107
+ #endif
108
+ #ifndef LANDLOCK_SCOPE_SIGNAL
109
+ #define LANDLOCK_SCOPE_SIGNAL (1ULL << 1)
110
+ #endif
111
+
112
+ #ifndef O_PATH
113
+ #define O_PATH 010000000
114
+ #endif
115
+
116
+ #ifndef O_CLOEXEC
117
+ #define O_CLOEXEC 02000000
118
+ #endif
119
+
120
+ #ifndef PR_SET_NO_NEW_PRIVS
121
+ #define PR_SET_NO_NEW_PRIVS 38
122
+ #endif
123
+
124
+ struct rb_landlock_ruleset_attr {
125
+ uint64_t handled_access_fs;
126
+ uint64_t handled_access_net;
127
+ uint64_t scoped;
128
+ };
129
+
130
+ struct rb_landlock_path_beneath_attr {
131
+ uint64_t allowed_access;
132
+ int32_t parent_fd;
133
+ } __attribute__((packed));
134
+
135
+ struct rb_landlock_net_port_attr {
136
+ uint64_t allowed_access;
137
+ uint64_t port;
138
+ };
139
+
140
+ static long ll_create_ruleset(const void *attr, size_t size, uint32_t flags) {
141
+ #ifdef SYS_landlock_create_ruleset
142
+ return syscall(SYS_landlock_create_ruleset, attr, size, flags);
143
+ #else
144
+ errno = ENOSYS;
145
+ return -1;
146
+ #endif
147
+ }
148
+
149
+ static long ll_add_rule(int ruleset_fd, int rule_type, const void *rule_attr, uint32_t flags) {
150
+ #ifdef SYS_landlock_add_rule
151
+ return syscall(SYS_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
152
+ #else
153
+ errno = ENOSYS;
154
+ return -1;
155
+ #endif
156
+ }
157
+
158
+ static long ll_restrict_self(int ruleset_fd, uint32_t flags) {
159
+ #ifdef SYS_landlock_restrict_self
160
+ return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
161
+ #else
162
+ errno = ENOSYS;
163
+ return -1;
164
+ #endif
165
+ }
166
+
167
+ #endif
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Landlock
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2"
5
5
  end