landlock 0.4.1 → 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 +12 -0
- data/README.md +38 -0
- data/ext/landlock/landlock.c +108 -0
- data/lib/landlock/execution.rb +117 -18
- data/lib/landlock/native.rb +12 -0
- data/lib/landlock/runner/fork.rb +111 -19
- data/lib/landlock/runner.rb +10 -2
- data/lib/landlock/version.rb +1 -1
- data/lib/landlock.rb +4 -0
- 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,18 @@ 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
|
+
|
|
13
|
+
## [0.5] - 2026-09-01
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Add `Landlock.fork` for running a sandboxed Ruby block in a supervised forked child.
|
|
18
|
+
|
|
7
19
|
## [0.4.1] - 2026-08-20
|
|
8
20
|
|
|
9
21
|
### Fixed
|
data/README.md
CHANGED
|
@@ -135,6 +135,44 @@ Capture options:
|
|
|
135
135
|
- `success_status_codes:` and `failure_message:` — `capture!` failure handling options.
|
|
136
136
|
- `allow_all_known:` — when filesystem rules are present, handle all Landlock filesystem rights known to the running ABI so unlisted filesystem access is denied.
|
|
137
137
|
|
|
138
|
+
## Forking a Ruby block
|
|
139
|
+
|
|
140
|
+
`Landlock.fork` is a supervised, synchronous fork. It forks the current Ruby process, applies the requested restrictions in the child, runs the block there, waits for it, and returns a `Landlock::CaptureResult`. It is intended for applications that need to reuse initialized Ruby state without executing a new command:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
result = Landlock.fork(
|
|
144
|
+
read: [input_path],
|
|
145
|
+
timeout: 5,
|
|
146
|
+
rlimits: { cpu_seconds: 5, memory_bytes: 512 * 1024 * 1024 },
|
|
147
|
+
seccomp_deny_network: true
|
|
148
|
+
) do |stdout, _stderr|
|
|
149
|
+
stdout.write(calculate_dominant_color(input_path))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
color = result.stdout if result.success?
|
|
153
|
+
```
|
|
154
|
+
|
|
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
|
+
|
|
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
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
result = Landlock.fork(
|
|
161
|
+
on_unsupported: :run_without_landlock,
|
|
162
|
+
timeout: 5,
|
|
163
|
+
rlimits: { memory_bytes: 512 * 1024 * 1024 },
|
|
164
|
+
seccomp_deny_network: true
|
|
165
|
+
) { |stdout, _stderr| stdout.write(run_plugin) }
|
|
166
|
+
```
|
|
167
|
+
|
|
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.
|
|
171
|
+
|
|
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.
|
|
173
|
+
|
|
174
|
+
Fork only from a process whose loaded libraries and runtime state are safe to use after `fork`. `Landlock.fork` cannot make an unsafe parent fork-safe, and the block must not depend on threads that exist only in the parent.
|
|
175
|
+
|
|
138
176
|
## Restrict current process
|
|
139
177
|
|
|
140
178
|
This is irreversible for the current thread and its future children. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.
|
data/ext/landlock/landlock.c
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
#include "landlock_native.h"
|
|
3
3
|
#include "seccomp_deny_network.h"
|
|
4
4
|
|
|
5
|
+
#include <signal.h>
|
|
5
6
|
#include <string.h>
|
|
6
7
|
|
|
8
|
+
#include <dirent.h>
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
|
|
7
11
|
static VALUE mLandlock;
|
|
8
12
|
static VALUE eLandlockError;
|
|
9
13
|
static VALUE eSyscallError;
|
|
@@ -121,6 +125,49 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
|
|
|
121
125
|
return Qnil;
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
#ifdef __linux__
|
|
129
|
+
#define LL_DESCRIPTOR_DIRECTORY "/proc/self/fd"
|
|
130
|
+
#else
|
|
131
|
+
#define LL_DESCRIPTOR_DIRECTORY "/dev/fd"
|
|
132
|
+
#endif
|
|
133
|
+
|
|
134
|
+
static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
135
|
+
/* The forked child keeps running Ruby, so interpreter-reserved descriptors
|
|
136
|
+
* must survive. This rules out close_range across the entire descriptor table. */
|
|
137
|
+
DIR *dir = opendir(LL_DESCRIPTOR_DIRECTORY);
|
|
138
|
+
if (!dir) {
|
|
139
|
+
raise_syscall_error("opendir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
int dir_fd = dirfd(dir);
|
|
143
|
+
struct dirent *entry;
|
|
144
|
+
for (;;) {
|
|
145
|
+
errno = 0;
|
|
146
|
+
entry = readdir(dir);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
if (errno != 0) {
|
|
149
|
+
int saved_errno = errno;
|
|
150
|
+
closedir(dir);
|
|
151
|
+
errno = saved_errno;
|
|
152
|
+
raise_syscall_error("readdir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
char *end = NULL;
|
|
158
|
+
errno = 0;
|
|
159
|
+
long fd = strtol(entry->d_name, &end, 10);
|
|
160
|
+
if (errno == 0 && end && *end == '\0' && fd >= 3 && fd != dir_fd &&
|
|
161
|
+
!rb_reserved_fd_p((int)fd)) {
|
|
162
|
+
close((int)fd);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (closedir(dir) != 0) {
|
|
166
|
+
raise_syscall_error("closedir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
167
|
+
}
|
|
168
|
+
return Qtrue;
|
|
169
|
+
}
|
|
170
|
+
|
|
124
171
|
static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
|
125
172
|
#ifdef SYS_pidfd_open
|
|
126
173
|
int fd = syscall(SYS_pidfd_open, NUM2PIDT(pid_value), 0);
|
|
@@ -135,6 +182,62 @@ static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
|
|
135
182
|
#endif
|
|
136
183
|
}
|
|
137
184
|
|
|
185
|
+
/* Runs after the worker has become its own process-group leader. */
|
|
186
|
+
static void terminate_own_process_group(int signal_number) {
|
|
187
|
+
(void)signal_number;
|
|
188
|
+
kill(0, SIGKILL);
|
|
189
|
+
_exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static VALUE rb_ll_arm_parent_death_process_group(VALUE self, VALUE parent_pid_value) {
|
|
193
|
+
#ifdef __linux__
|
|
194
|
+
pid_t parent_pid = NUM2PIDT(parent_pid_value);
|
|
195
|
+
/* Leave the first two application-visible realtime signals available to callers. */
|
|
196
|
+
int parent_death_signal = SIGRTMIN + 2;
|
|
197
|
+
|
|
198
|
+
struct sigaction action;
|
|
199
|
+
memset(&action, 0, sizeof(action));
|
|
200
|
+
action.sa_handler = terminate_own_process_group;
|
|
201
|
+
sigemptyset(&action.sa_mask);
|
|
202
|
+
if (sigaction(parent_death_signal, &action, NULL) != 0) {
|
|
203
|
+
raise_syscall_error("sigaction(parent death process group)");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
sigset_t signals;
|
|
207
|
+
sigemptyset(&signals);
|
|
208
|
+
sigaddset(&signals, parent_death_signal);
|
|
209
|
+
if (sigprocmask(SIG_UNBLOCK, &signals, NULL) != 0) {
|
|
210
|
+
raise_syscall_error("sigprocmask(parent death process group)");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (prctl(PR_SET_PDEATHSIG, parent_death_signal) != 0) {
|
|
214
|
+
raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (getppid() != parent_pid) {
|
|
218
|
+
terminate_own_process_group(parent_death_signal);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Qtrue;
|
|
222
|
+
#else
|
|
223
|
+
errno = ENOSYS;
|
|
224
|
+
raise_syscall_error("parent death process group");
|
|
225
|
+
return Qnil;
|
|
226
|
+
#endif
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static VALUE rb_ll_set_parent_death_signal(VALUE self) {
|
|
230
|
+
#ifdef __linux__
|
|
231
|
+
if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) {
|
|
232
|
+
raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
|
|
233
|
+
}
|
|
234
|
+
return Qtrue;
|
|
235
|
+
#else
|
|
236
|
+
errno = ENOSYS;
|
|
237
|
+
raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
|
|
238
|
+
#endif
|
|
239
|
+
}
|
|
240
|
+
|
|
138
241
|
static VALUE rb_ll_seccomp_deny_network(VALUE self) {
|
|
139
242
|
const char *error_message = "seccomp(SECCOMP_SET_MODE_FILTER)";
|
|
140
243
|
if (rb_landlock_seccomp_deny_network(&error_message) != 0) {
|
|
@@ -164,7 +267,12 @@ void Init_landlock(void) {
|
|
|
164
267
|
rb_define_singleton_method(mLandlock, "_add_net_rule", rb_ll_add_net_rule, 3);
|
|
165
268
|
rb_define_singleton_method(mLandlock, "_restrict_self", rb_ll_restrict_self, 1);
|
|
166
269
|
rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
|
|
270
|
+
rb_define_singleton_method(mLandlock, "_close_inherited_fds", rb_ll_close_inherited_fds, 0);
|
|
167
271
|
rb_define_singleton_method(mLandlock, "_pidfd_open", rb_ll_pidfd_open, 1);
|
|
272
|
+
rb_define_singleton_method(mLandlock, "_arm_parent_death_process_group",
|
|
273
|
+
rb_ll_arm_parent_death_process_group, 1);
|
|
274
|
+
rb_define_singleton_method(mLandlock, "_set_parent_death_signal", rb_ll_set_parent_death_signal,
|
|
275
|
+
0);
|
|
168
276
|
rb_define_singleton_method(mLandlock, "seccomp_deny_network!", rb_ll_seccomp_deny_network, 0);
|
|
169
277
|
|
|
170
278
|
rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
|
data/lib/landlock/execution.rb
CHANGED
|
@@ -80,6 +80,30 @@ module Landlock
|
|
|
80
80
|
capture_with(argv, raise_on_failure: true, **options)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
def fork(on_unsupported: :raise, **options, &block)
|
|
84
|
+
raise ArgumentError, "fork requires a block" if !block
|
|
85
|
+
if !%i[raise run_without_landlock].include?(on_unsupported)
|
|
86
|
+
raise ArgumentError, "on_unsupported must be :raise or :run_without_landlock"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
enforce_landlock = Landlock.supported?
|
|
90
|
+
raise UnsupportedError, "Linux Landlock is unavailable" if !enforce_landlock && on_unsupported == :raise
|
|
91
|
+
|
|
92
|
+
capture_options = prepare_capture_options(**options, require_landlock: enforce_landlock)
|
|
93
|
+
validate_fallback_restriction!(**capture_options) if !enforce_landlock
|
|
94
|
+
|
|
95
|
+
Runner::Fork.call_block(**capture_options, enforce_landlock:, &block)
|
|
96
|
+
rescue OutputTooLargeError => error
|
|
97
|
+
result = error.result
|
|
98
|
+
raise CommandError.new(
|
|
99
|
+
error.message,
|
|
100
|
+
stdout: result&.stdout.to_s,
|
|
101
|
+
stderr: result&.stderr.to_s,
|
|
102
|
+
status: result&.status,
|
|
103
|
+
result:
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
83
107
|
def capture_with(
|
|
84
108
|
argv,
|
|
85
109
|
read: nil,
|
|
@@ -105,31 +129,30 @@ module Landlock
|
|
|
105
129
|
raise_on_failure:
|
|
106
130
|
)
|
|
107
131
|
argv = Validation.normalize_argv(argv).map(&:to_s)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
result =
|
|
118
|
-
call_with_runner(
|
|
119
|
-
argv,
|
|
120
|
-
**policy,
|
|
132
|
+
options =
|
|
133
|
+
prepare_capture_options(
|
|
134
|
+
read:,
|
|
135
|
+
write:,
|
|
136
|
+
execute:,
|
|
137
|
+
connect_tcp:,
|
|
138
|
+
bind_tcp:,
|
|
139
|
+
paths:,
|
|
140
|
+
scope:,
|
|
121
141
|
chdir:,
|
|
122
142
|
env:,
|
|
123
143
|
unsetenv_others:,
|
|
124
144
|
close_others:,
|
|
145
|
+
allow_all_known:,
|
|
125
146
|
timeout:,
|
|
126
147
|
stdin:,
|
|
127
|
-
rlimits
|
|
148
|
+
rlimits:,
|
|
128
149
|
seccomp_deny_network:,
|
|
129
150
|
max_output_bytes:,
|
|
130
151
|
truncate_output:
|
|
131
152
|
)
|
|
132
153
|
|
|
154
|
+
result = call_with_runner(argv, **options)
|
|
155
|
+
|
|
133
156
|
if raise_on_failure &&
|
|
134
157
|
(result.timed_out? || !result.status.exited? || !success_status_codes.include?(result.status.exitstatus))
|
|
135
158
|
message = [argv.join(" "), failure_message, result.stderr].filter { |part| part.to_s != "" }.join("\n")
|
|
@@ -149,6 +172,62 @@ module Landlock
|
|
|
149
172
|
)
|
|
150
173
|
end
|
|
151
174
|
|
|
175
|
+
def prepare_capture_options(
|
|
176
|
+
read: nil,
|
|
177
|
+
write: nil,
|
|
178
|
+
execute: nil,
|
|
179
|
+
connect_tcp: nil,
|
|
180
|
+
bind_tcp: nil,
|
|
181
|
+
paths: nil,
|
|
182
|
+
scope: nil,
|
|
183
|
+
chdir: nil,
|
|
184
|
+
env: nil,
|
|
185
|
+
unsetenv_others: false,
|
|
186
|
+
close_others: true,
|
|
187
|
+
allow_all_known: false,
|
|
188
|
+
timeout: nil,
|
|
189
|
+
stdin: nil,
|
|
190
|
+
rlimits: {},
|
|
191
|
+
seccomp_deny_network: false,
|
|
192
|
+
max_output_bytes: nil,
|
|
193
|
+
truncate_output: false,
|
|
194
|
+
require_landlock: true
|
|
195
|
+
)
|
|
196
|
+
ensure_landlock_supported! if require_landlock
|
|
197
|
+
max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
|
|
198
|
+
timeout = Validation.validate_timeout!(timeout)
|
|
199
|
+
rlimits = Rlimits.normalize(rlimits)
|
|
200
|
+
env = Env.normalize(env)
|
|
201
|
+
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
|
+
)
|
|
214
|
+
validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
**policy,
|
|
218
|
+
chdir:,
|
|
219
|
+
env:,
|
|
220
|
+
unsetenv_others:,
|
|
221
|
+
close_others:,
|
|
222
|
+
timeout:,
|
|
223
|
+
stdin:,
|
|
224
|
+
rlimits:,
|
|
225
|
+
seccomp_deny_network:,
|
|
226
|
+
max_output_bytes:,
|
|
227
|
+
truncate_output:
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
152
231
|
def spawn_with_runner(argv, **options)
|
|
153
232
|
if Runner::Native.available?
|
|
154
233
|
begin
|
|
@@ -177,10 +256,21 @@ module Landlock
|
|
|
177
256
|
raise UnsupportedError, "Linux Landlock is unavailable" unless Native.abi_version.positive?
|
|
178
257
|
end
|
|
179
258
|
|
|
180
|
-
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
|
+
)
|
|
181
271
|
connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
|
|
182
272
|
bind_tcp = bind_tcp.nil? ? nil : Validation.normalize_ports(bind_tcp, :bind_tcp)
|
|
183
|
-
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:)
|
|
184
274
|
{ read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
|
|
185
275
|
end
|
|
186
276
|
|
|
@@ -199,6 +289,16 @@ module Landlock
|
|
|
199
289
|
raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
|
|
200
290
|
end
|
|
201
291
|
|
|
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
|
+
|
|
297
|
+
return if seccomp_deny_network || rlimits.any?
|
|
298
|
+
|
|
299
|
+
raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
|
|
300
|
+
end
|
|
301
|
+
|
|
202
302
|
def validate_capture_restriction!(
|
|
203
303
|
read:,
|
|
204
304
|
write:,
|
|
@@ -218,9 +318,8 @@ module Landlock
|
|
|
218
318
|
raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, or rlimits"
|
|
219
319
|
end
|
|
220
320
|
|
|
221
|
-
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
|
|
321
|
+
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
|
|
222
322
|
base = chdir ? File.expand_path(chdir) : Dir.pwd
|
|
223
|
-
abi = Native.abi_version
|
|
224
323
|
read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
|
|
225
324
|
write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
|
|
226
325
|
execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
|
data/lib/landlock/native.rb
CHANGED
|
@@ -31,10 +31,22 @@ module Landlock
|
|
|
31
31
|
Landlock.__send__(:_close_fd, fd)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def close_inherited_fds!
|
|
35
|
+
Landlock.__send__(:_close_inherited_fds)
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
def pidfd_open(pid)
|
|
35
39
|
Landlock.__send__(:_pidfd_open, pid)
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
def arm_parent_death_process_group!(parent_pid)
|
|
43
|
+
Landlock.__send__(:_arm_parent_death_process_group, parent_pid)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def set_parent_death_signal!
|
|
47
|
+
Landlock.__send__(:_set_parent_death_signal)
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def seccomp_deny_network!
|
|
39
51
|
Landlock.seccomp_deny_network!
|
|
40
52
|
end
|
data/lib/landlock/runner/fork.rb
CHANGED
|
@@ -71,44 +71,97 @@ module Landlock
|
|
|
71
71
|
seccomp_deny_network:,
|
|
72
72
|
max_output_bytes:,
|
|
73
73
|
truncate_output:
|
|
74
|
+
)
|
|
75
|
+
capture_pipes(timeout:, stdin:, max_output_bytes:, truncate_output:) do
|
|
76
|
+
setup_child!(
|
|
77
|
+
argv,
|
|
78
|
+
read:,
|
|
79
|
+
write:,
|
|
80
|
+
execute:,
|
|
81
|
+
connect_tcp:,
|
|
82
|
+
bind_tcp:,
|
|
83
|
+
paths:,
|
|
84
|
+
scope:,
|
|
85
|
+
chdir:,
|
|
86
|
+
env:,
|
|
87
|
+
unsetenv_others:,
|
|
88
|
+
close_others:,
|
|
89
|
+
allow_all_known:,
|
|
90
|
+
rlimits:,
|
|
91
|
+
seccomp_deny_network:
|
|
92
|
+
)
|
|
93
|
+
rescue Exception => error
|
|
94
|
+
Runner.exit_child!(error)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def call_block(timeout:, stdin:, max_output_bytes:, truncate_output:, enforce_landlock:, **options, &block)
|
|
99
|
+
capture_pipes(
|
|
100
|
+
timeout:,
|
|
101
|
+
stdin:,
|
|
102
|
+
max_output_bytes:,
|
|
103
|
+
truncate_output:,
|
|
104
|
+
kill_process_group_on_parent_death: true
|
|
105
|
+
) do
|
|
106
|
+
begin
|
|
107
|
+
prepare_forked_block!(**options, enforce_landlock:)
|
|
108
|
+
rescue SystemExit, SignalException
|
|
109
|
+
raise
|
|
110
|
+
rescue Exception => error
|
|
111
|
+
Runner.exit_child!(error)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
block.call(STDOUT, STDERR)
|
|
115
|
+
exit! 0
|
|
116
|
+
rescue SystemExit => error
|
|
117
|
+
exit! error.status
|
|
118
|
+
rescue SignalException
|
|
119
|
+
raise
|
|
120
|
+
rescue Exception => error
|
|
121
|
+
Runner.exit_forked_block!(error)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def capture_pipes(
|
|
126
|
+
timeout:,
|
|
127
|
+
stdin:,
|
|
128
|
+
max_output_bytes:,
|
|
129
|
+
truncate_output:,
|
|
130
|
+
kill_process_group_on_parent_death: false
|
|
74
131
|
)
|
|
75
132
|
stdout_reader, stdout_writer = IO.pipe
|
|
76
133
|
stderr_reader, stderr_writer = IO.pipe
|
|
77
134
|
stdin_reader, stdin_writer = IO.pipe
|
|
135
|
+
parent_pid = ::Process.pid
|
|
78
136
|
|
|
79
137
|
pid =
|
|
80
138
|
fork do
|
|
81
139
|
begin
|
|
140
|
+
# Arm group cleanup only after leaving the supervisor's process group.
|
|
141
|
+
::Process.setpgrp
|
|
142
|
+
if kill_process_group_on_parent_death && RUBY_PLATFORM.include?("linux")
|
|
143
|
+
Landlock::Native.arm_parent_death_process_group!(parent_pid)
|
|
144
|
+
elsif RUBY_PLATFORM.include?("linux")
|
|
145
|
+
Landlock::Native.set_parent_death_signal!
|
|
146
|
+
exit! 1 if ::Process.ppid != parent_pid
|
|
147
|
+
end
|
|
82
148
|
stdout_reader.close
|
|
83
149
|
stderr_reader.close
|
|
84
150
|
stdin_writer.close
|
|
85
|
-
::Process.setpgrp
|
|
86
151
|
STDIN.reopen(stdin_reader)
|
|
87
152
|
STDOUT.reopen(stdout_writer)
|
|
88
153
|
STDERR.reopen(stderr_writer)
|
|
154
|
+
STDOUT.sync = true
|
|
155
|
+
STDERR.sync = true
|
|
89
156
|
stdin_reader.close
|
|
90
157
|
stdout_writer.close
|
|
91
158
|
stderr_writer.close
|
|
92
159
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
write:,
|
|
97
|
-
execute:,
|
|
98
|
-
connect_tcp:,
|
|
99
|
-
bind_tcp:,
|
|
100
|
-
paths:,
|
|
101
|
-
scope:,
|
|
102
|
-
chdir:,
|
|
103
|
-
env:,
|
|
104
|
-
unsetenv_others:,
|
|
105
|
-
close_others:,
|
|
106
|
-
allow_all_known:,
|
|
107
|
-
rlimits:,
|
|
108
|
-
seccomp_deny_network:
|
|
109
|
-
)
|
|
160
|
+
yield
|
|
161
|
+
rescue SystemExit, SignalException
|
|
162
|
+
raise
|
|
110
163
|
rescue Exception => error
|
|
111
|
-
Runner.exit_child!(error)
|
|
164
|
+
Runner.exit_child!(error, stderr: capture_error_stream(stderr_writer))
|
|
112
165
|
end
|
|
113
166
|
end
|
|
114
167
|
|
|
@@ -141,6 +194,12 @@ module Landlock
|
|
|
141
194
|
end
|
|
142
195
|
end
|
|
143
196
|
|
|
197
|
+
def capture_error_stream(stderr_writer)
|
|
198
|
+
stderr_writer && !stderr_writer.closed? ? stderr_writer : STDERR
|
|
199
|
+
rescue IOError
|
|
200
|
+
STDERR
|
|
201
|
+
end
|
|
202
|
+
|
|
144
203
|
def setup_child!(
|
|
145
204
|
argv,
|
|
146
205
|
read:,
|
|
@@ -166,6 +225,39 @@ module Landlock
|
|
|
166
225
|
Rlimits.apply!(rlimits)
|
|
167
226
|
Kernel.exec(*Runner.kernel_exec_args(argv, env, unsetenv_others:, close_others:))
|
|
168
227
|
end
|
|
228
|
+
|
|
229
|
+
def prepare_forked_block!(
|
|
230
|
+
chdir:,
|
|
231
|
+
env:,
|
|
232
|
+
unsetenv_others:,
|
|
233
|
+
close_others:,
|
|
234
|
+
rlimits:,
|
|
235
|
+
seccomp_deny_network:,
|
|
236
|
+
enforce_landlock:,
|
|
237
|
+
**policy
|
|
238
|
+
)
|
|
239
|
+
close_inherited_ios if close_others
|
|
240
|
+
Dir.public_send(:chdir, chdir) if chdir
|
|
241
|
+
ENV.clear if unsetenv_others
|
|
242
|
+
env&.each { |key, value| value.nil? ? ENV.delete(key) : ENV[key] = value }
|
|
243
|
+
Landlock.restrict!(**policy) if enforce_landlock && Policy.requested?(**policy)
|
|
244
|
+
Landlock::Native.seccomp_deny_network! if seccomp_deny_network
|
|
245
|
+
Rlimits.apply!(rlimits)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def close_inherited_ios
|
|
249
|
+
ObjectSpace
|
|
250
|
+
.each_object(IO)
|
|
251
|
+
.to_a
|
|
252
|
+
.each do |io|
|
|
253
|
+
next if io.closed? || io.fileno <= 2
|
|
254
|
+
|
|
255
|
+
io.close
|
|
256
|
+
rescue IOError
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
Landlock::Native.close_inherited_fds!
|
|
260
|
+
end
|
|
169
261
|
end
|
|
170
262
|
end
|
|
171
263
|
end
|
data/lib/landlock/runner.rb
CHANGED
|
@@ -16,11 +16,19 @@ module Landlock
|
|
|
16
16
|
env ? [env, *argv_for_exec(argv), exec_options] : [*argv_for_exec(argv), exec_options]
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def exit_child!(error)
|
|
20
|
-
|
|
19
|
+
def exit_child!(error, stderr: STDERR)
|
|
20
|
+
stderr.puts "Landlock child setup failed: #{error.class}: #{error.message}"
|
|
21
|
+
stderr.flush
|
|
21
22
|
ensure
|
|
22
23
|
exit! 127
|
|
23
24
|
end
|
|
25
|
+
|
|
26
|
+
def exit_forked_block!(error, stderr: STDERR)
|
|
27
|
+
stderr.puts "Landlock forked block failed: #{error.class}: #{error.message}"
|
|
28
|
+
stderr.flush
|
|
29
|
+
ensure
|
|
30
|
+
exit! 1
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
data/lib/landlock/version.rb
CHANGED
data/lib/landlock.rb
CHANGED