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.
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/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 Error < StandardError; end
8
- class UnsupportedError < Error; end
9
-
10
- class SyscallError < Error
11
- attr_reader :errno, :syscall
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
- def supported?
60
- abi_version.positive?
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
- fd = _create_ruleset(fs_handled, net_handled, scoped)
78
- begin
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
- true
100
- end
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
- _, status = Process.wait2(pid)
119
- status
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
- if env
157
- [env, *argv_for_exec(argv), exec_options]
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.1.1
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:
@@ -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: