landlock 0.3 → 0.4
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 +7 -0
- data/README.md +3 -1
- data/ext/landlock/bin/safe_exec_helper.c +34 -14
- data/lib/landlock/execution.rb +26 -26
- data/lib/landlock/policy.rb +17 -14
- data/lib/landlock/runner/native.rb +15 -5
- data/lib/landlock/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7494b5195627610f67bb7a41a5adcb99857f19e1639d88b1107d3d5d43a639c
|
|
4
|
+
data.tar.gz: '065496b9a75a046e91b1d9445dae704bd4531d7de61391d4506c9f480789a32a'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 310b8cf56b7906297466b1273d4c5e924dc859148bc0adf143d44825b2c2b68be07d67d7058def44f9551470c48880b37963db2eb48be991b4a07829a03a3e9e
|
|
7
|
+
data.tar.gz: 362a3ea8d6aaf5115d100adf06524a53cfd87d9a46cfc29f1677c351196fadbffee88fa80d348b433d8d518ad3e517ec3893b6b5a827f9e67b799c5954b093b7
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.4] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Breaking.** `read:`, `write:`, `execute:`, `connect_tcp:` and `bind_tcp:` treat an empty array as "block all" rather than "allow all". These were previously controlled by the length of each allowlist, so `read: []` turned off all read restrictions instead of denying everything. `nil` is now the "restrictions disabled" flag and the new default; `[]` means "restriction on, empty allowlist". Callers relying on `[]` to mean unrestricted must pass `nil`; callers passing populated arrays are unaffected.
|
|
12
|
+
- The native helper marks an enabled-but-empty restriction by passing the existing flag with an empty value, e.g. `--write ""`. An omitted flag leaves the restriction disabled.
|
|
13
|
+
|
|
7
14
|
## [0.3] - 2026-06-17
|
|
8
15
|
|
|
9
16
|
### Added
|
data/README.md
CHANGED
|
@@ -119,8 +119,10 @@ stdout, stderr, status = Landlock.capture(
|
|
|
119
119
|
Capture options:
|
|
120
120
|
|
|
121
121
|
- `read:`, `write:`, `execute:` — filesystem allowlists. Explicit paths must exist; missing paths raise `ArgumentError` instead of being silently ignored.
|
|
122
|
+
|
|
123
|
+
These distinguish `nil` from `[]`. Omitting one (or passing `nil`) leaves that access class outside the ruleset's handled mask, so Landlock does not restrict it **anywhere** — `write: nil` means the child can still create, delete and rename files across the filesystem. Passing `[]` enforces the class while granting no paths, which is what you want for "must not write at all". Prefer `[]` over `nil` unless you deliberately want the class unpoliced.
|
|
122
124
|
- `paths:` — exact path rules with explicit Landlock rights, e.g. `{ path:, rights: %i[read_file] }`.
|
|
123
|
-
- `connect_tcp:` and `bind_tcp:` — allowed TCP ports. TCP
|
|
125
|
+
- `connect_tcp:` and `bind_tcp:` — allowed TCP ports, with the same `nil` versus `[]` distinction. `[]` denies all TCP for that operation and requires ABI v4+; `nil` leaves TCP unrestricted.
|
|
124
126
|
- `scope:` — Landlock ABI v6+ scopes such as `:signal` and `:abstract_unix_socket`.
|
|
125
127
|
- `seccomp_deny_network:` — additionally deny common Linux network syscalls with seccomp. This is Linux-specific and intended as defense in depth.
|
|
126
128
|
- `rlimits:` — resource limits. Supported keys are `:cpu_seconds`, `:memory_bytes`, `:file_size_bytes`, `:open_files`, and `:processes`. Values must be non-negative integers.
|
|
@@ -97,6 +97,15 @@ static void path_rule_list_push(path_rule_list *list, char *path, uint64_t right
|
|
|
97
97
|
list->len++;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/* An empty value marks the class handled while granting nothing; a real path
|
|
101
|
+
or port is never the empty string, so the two cannot collide. */
|
|
102
|
+
static void push_unless_empty(string_list *list, char *value, int *handled) {
|
|
103
|
+
*handled = 1;
|
|
104
|
+
if (value[0] != '\0') {
|
|
105
|
+
string_list_push(list, value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
static unsigned long long parse_ull(const char *value, const char *name) {
|
|
101
110
|
if (!value || value[0] == '\0' || value[0] == '-' || value[0] == '+' ||
|
|
102
111
|
isspace((unsigned char)value[0])) {
|
|
@@ -121,6 +130,13 @@ static unsigned long long parse_port(const char *value) {
|
|
|
121
130
|
return port;
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
static void push_port_unless_empty(ull_list *list, const char *value, int *handled) {
|
|
134
|
+
*handled = 1;
|
|
135
|
+
if (value[0] != '\0') {
|
|
136
|
+
ull_list_push(list, parse_port(value));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
static int abi_version(void) {
|
|
125
141
|
long abi = ll_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
|
|
126
142
|
if (abi < 0 && (errno == ENOSYS || errno == EOPNOTSUPP)) {
|
|
@@ -239,10 +255,11 @@ static void add_net_rule(int fd, unsigned long long port, uint64_t rights) {
|
|
|
239
255
|
static void apply_landlock(string_list *read_paths, string_list *write_paths,
|
|
240
256
|
string_list *execute_paths, path_rule_list *path_rules,
|
|
241
257
|
ull_list *connect_ports, ull_list *bind_ports, uint64_t scoped,
|
|
242
|
-
int allow_all_known
|
|
243
|
-
|
|
258
|
+
int allow_all_known, int handled_read, int handled_write,
|
|
259
|
+
int handled_execute, int handled_connect, int handled_bind) {
|
|
260
|
+
int need_fs = handled_read || handled_write || handled_execute || path_rules->len ||
|
|
244
261
|
allow_all_known;
|
|
245
|
-
int need_net =
|
|
262
|
+
int need_net = handled_connect || handled_bind;
|
|
246
263
|
int need_scope = scoped != 0;
|
|
247
264
|
if (!need_fs && !need_net && !need_scope) {
|
|
248
265
|
return;
|
|
@@ -262,13 +279,13 @@ static void apply_landlock(string_list *read_paths, string_list *write_paths,
|
|
|
262
279
|
uint64_t known_fs_rights = known_fs_rights_for_abi(abi);
|
|
263
280
|
uint64_t fs_handled = allow_all_known ? known_fs_rights : 0;
|
|
264
281
|
if (!allow_all_known) {
|
|
265
|
-
if (
|
|
282
|
+
if (handled_read) {
|
|
266
283
|
fs_handled |= read_rights();
|
|
267
284
|
}
|
|
268
|
-
if (
|
|
285
|
+
if (handled_execute) {
|
|
269
286
|
fs_handled |= execute_rights();
|
|
270
287
|
}
|
|
271
|
-
if (
|
|
288
|
+
if (handled_write) {
|
|
272
289
|
fs_handled |= write_rights(abi);
|
|
273
290
|
}
|
|
274
291
|
for (size_t i = 0; i < path_rules->len; i++) {
|
|
@@ -281,10 +298,10 @@ static void apply_landlock(string_list *read_paths, string_list *write_paths,
|
|
|
281
298
|
}
|
|
282
299
|
}
|
|
283
300
|
uint64_t net_handled = 0;
|
|
284
|
-
if (
|
|
301
|
+
if (handled_bind) {
|
|
285
302
|
net_handled |= LANDLOCK_ACCESS_NET_BIND_TCP;
|
|
286
303
|
}
|
|
287
|
-
if (
|
|
304
|
+
if (handled_connect) {
|
|
288
305
|
net_handled |= LANDLOCK_ACCESS_NET_CONNECT_TCP;
|
|
289
306
|
}
|
|
290
307
|
|
|
@@ -533,6 +550,8 @@ int main(int argc, char **argv) {
|
|
|
533
550
|
path_rule_list path_rules = {0};
|
|
534
551
|
ull_list connect_ports = {0}, bind_ports = {0};
|
|
535
552
|
int seccomp_deny_network = 0, allow_all_known = 0, close_others = 1;
|
|
553
|
+
int handled_read = 0, handled_write = 0, handled_execute = 0;
|
|
554
|
+
int handled_connect = 0, handled_bind = 0;
|
|
536
555
|
uint64_t scoped = 0;
|
|
537
556
|
char *chdir_path = NULL;
|
|
538
557
|
char **command_argv = NULL;
|
|
@@ -544,19 +563,19 @@ int main(int argc, char **argv) {
|
|
|
544
563
|
break;
|
|
545
564
|
}
|
|
546
565
|
if (strcmp(argv[i], "--read") == 0) {
|
|
547
|
-
|
|
566
|
+
push_unless_empty(&read_paths, require_arg(argc, argv, &i), &handled_read);
|
|
548
567
|
} else if (strcmp(argv[i], "--write") == 0) {
|
|
549
|
-
|
|
568
|
+
push_unless_empty(&write_paths, require_arg(argc, argv, &i), &handled_write);
|
|
550
569
|
} else if (strcmp(argv[i], "--execute") == 0) {
|
|
551
|
-
|
|
570
|
+
push_unless_empty(&execute_paths, require_arg(argc, argv, &i), &handled_execute);
|
|
552
571
|
} else if (strcmp(argv[i], "--path") == 0) {
|
|
553
572
|
char *path = require_arg(argc, argv, &i);
|
|
554
573
|
char *rights = require_arg(argc, argv, &i);
|
|
555
574
|
path_rule_list_push(&path_rules, path, parse_fs_rights(rights));
|
|
556
575
|
} else if (strcmp(argv[i], "--connect-tcp") == 0) {
|
|
557
|
-
|
|
576
|
+
push_port_unless_empty(&connect_ports, require_arg(argc, argv, &i), &handled_connect);
|
|
558
577
|
} else if (strcmp(argv[i], "--bind-tcp") == 0) {
|
|
559
|
-
|
|
578
|
+
push_port_unless_empty(&bind_ports, require_arg(argc, argv, &i), &handled_bind);
|
|
560
579
|
} else if (strcmp(argv[i], "--scope") == 0) {
|
|
561
580
|
scoped |= scope_name(require_arg(argc, argv, &i));
|
|
562
581
|
} else if (strcmp(argv[i], "--chdir") == 0) {
|
|
@@ -588,7 +607,8 @@ int main(int argc, char **argv) {
|
|
|
588
607
|
}
|
|
589
608
|
|
|
590
609
|
apply_landlock(&read_paths, &write_paths, &execute_paths, &path_rules, &connect_ports,
|
|
591
|
-
&bind_ports, scoped, allow_all_known
|
|
610
|
+
&bind_ports, scoped, allow_all_known, handled_read, handled_write,
|
|
611
|
+
handled_execute, handled_connect, handled_bind);
|
|
592
612
|
if (seccomp_deny_network) {
|
|
593
613
|
apply_seccomp_deny_network();
|
|
594
614
|
}
|
data/lib/landlock/execution.rb
CHANGED
|
@@ -14,13 +14,13 @@ module Landlock
|
|
|
14
14
|
|
|
15
15
|
def exec(
|
|
16
16
|
argv,
|
|
17
|
-
read:
|
|
18
|
-
write:
|
|
19
|
-
execute:
|
|
20
|
-
connect_tcp:
|
|
21
|
-
bind_tcp:
|
|
22
|
-
paths:
|
|
23
|
-
scope:
|
|
17
|
+
read: nil,
|
|
18
|
+
write: nil,
|
|
19
|
+
execute: nil,
|
|
20
|
+
connect_tcp: nil,
|
|
21
|
+
bind_tcp: nil,
|
|
22
|
+
paths: nil,
|
|
23
|
+
scope: nil,
|
|
24
24
|
chdir: nil,
|
|
25
25
|
env: nil,
|
|
26
26
|
unsetenv_others: false,
|
|
@@ -49,13 +49,13 @@ module Landlock
|
|
|
49
49
|
|
|
50
50
|
def spawn(
|
|
51
51
|
argv,
|
|
52
|
-
read:
|
|
53
|
-
write:
|
|
54
|
-
execute:
|
|
55
|
-
connect_tcp:
|
|
56
|
-
bind_tcp:
|
|
57
|
-
paths:
|
|
58
|
-
scope:
|
|
52
|
+
read: nil,
|
|
53
|
+
write: nil,
|
|
54
|
+
execute: nil,
|
|
55
|
+
connect_tcp: nil,
|
|
56
|
+
bind_tcp: nil,
|
|
57
|
+
paths: nil,
|
|
58
|
+
scope: nil,
|
|
59
59
|
chdir: nil,
|
|
60
60
|
env: nil,
|
|
61
61
|
unsetenv_others: false,
|
|
@@ -82,13 +82,13 @@ module Landlock
|
|
|
82
82
|
|
|
83
83
|
def capture_with(
|
|
84
84
|
argv,
|
|
85
|
-
read:
|
|
86
|
-
write:
|
|
87
|
-
execute:
|
|
88
|
-
connect_tcp:
|
|
89
|
-
bind_tcp:
|
|
90
|
-
paths:
|
|
91
|
-
scope:
|
|
85
|
+
read: nil,
|
|
86
|
+
write: nil,
|
|
87
|
+
execute: nil,
|
|
88
|
+
connect_tcp: nil,
|
|
89
|
+
bind_tcp: nil,
|
|
90
|
+
paths: nil,
|
|
91
|
+
scope: nil,
|
|
92
92
|
chdir: nil,
|
|
93
93
|
env: nil,
|
|
94
94
|
unsetenv_others: false,
|
|
@@ -178,8 +178,8 @@ module Landlock
|
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
def prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
|
|
181
|
-
connect_tcp = Validation.normalize_ports(connect_tcp, :connect_tcp)
|
|
182
|
-
bind_tcp = Validation.normalize_ports(bind_tcp, :bind_tcp)
|
|
181
|
+
connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
|
|
182
|
+
bind_tcp = bind_tcp.nil? ? nil : Validation.normalize_ports(bind_tcp, :bind_tcp)
|
|
183
183
|
read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
|
|
184
184
|
{ read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
|
|
185
185
|
end
|
|
@@ -221,9 +221,9 @@ module Landlock
|
|
|
221
221
|
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
|
|
222
222
|
base = chdir ? File.expand_path(chdir) : Dir.pwd
|
|
223
223
|
abi = Native.abi_version
|
|
224
|
-
read = Validation.validate_existing_paths(read, :read, chdir:)
|
|
225
|
-
write = Validation.validate_existing_paths(write, :write, chdir:)
|
|
226
|
-
execute = Validation.validate_existing_paths(execute, :execute, chdir:)
|
|
224
|
+
read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
|
|
225
|
+
write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
|
|
226
|
+
execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
|
|
227
227
|
paths =
|
|
228
228
|
Array(paths).map do |rule|
|
|
229
229
|
path, rights = Policy.normalize_path_rule(rule)
|
data/lib/landlock/policy.rb
CHANGED
|
@@ -8,13 +8,13 @@ module Landlock
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def restrict!(
|
|
11
|
-
read:
|
|
12
|
-
write:
|
|
13
|
-
execute:
|
|
14
|
-
connect_tcp:
|
|
15
|
-
bind_tcp:
|
|
16
|
-
paths:
|
|
17
|
-
scope:
|
|
11
|
+
read: nil,
|
|
12
|
+
write: nil,
|
|
13
|
+
execute: nil,
|
|
14
|
+
connect_tcp: nil,
|
|
15
|
+
bind_tcp: nil,
|
|
16
|
+
paths: nil,
|
|
17
|
+
scope: nil,
|
|
18
18
|
allow_all_known: false
|
|
19
19
|
)
|
|
20
20
|
abi = Native.abi_version
|
|
@@ -61,8 +61,8 @@ module Landlock
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
64
|
-
allow_all_known ||
|
|
65
|
-
Array(
|
|
64
|
+
allow_all_known || !read.nil? || !write.nil? || !execute.nil? || !connect_tcp.nil? || !bind_tcp.nil? ||
|
|
65
|
+
!Array(paths).empty? || !Array(scope).empty?
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def path_rights(path, rights)
|
|
@@ -126,11 +126,14 @@ module Landlock
|
|
|
126
126
|
rights
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Landlock leaves any right absent from handled_access_fs unrestricted
|
|
130
|
+
# kernel-wide, so nil (do not police this class) and [] (police it, grant
|
|
131
|
+
# nothing) must produce different masks.
|
|
129
132
|
def handled_fs_for(read:, write:, execute:, paths:, abi:)
|
|
130
133
|
bits = 0
|
|
131
|
-
bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless
|
|
132
|
-
bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless
|
|
133
|
-
bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless
|
|
134
|
+
bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless read.nil?
|
|
135
|
+
bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless execute.nil?
|
|
136
|
+
bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless write.nil?
|
|
134
137
|
Array(paths).each do |rule|
|
|
135
138
|
path, rights = normalize_path_rule(rule)
|
|
136
139
|
bits |= path_rule_access_mask(File.expand_path(path), rights, abi)
|
|
@@ -140,8 +143,8 @@ module Landlock
|
|
|
140
143
|
|
|
141
144
|
def handled_net_for(connect_tcp:, bind_tcp:, abi:)
|
|
142
145
|
bits = 0
|
|
143
|
-
bits |= ACCESS_NET_CONNECT_TCP unless
|
|
144
|
-
bits |= ACCESS_NET_BIND_TCP unless
|
|
146
|
+
bits |= ACCESS_NET_CONNECT_TCP unless connect_tcp.nil?
|
|
147
|
+
bits |= ACCESS_NET_BIND_TCP unless bind_tcp.nil?
|
|
145
148
|
return 0 if bits.zero?
|
|
146
149
|
|
|
147
150
|
raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
|
|
@@ -202,15 +202,15 @@ module Landlock
|
|
|
202
202
|
close_others:
|
|
203
203
|
)
|
|
204
204
|
args = [helper_path]
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
emit_rules(args, "--read", read)
|
|
206
|
+
emit_rules(args, "--write", write)
|
|
207
|
+
emit_rules(args, "--execute", execute)
|
|
208
208
|
Array(paths).each do |rule|
|
|
209
209
|
path, rights = Policy.normalize_path_rule(rule)
|
|
210
210
|
args << "--path" << path.to_s << Array(rights).map(&:to_s).join(",")
|
|
211
211
|
end
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
emit_rules(args, "--connect-tcp", connect_tcp)
|
|
213
|
+
emit_rules(args, "--bind-tcp", bind_tcp)
|
|
214
214
|
Array(scope).each { |name| args << "--scope" << name.to_s }
|
|
215
215
|
Array(rlimits).each { |key, value| args << "--rlimit" << "#{key}=#{value}" }
|
|
216
216
|
args << "--allow-all-known" if allow_all_known
|
|
@@ -220,6 +220,16 @@ module Landlock
|
|
|
220
220
|
args.concat(argv.map(&:to_s))
|
|
221
221
|
args
|
|
222
222
|
end
|
|
223
|
+
|
|
224
|
+
# An empty value keeps the rights in the handled mask while granting
|
|
225
|
+
# nothing; the empty string is never a valid path or port, so it cannot
|
|
226
|
+
# collide with a real entry.
|
|
227
|
+
def emit_rules(args, flag, values)
|
|
228
|
+
return if values.nil?
|
|
229
|
+
return args.push(flag, "") if Array(values).empty?
|
|
230
|
+
|
|
231
|
+
Array(values).each { |value| args.push(flag, value.to_s) }
|
|
232
|
+
end
|
|
223
233
|
end
|
|
224
234
|
end
|
|
225
235
|
end
|
data/lib/landlock/version.rb
CHANGED
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.4'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
147
147
|
- !ruby/object:Gem::Version
|
|
148
148
|
version: '0'
|
|
149
149
|
requirements: []
|
|
150
|
-
rubygems_version:
|
|
150
|
+
rubygems_version: 3.6.9
|
|
151
151
|
specification_version: 4
|
|
152
152
|
summary: Ruby bindings for Linux Landlock sandboxing
|
|
153
153
|
test_files: []
|