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