landlock 0.5 → 0.5.1
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 +6 -0
- data/README.md +4 -2
- data/ext/landlock/landlock.c +10 -12
- data/lib/landlock/execution.rb +33 -14
- data/lib/landlock/runner/fork.rb +2 -2
- data/lib/landlock/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 212a2155cecade152248158abbbfea18f6592f187f1338a2faf442c5b727a5b4
|
|
4
|
+
data.tar.gz: 110062bf45e0f020d44112964f2b140c8eac867f8c502171a66c5257c19c2ba6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 711002205ed3686d42d411d86cda9b18c6f435d467a1732ac5cd97fdb7404549b01a2617232bf7ac480b10f10a07e212331ac57c73f74128ddc5c8bbb17961b2
|
|
7
|
+
data.tar.gz: 42cf382fe1cd2b29b3f23a5425522eb7a198dd19de3d59fc4ce2be33fb07483ba9ae53b199af6f8f126d960756850552f329b8e3f6dead5e0b08764e5a3d488b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.5.1] - 2026-09-09
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Allow `Landlock.fork(on_unsupported: :run_without_landlock)` to fall back when the support check is blocked or unavailable, including on macOS.
|
|
12
|
+
|
|
7
13
|
## [0.5] - 2026-09-01
|
|
8
14
|
|
|
9
15
|
### Added
|
data/README.md
CHANGED
|
@@ -154,7 +154,7 @@ color = result.stdout if result.success?
|
|
|
154
154
|
|
|
155
155
|
The block receives its child-side stdout and stderr streams. Write response data to stdout and diagnostics to stderr, then inspect them through the capture result in the parent. The block's return value is discarded. An exception makes the child exit with status 1 and writes a diagnostic to stderr. `fork` accepts the capture options listed above except `success_status_codes:` and `failure_message:`, which only apply to `capture!`.
|
|
156
156
|
|
|
157
|
-
By default, `Landlock.fork` requires Linux Landlock support and raises `Landlock::UnsupportedError` before forking when the Landlock ABI is unavailable. A
|
|
157
|
+
By default, `Landlock.fork` requires Linux Landlock support and raises `Landlock::UnsupportedError` before forking when the Landlock ABI is unavailable. A caller that explicitly accepts running without Landlock filesystem, TCP, and scope enforcement can opt in to the fallback:
|
|
158
158
|
|
|
159
159
|
```ruby
|
|
160
160
|
result = Landlock.fork(
|
|
@@ -165,7 +165,9 @@ result = Landlock.fork(
|
|
|
165
165
|
) { |stdout, _stderr| stdout.write(run_plugin) }
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
This fallback is used
|
|
168
|
+
This fallback is used when the Landlock ABI is unavailable, including when the capability probe fails or the platform does not support Landlock. Errors applying a policy after Landlock is detected still fail closed. Timeout handling, environment changes, descriptor closing, rlimits, and output capture remain active. Linux also retains parent-death cleanup and any requested seccomp restrictions. Non-Linux systems cannot provide parent-death cleanup and reject `seccomp_deny_network: true`; callers must omit that option and supply at least one `rlimits:` entry.
|
|
169
|
+
|
|
170
|
+
Fallback is never selected implicitly. When active, the call must include `seccomp_deny_network: true` or at least one `rlimits:` entry because Landlock rules are not effective restrictions in that mode. Timeout, environment handling, descriptor closing, and output limits do not satisfy this requirement. By default, the child closes inherited Ruby `IO` objects other than stdin, stdout, and stderr, then closes every other application descriptor numbered 3 or higher while preserving Ruby VM-reserved descriptors. It enumerates `/proc/self/fd` on Linux and `/dev/fd` elsewhere, including macOS, and fails closed with setup status 127 if enumeration is unavailable or fails. Pass `close_others: false` only when the child intentionally needs an inherited descriptor. Child setup failures exit 127.
|
|
169
171
|
|
|
170
172
|
The worker is a process-group leader and reserves Linux real-time signal `SIGRTMIN+2` for parent-death handling while the block runs. If the Ruby thread supervising the synchronous `Landlock.fork` call terminates, a native signal handler sends `SIGKILL` to the worker's process group. This terminates the worker and ordinary descendants that remain in that group. It does not cover descendants that create another process group or session, and the group-wide guarantee can be disabled by code that replaces or blocks the reserved signal, clears the parent-death signal, changes credentials in a way that clears it, or replaces the worker with `exec`. After `exec`, the reserved signal still terminates the worker by default, but the reset handler no longer kills its process group. This is process-lifecycle hardening, not a cgroup, PID namespace, or hostile-process containment boundary.
|
|
171
173
|
|
data/ext/landlock/landlock.c
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
#include <signal.h>
|
|
6
6
|
#include <string.h>
|
|
7
7
|
|
|
8
|
-
#ifdef __linux__
|
|
9
8
|
#include <dirent.h>
|
|
10
9
|
#include <stdlib.h>
|
|
11
|
-
#endif
|
|
12
10
|
|
|
13
11
|
static VALUE mLandlock;
|
|
14
12
|
static VALUE eLandlockError;
|
|
@@ -127,13 +125,18 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
|
|
|
127
125
|
return Qnil;
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
#ifdef __linux__
|
|
129
|
+
#define LL_DESCRIPTOR_DIRECTORY "/proc/self/fd"
|
|
130
|
+
#else
|
|
131
|
+
#define LL_DESCRIPTOR_DIRECTORY "/dev/fd"
|
|
132
|
+
#endif
|
|
133
|
+
|
|
130
134
|
static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
131
135
|
/* The forked child keeps running Ruby, so interpreter-reserved descriptors
|
|
132
136
|
* must survive. This rules out close_range across the entire descriptor table. */
|
|
133
|
-
|
|
134
|
-
DIR *dir = opendir("/proc/self/fd");
|
|
137
|
+
DIR *dir = opendir(LL_DESCRIPTOR_DIRECTORY);
|
|
135
138
|
if (!dir) {
|
|
136
|
-
raise_syscall_error("opendir(
|
|
139
|
+
raise_syscall_error("opendir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
int dir_fd = dirfd(dir);
|
|
@@ -146,7 +149,7 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
|
146
149
|
int saved_errno = errno;
|
|
147
150
|
closedir(dir);
|
|
148
151
|
errno = saved_errno;
|
|
149
|
-
raise_syscall_error("readdir(
|
|
152
|
+
raise_syscall_error("readdir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
150
153
|
}
|
|
151
154
|
break;
|
|
152
155
|
}
|
|
@@ -160,14 +163,9 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
if (closedir(dir) != 0) {
|
|
163
|
-
raise_syscall_error("closedir(
|
|
166
|
+
raise_syscall_error("closedir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
164
167
|
}
|
|
165
168
|
return Qtrue;
|
|
166
|
-
#else
|
|
167
|
-
errno = ENOSYS;
|
|
168
|
-
raise_syscall_error("opendir(/proc/self/fd)");
|
|
169
|
-
return Qnil;
|
|
170
|
-
#endif
|
|
171
169
|
}
|
|
172
170
|
|
|
173
171
|
static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
data/lib/landlock/execution.rb
CHANGED
|
@@ -86,19 +86,13 @@ module Landlock
|
|
|
86
86
|
raise ArgumentError, "on_unsupported must be :raise or :run_without_landlock"
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
enforce_landlock =
|
|
90
|
-
if !enforce_landlock &&
|
|
91
|
-
raise UnsupportedError, "Linux Landlock is unavailable"
|
|
92
|
-
end
|
|
89
|
+
enforce_landlock = Landlock.supported?
|
|
90
|
+
raise UnsupportedError, "Linux Landlock is unavailable" if !enforce_landlock && on_unsupported == :raise
|
|
93
91
|
|
|
94
92
|
capture_options = prepare_capture_options(**options, require_landlock: enforce_landlock)
|
|
95
93
|
validate_fallback_restriction!(**capture_options) if !enforce_landlock
|
|
96
94
|
|
|
97
|
-
Runner::Fork.call_block(
|
|
98
|
-
**capture_options,
|
|
99
|
-
enforce_landlock:,
|
|
100
|
-
&block
|
|
101
|
-
)
|
|
95
|
+
Runner::Fork.call_block(**capture_options, enforce_landlock:, &block)
|
|
102
96
|
rescue OutputTooLargeError => error
|
|
103
97
|
result = error.result
|
|
104
98
|
raise CommandError.new(
|
|
@@ -205,7 +199,18 @@ module Landlock
|
|
|
205
199
|
rlimits = Rlimits.normalize(rlimits)
|
|
206
200
|
env = Env.normalize(env)
|
|
207
201
|
policy =
|
|
208
|
-
prepare_policy(
|
|
202
|
+
prepare_policy(
|
|
203
|
+
read:,
|
|
204
|
+
write:,
|
|
205
|
+
execute:,
|
|
206
|
+
connect_tcp:,
|
|
207
|
+
bind_tcp:,
|
|
208
|
+
paths:,
|
|
209
|
+
scope:,
|
|
210
|
+
chdir:,
|
|
211
|
+
allow_all_known:,
|
|
212
|
+
abi: require_landlock ? Native.abi_version : 0
|
|
213
|
+
)
|
|
209
214
|
validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
|
|
210
215
|
|
|
211
216
|
{
|
|
@@ -251,10 +256,21 @@ module Landlock
|
|
|
251
256
|
raise UnsupportedError, "Linux Landlock is unavailable" unless Native.abi_version.positive?
|
|
252
257
|
end
|
|
253
258
|
|
|
254
|
-
def prepare_policy(
|
|
259
|
+
def prepare_policy(
|
|
260
|
+
read:,
|
|
261
|
+
write:,
|
|
262
|
+
execute:,
|
|
263
|
+
connect_tcp:,
|
|
264
|
+
bind_tcp:,
|
|
265
|
+
paths:,
|
|
266
|
+
scope:,
|
|
267
|
+
chdir:,
|
|
268
|
+
allow_all_known:,
|
|
269
|
+
abi: Native.abi_version
|
|
270
|
+
)
|
|
255
271
|
connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
|
|
256
272
|
bind_tcp = bind_tcp.nil? ? nil : Validation.normalize_ports(bind_tcp, :bind_tcp)
|
|
257
|
-
read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
|
|
273
|
+
read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
|
|
258
274
|
{ read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
|
|
259
275
|
end
|
|
260
276
|
|
|
@@ -274,6 +290,10 @@ module Landlock
|
|
|
274
290
|
end
|
|
275
291
|
|
|
276
292
|
def validate_fallback_restriction!(seccomp_deny_network:, rlimits:, **)
|
|
293
|
+
if seccomp_deny_network && !RUBY_PLATFORM.include?("linux")
|
|
294
|
+
raise UnsupportedError, "seccomp_deny_network requires Linux"
|
|
295
|
+
end
|
|
296
|
+
|
|
277
297
|
return if seccomp_deny_network || rlimits.any?
|
|
278
298
|
|
|
279
299
|
raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
|
|
@@ -298,9 +318,8 @@ module Landlock
|
|
|
298
318
|
raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, or rlimits"
|
|
299
319
|
end
|
|
300
320
|
|
|
301
|
-
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
|
|
321
|
+
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
|
|
302
322
|
base = chdir ? File.expand_path(chdir) : Dir.pwd
|
|
303
|
-
abi = Native.abi_version
|
|
304
323
|
read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
|
|
305
324
|
write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
|
|
306
325
|
execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
|
data/lib/landlock/runner/fork.rb
CHANGED
|
@@ -139,9 +139,9 @@ module Landlock
|
|
|
139
139
|
begin
|
|
140
140
|
# Arm group cleanup only after leaving the supervisor's process group.
|
|
141
141
|
::Process.setpgrp
|
|
142
|
-
if kill_process_group_on_parent_death
|
|
142
|
+
if kill_process_group_on_parent_death && RUBY_PLATFORM.include?("linux")
|
|
143
143
|
Landlock::Native.arm_parent_death_process_group!(parent_pid)
|
|
144
|
-
|
|
144
|
+
elsif RUBY_PLATFORM.include?("linux")
|
|
145
145
|
Landlock::Native.set_parent_death_signal!
|
|
146
146
|
exit! 1 if ::Process.ppid != parent_pid
|
|
147
147
|
end
|
data/lib/landlock/version.rb
CHANGED