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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +77 -6
- data/benchmark/landlock_overhead.rb +9 -30
- data/ext/landlock/bin/safe_exec_helper.c +602 -0
- data/ext/landlock/extconf.rb +33 -0
- data/ext/landlock/landlock.c +40 -174
- data/ext/landlock/landlock_native.h +168 -0
- data/ext/landlock/seccomp_deny_network.h +176 -0
- data/lib/landlock/env.rb +31 -0
- data/lib/landlock/errors.rb +32 -0
- data/lib/landlock/execution.rb +238 -0
- data/lib/landlock/native.rb +38 -0
- data/lib/landlock/policy.rb +161 -0
- data/lib/landlock/process_io.rb +249 -0
- data/lib/landlock/result.rb +43 -0
- data/lib/landlock/rights.rb +48 -0
- data/lib/landlock/rlimits.rb +40 -0
- data/lib/landlock/runner/fork.rb +171 -0
- data/lib/landlock/runner/native.rb +225 -0
- data/lib/landlock/runner.rb +28 -0
- data/lib/landlock/validation.rb +59 -0
- data/lib/landlock/version.rb +1 -1
- data/lib/landlock.rb +25 -245
- metadata +53 -9
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Landlock
|
|
4
|
+
module ResultBehavior
|
|
5
|
+
attr_reader :stdout, :stderr, :status
|
|
6
|
+
|
|
7
|
+
def success?
|
|
8
|
+
!timed_out? && status&.success?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def output_truncated?
|
|
12
|
+
@output_truncated
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def timed_out?
|
|
16
|
+
@timed_out
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_ary
|
|
20
|
+
[stdout, stderr, status]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_s
|
|
24
|
+
stdout.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inspect
|
|
28
|
+
"#<#{self.class} status=#{status.inspect} timed_out=#{timed_out?} output_truncated=#{output_truncated?} stdout=#{stdout.inspect} stderr=#{stderr.inspect}>"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class CaptureResult
|
|
33
|
+
include ResultBehavior
|
|
34
|
+
|
|
35
|
+
def initialize(stdout:, stderr:, status:, output_truncated: false, timed_out: false)
|
|
36
|
+
@stdout = stdout
|
|
37
|
+
@stderr = stderr
|
|
38
|
+
@status = status
|
|
39
|
+
@output_truncated = output_truncated
|
|
40
|
+
@timed_out = timed_out
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "native"
|
|
4
|
+
|
|
5
|
+
module Landlock
|
|
6
|
+
FS_RIGHTS = {
|
|
7
|
+
execute: ACCESS_FS_EXECUTE,
|
|
8
|
+
write_file: ACCESS_FS_WRITE_FILE,
|
|
9
|
+
read_file: ACCESS_FS_READ_FILE,
|
|
10
|
+
read_dir: ACCESS_FS_READ_DIR,
|
|
11
|
+
remove_dir: ACCESS_FS_REMOVE_DIR,
|
|
12
|
+
remove_file: ACCESS_FS_REMOVE_FILE,
|
|
13
|
+
make_char: ACCESS_FS_MAKE_CHAR,
|
|
14
|
+
make_dir: ACCESS_FS_MAKE_DIR,
|
|
15
|
+
make_reg: ACCESS_FS_MAKE_REG,
|
|
16
|
+
make_sock: ACCESS_FS_MAKE_SOCK,
|
|
17
|
+
make_fifo: ACCESS_FS_MAKE_FIFO,
|
|
18
|
+
make_block: ACCESS_FS_MAKE_BLOCK,
|
|
19
|
+
make_sym: ACCESS_FS_MAKE_SYM,
|
|
20
|
+
refer: ACCESS_FS_REFER,
|
|
21
|
+
truncate: ACCESS_FS_TRUNCATE,
|
|
22
|
+
ioctl_dev: ACCESS_FS_IOCTL_DEV
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
NET_RIGHTS = { bind_tcp: ACCESS_NET_BIND_TCP, connect_tcp: ACCESS_NET_CONNECT_TCP }.freeze
|
|
26
|
+
|
|
27
|
+
SCOPE_FLAGS = { abstract_unix_socket: SCOPE_ABSTRACT_UNIX_SOCKET, signal: SCOPE_SIGNAL }.freeze
|
|
28
|
+
|
|
29
|
+
READ_RIGHTS = %i[read_file read_dir].freeze
|
|
30
|
+
EXEC_RIGHTS = %i[execute read_file read_dir].freeze
|
|
31
|
+
WRITE_RIGHTS = %i[
|
|
32
|
+
read_file
|
|
33
|
+
read_dir
|
|
34
|
+
write_file
|
|
35
|
+
truncate
|
|
36
|
+
remove_dir
|
|
37
|
+
remove_file
|
|
38
|
+
make_char
|
|
39
|
+
make_dir
|
|
40
|
+
make_reg
|
|
41
|
+
make_sock
|
|
42
|
+
make_fifo
|
|
43
|
+
make_block
|
|
44
|
+
make_sym
|
|
45
|
+
refer
|
|
46
|
+
].freeze
|
|
47
|
+
FILE_PATH_RIGHTS = %i[execute write_file read_file truncate ioctl_dev].freeze
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Landlock
|
|
4
|
+
module Rlimits
|
|
5
|
+
VALID_NAMES = %i[cpu_seconds memory_bytes file_size_bytes open_files processes].freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def normalize(rlimits)
|
|
10
|
+
Array(rlimits).filter_map do |name, value|
|
|
11
|
+
next if value.nil?
|
|
12
|
+
|
|
13
|
+
key = name.to_sym
|
|
14
|
+
raise ArgumentError, "Unknown rlimit: #{name}" if !VALID_NAMES.include?(key)
|
|
15
|
+
|
|
16
|
+
value = Integer(value)
|
|
17
|
+
raise ArgumentError, "rlimit #{name} must be non-negative" if value.negative?
|
|
18
|
+
|
|
19
|
+
[key, value]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def apply!(rlimits)
|
|
24
|
+
rlimits.each do |key, value|
|
|
25
|
+
case key
|
|
26
|
+
when :cpu_seconds
|
|
27
|
+
::Process.setrlimit(:CPU, value, value)
|
|
28
|
+
when :memory_bytes
|
|
29
|
+
::Process.setrlimit(:AS, value, value)
|
|
30
|
+
when :file_size_bytes
|
|
31
|
+
::Process.setrlimit(:FSIZE, value, value)
|
|
32
|
+
when :open_files
|
|
33
|
+
::Process.setrlimit(:NOFILE, value, value)
|
|
34
|
+
when :processes
|
|
35
|
+
::Process.setrlimit(:NPROC, value, value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../native"
|
|
5
|
+
require_relative "../policy"
|
|
6
|
+
require_relative "../process_io"
|
|
7
|
+
require_relative "../rlimits"
|
|
8
|
+
|
|
9
|
+
module Landlock
|
|
10
|
+
module Runner
|
|
11
|
+
module Fork
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def spawn(
|
|
15
|
+
argv,
|
|
16
|
+
read:,
|
|
17
|
+
write:,
|
|
18
|
+
execute:,
|
|
19
|
+
connect_tcp:,
|
|
20
|
+
bind_tcp:,
|
|
21
|
+
paths:,
|
|
22
|
+
scope:,
|
|
23
|
+
chdir:,
|
|
24
|
+
env:,
|
|
25
|
+
unsetenv_others:,
|
|
26
|
+
close_others:,
|
|
27
|
+
allow_all_known:
|
|
28
|
+
)
|
|
29
|
+
fork do
|
|
30
|
+
begin
|
|
31
|
+
setup_child!(
|
|
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
|
+
rlimits: [],
|
|
46
|
+
seccomp_deny_network: false
|
|
47
|
+
)
|
|
48
|
+
rescue Exception => error
|
|
49
|
+
Runner.exit_child!(error)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call(
|
|
55
|
+
argv,
|
|
56
|
+
read:,
|
|
57
|
+
write:,
|
|
58
|
+
execute:,
|
|
59
|
+
connect_tcp:,
|
|
60
|
+
bind_tcp:,
|
|
61
|
+
paths:,
|
|
62
|
+
scope:,
|
|
63
|
+
chdir:,
|
|
64
|
+
env:,
|
|
65
|
+
unsetenv_others:,
|
|
66
|
+
close_others:,
|
|
67
|
+
allow_all_known:,
|
|
68
|
+
timeout:,
|
|
69
|
+
stdin:,
|
|
70
|
+
rlimits:,
|
|
71
|
+
seccomp_deny_network:,
|
|
72
|
+
max_output_bytes:,
|
|
73
|
+
truncate_output:
|
|
74
|
+
)
|
|
75
|
+
stdout_reader, stdout_writer = IO.pipe
|
|
76
|
+
stderr_reader, stderr_writer = IO.pipe
|
|
77
|
+
stdin_reader, stdin_writer = IO.pipe
|
|
78
|
+
|
|
79
|
+
pid =
|
|
80
|
+
fork do
|
|
81
|
+
begin
|
|
82
|
+
stdout_reader.close
|
|
83
|
+
stderr_reader.close
|
|
84
|
+
stdin_writer.close
|
|
85
|
+
::Process.setpgrp
|
|
86
|
+
STDIN.reopen(stdin_reader)
|
|
87
|
+
STDOUT.reopen(stdout_writer)
|
|
88
|
+
STDERR.reopen(stderr_writer)
|
|
89
|
+
stdin_reader.close
|
|
90
|
+
stdout_writer.close
|
|
91
|
+
stderr_writer.close
|
|
92
|
+
|
|
93
|
+
setup_child!(
|
|
94
|
+
argv,
|
|
95
|
+
read:,
|
|
96
|
+
write:,
|
|
97
|
+
execute:,
|
|
98
|
+
connect_tcp:,
|
|
99
|
+
bind_tcp:,
|
|
100
|
+
paths:,
|
|
101
|
+
scope:,
|
|
102
|
+
chdir:,
|
|
103
|
+
env:,
|
|
104
|
+
unsetenv_others:,
|
|
105
|
+
close_others:,
|
|
106
|
+
allow_all_known:,
|
|
107
|
+
rlimits:,
|
|
108
|
+
seccomp_deny_network:
|
|
109
|
+
)
|
|
110
|
+
rescue Exception => error
|
|
111
|
+
Runner.exit_child!(error)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
stdin_reader.close
|
|
116
|
+
stdout_writer.close
|
|
117
|
+
stderr_writer.close
|
|
118
|
+
|
|
119
|
+
ProcessIO.complete_pipe_capture(
|
|
120
|
+
pid,
|
|
121
|
+
stdout_reader,
|
|
122
|
+
stderr_reader,
|
|
123
|
+
stdin_writer,
|
|
124
|
+
stdin,
|
|
125
|
+
timeout,
|
|
126
|
+
max_output_bytes,
|
|
127
|
+
truncate_output
|
|
128
|
+
)
|
|
129
|
+
rescue OutputTooLargeError
|
|
130
|
+
raise
|
|
131
|
+
rescue Exception
|
|
132
|
+
if pid
|
|
133
|
+
ProcessIO.terminate_process(pid)
|
|
134
|
+
ProcessIO.wait_for_pid(pid)
|
|
135
|
+
end
|
|
136
|
+
raise
|
|
137
|
+
ensure
|
|
138
|
+
[stdin_reader, stdin_writer, stdout_reader, stdout_writer, stderr_reader, stderr_writer].each do |io|
|
|
139
|
+
io&.close unless io.closed?
|
|
140
|
+
rescue IOError
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def setup_child!(
|
|
145
|
+
argv,
|
|
146
|
+
read:,
|
|
147
|
+
write:,
|
|
148
|
+
execute:,
|
|
149
|
+
connect_tcp:,
|
|
150
|
+
bind_tcp:,
|
|
151
|
+
paths:,
|
|
152
|
+
scope:,
|
|
153
|
+
chdir:,
|
|
154
|
+
env:,
|
|
155
|
+
unsetenv_others:,
|
|
156
|
+
close_others:,
|
|
157
|
+
allow_all_known:,
|
|
158
|
+
rlimits:,
|
|
159
|
+
seccomp_deny_network:
|
|
160
|
+
)
|
|
161
|
+
Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir
|
|
162
|
+
if Policy.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
163
|
+
Landlock.restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
164
|
+
end
|
|
165
|
+
Landlock::Native.seccomp_deny_network! if seccomp_deny_network
|
|
166
|
+
Rlimits.apply!(rlimits)
|
|
167
|
+
Kernel.exec(*Runner.kernel_exec_args(argv, env, unsetenv_others:, close_others:))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
require_relative "../policy"
|
|
6
|
+
require_relative "../process_io"
|
|
7
|
+
|
|
8
|
+
module Landlock
|
|
9
|
+
module Runner
|
|
10
|
+
module Native
|
|
11
|
+
# Starts the native helper with the sandbox policy encoded as argv flags.
|
|
12
|
+
# The helper applies the policy, then execs the target command so the
|
|
13
|
+
# long-lived child is not a forked Ruby process.
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def available?
|
|
17
|
+
File.executable?(helper_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def helper_path
|
|
21
|
+
candidates = [
|
|
22
|
+
File.expand_path("../landlock-safe-exec", __dir__),
|
|
23
|
+
File.expand_path(
|
|
24
|
+
"../../../tmp/#{RbConfig::CONFIG.fetch("arch")}/landlock/#{RUBY_VERSION}/landlock-safe-exec",
|
|
25
|
+
__dir__
|
|
26
|
+
),
|
|
27
|
+
File.expand_path("../../../ext/landlock/landlock-safe-exec", __dir__)
|
|
28
|
+
]
|
|
29
|
+
candidates.find { |path| File.executable?(path) } || candidates.first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def spawn(
|
|
33
|
+
argv,
|
|
34
|
+
read:,
|
|
35
|
+
write:,
|
|
36
|
+
execute:,
|
|
37
|
+
connect_tcp:,
|
|
38
|
+
bind_tcp:,
|
|
39
|
+
paths:,
|
|
40
|
+
scope:,
|
|
41
|
+
chdir:,
|
|
42
|
+
env:,
|
|
43
|
+
unsetenv_others:,
|
|
44
|
+
close_others:,
|
|
45
|
+
allow_all_known:
|
|
46
|
+
)
|
|
47
|
+
spawn_helper(
|
|
48
|
+
argv,
|
|
49
|
+
read:,
|
|
50
|
+
write:,
|
|
51
|
+
execute:,
|
|
52
|
+
connect_tcp:,
|
|
53
|
+
bind_tcp:,
|
|
54
|
+
paths:,
|
|
55
|
+
scope:,
|
|
56
|
+
chdir:,
|
|
57
|
+
env:,
|
|
58
|
+
unsetenv_others:,
|
|
59
|
+
close_others:,
|
|
60
|
+
allow_all_known:,
|
|
61
|
+
rlimits: [],
|
|
62
|
+
seccomp_deny_network: false
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def call(
|
|
67
|
+
argv,
|
|
68
|
+
read:,
|
|
69
|
+
write:,
|
|
70
|
+
execute:,
|
|
71
|
+
connect_tcp:,
|
|
72
|
+
bind_tcp:,
|
|
73
|
+
paths:,
|
|
74
|
+
scope:,
|
|
75
|
+
chdir:,
|
|
76
|
+
env:,
|
|
77
|
+
unsetenv_others:,
|
|
78
|
+
close_others:,
|
|
79
|
+
allow_all_known:,
|
|
80
|
+
timeout:,
|
|
81
|
+
stdin:,
|
|
82
|
+
rlimits:,
|
|
83
|
+
seccomp_deny_network:,
|
|
84
|
+
max_output_bytes:,
|
|
85
|
+
truncate_output:
|
|
86
|
+
)
|
|
87
|
+
stdout_reader, stdout_writer = IO.pipe
|
|
88
|
+
stderr_reader, stderr_writer = IO.pipe
|
|
89
|
+
stdin_reader, stdin_writer = IO.pipe
|
|
90
|
+
|
|
91
|
+
pid =
|
|
92
|
+
spawn_helper(
|
|
93
|
+
argv,
|
|
94
|
+
read:,
|
|
95
|
+
write:,
|
|
96
|
+
execute:,
|
|
97
|
+
connect_tcp:,
|
|
98
|
+
bind_tcp:,
|
|
99
|
+
paths:,
|
|
100
|
+
scope:,
|
|
101
|
+
chdir:,
|
|
102
|
+
env:,
|
|
103
|
+
unsetenv_others:,
|
|
104
|
+
close_others:,
|
|
105
|
+
allow_all_known:,
|
|
106
|
+
rlimits:,
|
|
107
|
+
seccomp_deny_network:,
|
|
108
|
+
stdin_reader:,
|
|
109
|
+
stdout_writer:,
|
|
110
|
+
stderr_writer:,
|
|
111
|
+
pgroup: true
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
stdin_reader.close
|
|
115
|
+
stdout_writer.close
|
|
116
|
+
stderr_writer.close
|
|
117
|
+
|
|
118
|
+
ProcessIO.complete_pipe_capture(
|
|
119
|
+
pid,
|
|
120
|
+
stdout_reader,
|
|
121
|
+
stderr_reader,
|
|
122
|
+
stdin_writer,
|
|
123
|
+
stdin,
|
|
124
|
+
timeout,
|
|
125
|
+
max_output_bytes,
|
|
126
|
+
truncate_output
|
|
127
|
+
)
|
|
128
|
+
rescue OutputTooLargeError
|
|
129
|
+
raise
|
|
130
|
+
rescue Exception
|
|
131
|
+
if pid
|
|
132
|
+
ProcessIO.terminate_process(pid)
|
|
133
|
+
ProcessIO.wait_for_pid(pid)
|
|
134
|
+
end
|
|
135
|
+
raise
|
|
136
|
+
ensure
|
|
137
|
+
[stdin_reader, stdin_writer, stdout_reader, stdout_writer, stderr_reader, stderr_writer].each do |io|
|
|
138
|
+
io&.close unless io.closed?
|
|
139
|
+
rescue IOError
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def spawn_helper(
|
|
144
|
+
argv,
|
|
145
|
+
read:,
|
|
146
|
+
write:,
|
|
147
|
+
execute:,
|
|
148
|
+
connect_tcp:,
|
|
149
|
+
bind_tcp:,
|
|
150
|
+
paths:,
|
|
151
|
+
scope:,
|
|
152
|
+
chdir:,
|
|
153
|
+
env:,
|
|
154
|
+
unsetenv_others:,
|
|
155
|
+
close_others:,
|
|
156
|
+
allow_all_known:,
|
|
157
|
+
rlimits:,
|
|
158
|
+
seccomp_deny_network:,
|
|
159
|
+
stdin_reader: nil,
|
|
160
|
+
stdout_writer: nil,
|
|
161
|
+
stderr_writer: nil,
|
|
162
|
+
pgroup: false
|
|
163
|
+
)
|
|
164
|
+
spawn_options = { close_others: }
|
|
165
|
+
spawn_options[:unsetenv_others] = true if unsetenv_others
|
|
166
|
+
spawn_options[:chdir] = chdir if chdir
|
|
167
|
+
spawn_options[:in] = stdin_reader if stdin_reader
|
|
168
|
+
spawn_options[:out] = stdout_writer if stdout_writer
|
|
169
|
+
spawn_options[:err] = stderr_writer if stderr_writer
|
|
170
|
+
spawn_options[:pgroup] = true if pgroup
|
|
171
|
+
|
|
172
|
+
spawn_args =
|
|
173
|
+
helper_argv(
|
|
174
|
+
argv,
|
|
175
|
+
read:,
|
|
176
|
+
write:,
|
|
177
|
+
execute:,
|
|
178
|
+
connect_tcp:,
|
|
179
|
+
bind_tcp:,
|
|
180
|
+
paths:,
|
|
181
|
+
scope:,
|
|
182
|
+
allow_all_known:,
|
|
183
|
+
rlimits:,
|
|
184
|
+
seccomp_deny_network:,
|
|
185
|
+
close_others:
|
|
186
|
+
)
|
|
187
|
+
env ? ::Process.spawn(env, *spawn_args, spawn_options) : ::Process.spawn(*spawn_args, spawn_options)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def helper_argv(
|
|
191
|
+
argv,
|
|
192
|
+
read:,
|
|
193
|
+
write:,
|
|
194
|
+
execute:,
|
|
195
|
+
connect_tcp:,
|
|
196
|
+
bind_tcp:,
|
|
197
|
+
paths:,
|
|
198
|
+
scope:,
|
|
199
|
+
allow_all_known:,
|
|
200
|
+
rlimits:,
|
|
201
|
+
seccomp_deny_network:,
|
|
202
|
+
close_others:
|
|
203
|
+
)
|
|
204
|
+
args = [helper_path]
|
|
205
|
+
Array(read).each { |path| args << "--read" << path.to_s }
|
|
206
|
+
Array(write).each { |path| args << "--write" << path.to_s }
|
|
207
|
+
Array(execute).each { |path| args << "--execute" << path.to_s }
|
|
208
|
+
Array(paths).each do |rule|
|
|
209
|
+
path, rights = Policy.normalize_path_rule(rule)
|
|
210
|
+
args << "--path" << path.to_s << Array(rights).map(&:to_s).join(",")
|
|
211
|
+
end
|
|
212
|
+
Array(connect_tcp).each { |port| args << "--connect-tcp" << port.to_s }
|
|
213
|
+
Array(bind_tcp).each { |port| args << "--bind-tcp" << port.to_s }
|
|
214
|
+
Array(scope).each { |name| args << "--scope" << name.to_s }
|
|
215
|
+
Array(rlimits).each { |key, value| args << "--rlimit" << "#{key}=#{value}" }
|
|
216
|
+
args << "--allow-all-known" if allow_all_known
|
|
217
|
+
args << "--seccomp-deny-network" if seccomp_deny_network
|
|
218
|
+
args << "--keep-fds" unless close_others
|
|
219
|
+
args << "--"
|
|
220
|
+
args.concat(argv.map(&:to_s))
|
|
221
|
+
args
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Landlock
|
|
4
|
+
module Runner
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def argv_for_exec(argv)
|
|
8
|
+
command = argv.fetch(0)
|
|
9
|
+
[[command, command], *argv.drop(1)]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def kernel_exec_args(argv, env, unsetenv_others:, close_others:)
|
|
13
|
+
exec_options = { close_others: }
|
|
14
|
+
exec_options[:unsetenv_others] = true if unsetenv_others
|
|
15
|
+
|
|
16
|
+
env ? [env, *argv_for_exec(argv), exec_options] : [*argv_for_exec(argv), exec_options]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def exit_child!(error)
|
|
20
|
+
warn "Landlock child failed before exec: #{error.class}: #{error.message}"
|
|
21
|
+
ensure
|
|
22
|
+
exit! 127
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require_relative "runner/fork"
|
|
28
|
+
require_relative "runner/native"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Landlock
|
|
4
|
+
module Validation
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def normalize_argv(argv)
|
|
8
|
+
raise ArgumentError, "argv must be an Array of command arguments" unless argv.is_a?(Array)
|
|
9
|
+
raise ArgumentError, "argv must not be empty" if argv.empty?
|
|
10
|
+
|
|
11
|
+
argv
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def normalize_ports(ports, name)
|
|
15
|
+
Array(ports).map do |port|
|
|
16
|
+
integer = Integer(port)
|
|
17
|
+
raise ArgumentError, "#{name} port must be between 0 and 65535" if integer.negative? || integer > 65_535
|
|
18
|
+
|
|
19
|
+
integer
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_existing_paths(paths, name, chdir: nil)
|
|
24
|
+
base = chdir ? File.expand_path(chdir) : Dir.pwd
|
|
25
|
+
Array(paths)
|
|
26
|
+
.map do |path|
|
|
27
|
+
validate_existing_path!(path, name, base)
|
|
28
|
+
path.to_s
|
|
29
|
+
end
|
|
30
|
+
.uniq
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_existing_path!(path, name, base)
|
|
34
|
+
string = path.to_s
|
|
35
|
+
raise ArgumentError, "#{name} path must not be empty" if string.empty?
|
|
36
|
+
|
|
37
|
+
expanded = File.expand_path(string, base)
|
|
38
|
+
raise ArgumentError, "#{name} path does not exist: #{string}" if !File.exist?(expanded)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_output_limit!(max_output_bytes)
|
|
42
|
+
return if max_output_bytes.nil?
|
|
43
|
+
|
|
44
|
+
Integer(max_output_bytes).tap do |value|
|
|
45
|
+
raise ArgumentError, "max_output_bytes must be non-negative" if value.negative?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_timeout!(timeout)
|
|
50
|
+
return if timeout.nil?
|
|
51
|
+
raise ArgumentError, "timeout must be numeric" unless timeout.is_a?(Numeric)
|
|
52
|
+
|
|
53
|
+
Float(timeout).tap do |value|
|
|
54
|
+
raise ArgumentError, "timeout must be finite" unless value.finite?
|
|
55
|
+
raise ArgumentError, "timeout must be non-negative" if value.negative?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/landlock/version.rb
CHANGED