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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +141 -9
- data/benchmark/landlock_overhead.rb +212 -0
- data/ext/landlock/bin/safe_exec_helper.c +369 -0
- data/ext/landlock/extconf.rb +30 -0
- data/ext/landlock/landlock.c +25 -153
- data/ext/landlock/landlock_native.h +167 -0
- data/lib/landlock/safe_exec.rb +522 -0
- data/lib/landlock/version.rb +1 -1
- data/lib/landlock.rb +103 -34
- metadata +6 -1
data/lib/landlock.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "landlock/version"
|
|
4
4
|
require_relative "landlock/landlock"
|
|
5
|
+
require_relative "landlock/safe_exec"
|
|
5
6
|
|
|
6
7
|
module Landlock
|
|
7
8
|
class Error < StandardError; end
|
|
@@ -41,12 +42,18 @@ module Landlock
|
|
|
41
42
|
connect_tcp: ACCESS_NET_CONNECT_TCP
|
|
42
43
|
}.freeze
|
|
43
44
|
|
|
45
|
+
SCOPE_FLAGS = {
|
|
46
|
+
abstract_unix_socket: SCOPE_ABSTRACT_UNIX_SOCKET,
|
|
47
|
+
signal: SCOPE_SIGNAL
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
44
50
|
READ_RIGHTS = %i[read_file read_dir].freeze
|
|
45
51
|
EXEC_RIGHTS = %i[execute read_file read_dir].freeze
|
|
46
52
|
WRITE_RIGHTS = %i[
|
|
47
53
|
read_file read_dir write_file truncate remove_dir remove_file make_char
|
|
48
54
|
make_dir make_reg make_sock make_fifo make_block make_sym refer
|
|
49
55
|
].freeze
|
|
56
|
+
FILE_PATH_RIGHTS = %i[execute write_file read_file truncate ioctl_dev].freeze
|
|
50
57
|
|
|
51
58
|
module_function
|
|
52
59
|
|
|
@@ -56,18 +63,19 @@ module Landlock
|
|
|
56
63
|
false
|
|
57
64
|
end
|
|
58
65
|
|
|
59
|
-
def restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], allow_all_known: false)
|
|
66
|
+
def restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], allow_all_known: false)
|
|
60
67
|
abi = abi_version
|
|
61
68
|
raise UnsupportedError, "Linux Landlock is unavailable" unless abi.positive?
|
|
62
69
|
|
|
63
70
|
fs_handled = allow_all_known ? _fs_rights_for_abi(abi) : _handled_fs_for(read:, write:, execute:, paths:, abi:)
|
|
64
71
|
net_handled = _handled_net_for(connect_tcp:, bind_tcp:, abi:)
|
|
72
|
+
scoped = _scope_for(scope:, abi:)
|
|
65
73
|
|
|
66
|
-
if fs_handled.zero? && net_handled.zero?
|
|
67
|
-
raise ArgumentError, "empty Landlock policy: provide filesystem paths
|
|
74
|
+
if fs_handled.zero? && net_handled.zero? && scoped.zero?
|
|
75
|
+
raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
|
|
68
76
|
end
|
|
69
77
|
|
|
70
|
-
fd = _create_ruleset(fs_handled, net_handled)
|
|
78
|
+
fd = _create_ruleset(fs_handled, net_handled, scoped)
|
|
71
79
|
begin
|
|
72
80
|
add_path_rules(fd, read, READ_RIGHTS, abi)
|
|
73
81
|
add_path_rules(fd, execute, EXEC_RIGHTS, abi)
|
|
@@ -75,7 +83,10 @@ module Landlock
|
|
|
75
83
|
|
|
76
84
|
paths.each do |rule|
|
|
77
85
|
path, rights = normalize_path_rule(rule)
|
|
78
|
-
|
|
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)
|
|
79
90
|
end
|
|
80
91
|
|
|
81
92
|
add_net_rules(fd, connect_tcp, [:connect_tcp], abi)
|
|
@@ -89,20 +100,19 @@ module Landlock
|
|
|
89
100
|
true
|
|
90
101
|
end
|
|
91
102
|
|
|
92
|
-
def exec(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true)
|
|
93
|
-
argv =
|
|
94
|
-
|
|
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!
|
|
95
106
|
|
|
96
107
|
pid = fork do
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Kernel.exec(*argv, close_others: close_others)
|
|
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)
|
|
106
116
|
end
|
|
107
117
|
end
|
|
108
118
|
|
|
@@ -110,35 +120,85 @@ module Landlock
|
|
|
110
120
|
status
|
|
111
121
|
end
|
|
112
122
|
|
|
113
|
-
def spawn(argv,
|
|
114
|
-
argv =
|
|
115
|
-
|
|
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!
|
|
116
126
|
|
|
117
127
|
fork do
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
read
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
136
|
+
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
|
+
|
|
157
|
+
if env
|
|
158
|
+
[env, *argv_for_exec(argv), exec_options]
|
|
159
|
+
else
|
|
160
|
+
[*argv_for_exec(argv), exec_options]
|
|
129
161
|
end
|
|
130
162
|
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
|
|
131
181
|
|
|
132
182
|
def add_path_rules(fd, paths, rights, abi)
|
|
133
|
-
Array(paths).each
|
|
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
|
|
134
190
|
end
|
|
135
191
|
private_class_method :add_path_rules
|
|
136
192
|
|
|
137
193
|
def add_net_rules(fd, ports, rights, abi)
|
|
138
|
-
|
|
194
|
+
ports = Array(ports)
|
|
195
|
+
return if ports.empty?
|
|
139
196
|
raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
|
|
140
197
|
|
|
141
|
-
|
|
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) }
|
|
142
202
|
end
|
|
143
203
|
private_class_method :add_net_rules
|
|
144
204
|
|
|
@@ -192,4 +252,13 @@ module Landlock
|
|
|
192
252
|
bits
|
|
193
253
|
end
|
|
194
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
|
|
195
264
|
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.2'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
@@ -74,11 +74,16 @@ extensions:
|
|
|
74
74
|
- ext/landlock/extconf.rb
|
|
75
75
|
extra_rdoc_files: []
|
|
76
76
|
files:
|
|
77
|
+
- CHANGELOG.md
|
|
77
78
|
- LICENSE.txt
|
|
78
79
|
- README.md
|
|
80
|
+
- benchmark/landlock_overhead.rb
|
|
81
|
+
- ext/landlock/bin/safe_exec_helper.c
|
|
79
82
|
- ext/landlock/extconf.rb
|
|
80
83
|
- ext/landlock/landlock.c
|
|
84
|
+
- ext/landlock/landlock_native.h
|
|
81
85
|
- lib/landlock.rb
|
|
86
|
+
- lib/landlock/safe_exec.rb
|
|
82
87
|
- lib/landlock/version.rb
|
|
83
88
|
homepage: https://github.com/discourse/ruby-landlock
|
|
84
89
|
licenses:
|