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
data/lib/landlock.rb
CHANGED
|
@@ -1,263 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "landlock/version"
|
|
4
|
-
require_relative "landlock/
|
|
4
|
+
require_relative "landlock/errors"
|
|
5
|
+
require_relative "landlock/native"
|
|
6
|
+
require_relative "landlock/result"
|
|
7
|
+
require_relative "landlock/rights"
|
|
8
|
+
require_relative "landlock/validation"
|
|
9
|
+
require_relative "landlock/env"
|
|
10
|
+
require_relative "landlock/rlimits"
|
|
11
|
+
require_relative "landlock/process_io"
|
|
12
|
+
require_relative "landlock/policy"
|
|
13
|
+
require_relative "landlock/execution"
|
|
5
14
|
|
|
6
15
|
module Landlock
|
|
7
|
-
class
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def initialize(syscall, errno, message = nil)
|
|
14
|
-
@syscall = syscall
|
|
15
|
-
@errno = errno
|
|
16
|
-
super(message || "#{syscall} failed: #{errno}")
|
|
16
|
+
class << self
|
|
17
|
+
def supported?
|
|
18
|
+
abi_version.positive?
|
|
19
|
+
rescue Error
|
|
20
|
+
false
|
|
17
21
|
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
FS_RIGHTS = {
|
|
21
|
-
execute: ACCESS_FS_EXECUTE,
|
|
22
|
-
write_file: ACCESS_FS_WRITE_FILE,
|
|
23
|
-
read_file: ACCESS_FS_READ_FILE,
|
|
24
|
-
read_dir: ACCESS_FS_READ_DIR,
|
|
25
|
-
remove_dir: ACCESS_FS_REMOVE_DIR,
|
|
26
|
-
remove_file: ACCESS_FS_REMOVE_FILE,
|
|
27
|
-
make_char: ACCESS_FS_MAKE_CHAR,
|
|
28
|
-
make_dir: ACCESS_FS_MAKE_DIR,
|
|
29
|
-
make_reg: ACCESS_FS_MAKE_REG,
|
|
30
|
-
make_sock: ACCESS_FS_MAKE_SOCK,
|
|
31
|
-
make_fifo: ACCESS_FS_MAKE_FIFO,
|
|
32
|
-
make_block: ACCESS_FS_MAKE_BLOCK,
|
|
33
|
-
make_sym: ACCESS_FS_MAKE_SYM,
|
|
34
|
-
refer: ACCESS_FS_REFER,
|
|
35
|
-
truncate: ACCESS_FS_TRUNCATE,
|
|
36
|
-
ioctl_dev: ACCESS_FS_IOCTL_DEV
|
|
37
|
-
}.freeze
|
|
38
|
-
|
|
39
|
-
NET_RIGHTS = {
|
|
40
|
-
bind_tcp: ACCESS_NET_BIND_TCP,
|
|
41
|
-
connect_tcp: ACCESS_NET_CONNECT_TCP
|
|
42
|
-
}.freeze
|
|
43
|
-
|
|
44
|
-
SCOPE_FLAGS = {
|
|
45
|
-
abstract_unix_socket: SCOPE_ABSTRACT_UNIX_SOCKET,
|
|
46
|
-
signal: SCOPE_SIGNAL
|
|
47
|
-
}.freeze
|
|
48
|
-
|
|
49
|
-
READ_RIGHTS = %i[read_file read_dir].freeze
|
|
50
|
-
EXEC_RIGHTS = %i[execute read_file read_dir].freeze
|
|
51
|
-
WRITE_RIGHTS = %i[
|
|
52
|
-
read_file read_dir write_file truncate remove_dir remove_file make_char
|
|
53
|
-
make_dir make_reg make_sock make_fifo make_block make_sym refer
|
|
54
|
-
].freeze
|
|
55
|
-
FILE_PATH_RIGHTS = %i[execute write_file read_file truncate ioctl_dev].freeze
|
|
56
|
-
|
|
57
|
-
module_function
|
|
58
22
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
rescue Error
|
|
62
|
-
false
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], allow_all_known: false)
|
|
66
|
-
abi = abi_version
|
|
67
|
-
raise UnsupportedError, "Linux Landlock is unavailable" unless abi.positive?
|
|
68
|
-
|
|
69
|
-
fs_handled = allow_all_known ? _fs_rights_for_abi(abi) : _handled_fs_for(read:, write:, execute:, paths:, abi:)
|
|
70
|
-
net_handled = _handled_net_for(connect_tcp:, bind_tcp:, abi:)
|
|
71
|
-
scoped = _scope_for(scope:, abi:)
|
|
72
|
-
|
|
73
|
-
if fs_handled.zero? && net_handled.zero? && scoped.zero?
|
|
74
|
-
raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
|
|
23
|
+
def restrict!(...)
|
|
24
|
+
Policy.restrict!(...)
|
|
75
25
|
end
|
|
76
26
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
add_path_rules(fd, read, READ_RIGHTS, abi)
|
|
80
|
-
add_path_rules(fd, execute, EXEC_RIGHTS, abi)
|
|
81
|
-
add_path_rules(fd, write, WRITE_RIGHTS, abi)
|
|
82
|
-
|
|
83
|
-
paths.each do |rule|
|
|
84
|
-
path, rights = normalize_path_rule(rule)
|
|
85
|
-
access_mask = mask(rights, FS_RIGHTS, abi)
|
|
86
|
-
next if access_mask.zero?
|
|
87
|
-
|
|
88
|
-
_add_path_rule(fd, File.expand_path(path), access_mask)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
add_net_rules(fd, connect_tcp, [:connect_tcp], abi)
|
|
92
|
-
add_net_rules(fd, bind_tcp, [:bind_tcp], abi)
|
|
93
|
-
|
|
94
|
-
_restrict_self(fd)
|
|
95
|
-
ensure
|
|
96
|
-
_close_fd(fd) if fd && fd >= 0
|
|
27
|
+
def exec(...)
|
|
28
|
+
Execution.exec(...)
|
|
97
29
|
end
|
|
98
30
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def exec(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false)
|
|
103
|
-
argv = normalize_argv(argv)
|
|
104
|
-
ensure_landlock_supported!
|
|
105
|
-
|
|
106
|
-
pid = fork do
|
|
107
|
-
begin
|
|
108
|
-
# Safe after fork: this runs only in the child process before exec.
|
|
109
|
-
Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir
|
|
110
|
-
restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
111
|
-
|
|
112
|
-
Kernel.exec(*kernel_exec_args(argv, env, unsetenv_others:, close_others:))
|
|
113
|
-
rescue Exception => error
|
|
114
|
-
exit_child!(error)
|
|
115
|
-
end
|
|
31
|
+
def spawn(...)
|
|
32
|
+
Execution.spawn(...)
|
|
116
33
|
end
|
|
117
34
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def spawn(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false)
|
|
123
|
-
argv = normalize_argv(argv)
|
|
124
|
-
ensure_landlock_supported!
|
|
125
|
-
|
|
126
|
-
fork do
|
|
127
|
-
begin
|
|
128
|
-
# Safe after fork: this runs only in the child process before exec.
|
|
129
|
-
Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir
|
|
130
|
-
restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
131
|
-
Kernel.exec(*kernel_exec_args(argv, env, unsetenv_others:, close_others:))
|
|
132
|
-
rescue Exception => error
|
|
133
|
-
exit_child!(error)
|
|
134
|
-
end
|
|
35
|
+
def capture(...)
|
|
36
|
+
Execution.capture(...)
|
|
135
37
|
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def normalize_argv(argv)
|
|
139
|
-
raise ArgumentError, "argv must be an Array of command arguments" unless argv.is_a?(Array)
|
|
140
|
-
raise ArgumentError, "argv must not be empty" if argv.empty?
|
|
141
|
-
|
|
142
|
-
argv
|
|
143
|
-
end
|
|
144
|
-
private_class_method :normalize_argv
|
|
145
|
-
|
|
146
|
-
def argv_for_exec(argv)
|
|
147
|
-
command = argv.fetch(0)
|
|
148
|
-
[[command, command], *argv.drop(1)]
|
|
149
|
-
end
|
|
150
|
-
private_class_method :argv_for_exec
|
|
151
|
-
|
|
152
|
-
def kernel_exec_args(argv, env, unsetenv_others:, close_others:)
|
|
153
|
-
exec_options = { close_others: close_others }
|
|
154
|
-
exec_options[:unsetenv_others] = true if unsetenv_others
|
|
155
38
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
else
|
|
159
|
-
[*argv_for_exec(argv), exec_options]
|
|
39
|
+
def capture!(...)
|
|
40
|
+
Execution.capture!(...)
|
|
160
41
|
end
|
|
161
42
|
end
|
|
162
|
-
private_class_method :kernel_exec_args
|
|
163
|
-
|
|
164
|
-
def ensure_landlock_supported!
|
|
165
|
-
raise UnsupportedError, "Linux Landlock is unavailable" unless abi_version.positive?
|
|
166
|
-
end
|
|
167
|
-
private_class_method :ensure_landlock_supported!
|
|
168
|
-
|
|
169
|
-
def exit_child!(error)
|
|
170
|
-
warn "Landlock child failed before exec: #{error.class}: #{error.message}"
|
|
171
|
-
ensure
|
|
172
|
-
exit! 127
|
|
173
|
-
end
|
|
174
|
-
private_class_method :exit_child!
|
|
175
|
-
|
|
176
|
-
def path_rights(path, rights)
|
|
177
|
-
File.directory?(path) ? rights : Array(rights) & FILE_PATH_RIGHTS
|
|
178
|
-
end
|
|
179
|
-
private_class_method :path_rights
|
|
180
|
-
|
|
181
|
-
def add_path_rules(fd, paths, rights, abi)
|
|
182
|
-
Array(paths).each do |path|
|
|
183
|
-
expanded_path = File.expand_path(path)
|
|
184
|
-
access_mask = mask(path_rights(expanded_path, rights), FS_RIGHTS, abi)
|
|
185
|
-
next if access_mask.zero?
|
|
186
|
-
|
|
187
|
-
_add_path_rule(fd, expanded_path, access_mask)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
private_class_method :add_path_rules
|
|
191
|
-
|
|
192
|
-
def add_net_rules(fd, ports, rights, abi)
|
|
193
|
-
ports = Array(ports)
|
|
194
|
-
return if ports.empty?
|
|
195
|
-
raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
|
|
196
|
-
|
|
197
|
-
access_mask = mask(rights, NET_RIGHTS, abi)
|
|
198
|
-
return if access_mask.zero?
|
|
199
|
-
|
|
200
|
-
ports.each { |port| _add_net_rule(fd, Integer(port), access_mask) }
|
|
201
|
-
end
|
|
202
|
-
private_class_method :add_net_rules
|
|
203
|
-
|
|
204
|
-
def normalize_path_rule(rule)
|
|
205
|
-
case rule
|
|
206
|
-
when Hash
|
|
207
|
-
[rule.fetch(:path), Array(rule.fetch(:rights))]
|
|
208
|
-
when Array
|
|
209
|
-
[rule.fetch(0), Array(rule.fetch(1))]
|
|
210
|
-
else
|
|
211
|
-
raise ArgumentError, "path rule must be {path:, rights:} or [path, rights]"
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
private_class_method :normalize_path_rule
|
|
215
|
-
|
|
216
|
-
def mask(names, table, abi)
|
|
217
|
-
Array(names).reduce(0) do |bits, name|
|
|
218
|
-
bit = table.fetch(name.to_sym) { raise ArgumentError, "unknown Landlock right: #{name.inspect}" }
|
|
219
|
-
next bits if bit == ACCESS_FS_REFER && abi < 2
|
|
220
|
-
next bits if bit == ACCESS_FS_TRUNCATE && abi < 3
|
|
221
|
-
next bits if bit == ACCESS_FS_IOCTL_DEV && abi < 5
|
|
222
|
-
bits | bit
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
private_class_method :mask
|
|
226
|
-
|
|
227
|
-
def _fs_rights_for_abi(abi)
|
|
228
|
-
rights = FS_RIGHTS.values.reduce(0, :|)
|
|
229
|
-
rights &= ~ACCESS_FS_REFER if abi < 2
|
|
230
|
-
rights &= ~ACCESS_FS_TRUNCATE if abi < 3
|
|
231
|
-
rights &= ~ACCESS_FS_IOCTL_DEV if abi < 5
|
|
232
|
-
rights
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def _handled_fs_for(read:, write:, execute:, paths:, abi:)
|
|
236
|
-
bits = 0
|
|
237
|
-
bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless Array(read).empty?
|
|
238
|
-
bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless Array(execute).empty?
|
|
239
|
-
bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless Array(write).empty?
|
|
240
|
-
Array(paths).each { |rule| bits |= mask(normalize_path_rule(rule).last, FS_RIGHTS, abi) }
|
|
241
|
-
bits
|
|
242
|
-
end
|
|
243
|
-
private_class_method :_handled_fs_for
|
|
244
|
-
|
|
245
|
-
def _handled_net_for(connect_tcp:, bind_tcp:, abi:)
|
|
246
|
-
bits = 0
|
|
247
|
-
bits |= ACCESS_NET_CONNECT_TCP unless Array(connect_tcp).empty?
|
|
248
|
-
bits |= ACCESS_NET_BIND_TCP unless Array(bind_tcp).empty?
|
|
249
|
-
return 0 if bits.zero?
|
|
250
|
-
raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
|
|
251
|
-
bits
|
|
252
|
-
end
|
|
253
|
-
private_class_method :_handled_net_for
|
|
254
|
-
|
|
255
|
-
def _scope_for(scope:, abi:)
|
|
256
|
-
bits = mask(scope, SCOPE_FLAGS, abi)
|
|
257
|
-
return 0 if bits.zero?
|
|
258
|
-
raise UnsupportedError, "Landlock scopes require ABI v6+; running ABI v#{abi}" if abi < 6
|
|
259
|
-
|
|
260
|
-
bits
|
|
261
|
-
end
|
|
262
|
-
private_class_method :_scope_for
|
|
263
43
|
end
|
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.3'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
@@ -15,56 +15,84 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '5.
|
|
18
|
+
version: '5.27'
|
|
19
19
|
type: :development
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '5.
|
|
25
|
+
version: '5.27'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rake
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '13.
|
|
32
|
+
version: '13.4'
|
|
33
33
|
type: :development
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '13.
|
|
39
|
+
version: '13.4'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: rake-compiler
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1.
|
|
46
|
+
version: '1.3'
|
|
47
47
|
type: :development
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.
|
|
53
|
+
version: '1.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop-capybara
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: rubocop-discourse
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
57
71
|
requirements:
|
|
58
72
|
- - "~>"
|
|
59
73
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '3.
|
|
74
|
+
version: '3.18'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.18'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: syntax_tree
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '6.3'
|
|
61
89
|
type: :development
|
|
62
90
|
prerelease: false
|
|
63
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
92
|
requirements:
|
|
65
93
|
- - "~>"
|
|
66
94
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '3
|
|
95
|
+
version: '6.3'
|
|
68
96
|
description: Native Ruby wrappers for Linux Landlock with filesystem and TCP port
|
|
69
97
|
restrictions for safe subprocess execution.
|
|
70
98
|
email:
|
|
@@ -78,9 +106,25 @@ files:
|
|
|
78
106
|
- LICENSE.txt
|
|
79
107
|
- README.md
|
|
80
108
|
- benchmark/landlock_overhead.rb
|
|
109
|
+
- ext/landlock/bin/safe_exec_helper.c
|
|
81
110
|
- ext/landlock/extconf.rb
|
|
82
111
|
- ext/landlock/landlock.c
|
|
112
|
+
- ext/landlock/landlock_native.h
|
|
113
|
+
- ext/landlock/seccomp_deny_network.h
|
|
83
114
|
- lib/landlock.rb
|
|
115
|
+
- lib/landlock/env.rb
|
|
116
|
+
- lib/landlock/errors.rb
|
|
117
|
+
- lib/landlock/execution.rb
|
|
118
|
+
- lib/landlock/native.rb
|
|
119
|
+
- lib/landlock/policy.rb
|
|
120
|
+
- lib/landlock/process_io.rb
|
|
121
|
+
- lib/landlock/result.rb
|
|
122
|
+
- lib/landlock/rights.rb
|
|
123
|
+
- lib/landlock/rlimits.rb
|
|
124
|
+
- lib/landlock/runner.rb
|
|
125
|
+
- lib/landlock/runner/fork.rb
|
|
126
|
+
- lib/landlock/runner/native.rb
|
|
127
|
+
- lib/landlock/validation.rb
|
|
84
128
|
- lib/landlock/version.rb
|
|
85
129
|
homepage: https://github.com/discourse/ruby-landlock
|
|
86
130
|
licenses:
|