landlock 0.4.1 → 0.5
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 +36 -0
- data/ext/landlock/landlock.c +110 -0
- data/lib/landlock/execution.rb +94 -14
- 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: 5bfc5c69234e004834818ba52aab192c9219ff0a398d40ada62c4f45be461cdb
|
|
4
|
+
data.tar.gz: 6ec4b746a959a8f68c5ba11e1c4b7342d0a4facda031a8627f05ae6fedc6c6cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efd09558eb3b207ca4be70d4a6509b39338f9cc6374f6422a0fbeee87f2fb8fbb99dd4095044a27f94355062da6016672719a64ab3af78d8d89f622b3b0bb64b
|
|
7
|
+
data.tar.gz: f7e6d2ebf580fd200fa05217d3266f4a48e93d226757e4a3d9c409d22a844c3f6245fb81956201a4b03c3b8ced3ddd29c169653367efa007a8cd1c71bc833048
|
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] - 2026-09-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Add `Landlock.fork` for running a sandboxed Ruby block in a supervised forked child.
|
|
12
|
+
|
|
7
13
|
## [0.4.1] - 2026-08-20
|
|
8
14
|
|
|
9
15
|
### Fixed
|
data/README.md
CHANGED
|
@@ -135,6 +135,42 @@ 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 Linux 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 only when the Linux kernel has no Landlock ABI. It skips only Landlock policy enforcement; fork supervision, timeout handling, environment changes, descriptor closing, rlimits, output capture, and seccomp remain active. It is never selected implicitly, and non-Linux systems still raise `Landlock::UnsupportedError`. When fallback is 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. `Landlock.fork` requires an actual restriction. 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` for this sweep 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
|
+
|
|
170
|
+
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
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
138
174
|
## Restrict current process
|
|
139
175
|
|
|
140
176
|
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,14 @@
|
|
|
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
|
+
#ifdef __linux__
|
|
9
|
+
#include <dirent.h>
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#endif
|
|
12
|
+
|
|
7
13
|
static VALUE mLandlock;
|
|
8
14
|
static VALUE eLandlockError;
|
|
9
15
|
static VALUE eSyscallError;
|
|
@@ -121,6 +127,49 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
|
|
|
121
127
|
return Qnil;
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
131
|
+
/* The forked child keeps running Ruby, so interpreter-reserved descriptors
|
|
132
|
+
* must survive. This rules out close_range across the entire descriptor table. */
|
|
133
|
+
#ifdef __linux__
|
|
134
|
+
DIR *dir = opendir("/proc/self/fd");
|
|
135
|
+
if (!dir) {
|
|
136
|
+
raise_syscall_error("opendir(/proc/self/fd)");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
int dir_fd = dirfd(dir);
|
|
140
|
+
struct dirent *entry;
|
|
141
|
+
for (;;) {
|
|
142
|
+
errno = 0;
|
|
143
|
+
entry = readdir(dir);
|
|
144
|
+
if (!entry) {
|
|
145
|
+
if (errno != 0) {
|
|
146
|
+
int saved_errno = errno;
|
|
147
|
+
closedir(dir);
|
|
148
|
+
errno = saved_errno;
|
|
149
|
+
raise_syscall_error("readdir(/proc/self/fd)");
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
char *end = NULL;
|
|
155
|
+
errno = 0;
|
|
156
|
+
long fd = strtol(entry->d_name, &end, 10);
|
|
157
|
+
if (errno == 0 && end && *end == '\0' && fd >= 3 && fd != dir_fd &&
|
|
158
|
+
!rb_reserved_fd_p((int)fd)) {
|
|
159
|
+
close((int)fd);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (closedir(dir) != 0) {
|
|
163
|
+
raise_syscall_error("closedir(/proc/self/fd)");
|
|
164
|
+
}
|
|
165
|
+
return Qtrue;
|
|
166
|
+
#else
|
|
167
|
+
errno = ENOSYS;
|
|
168
|
+
raise_syscall_error("opendir(/proc/self/fd)");
|
|
169
|
+
return Qnil;
|
|
170
|
+
#endif
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
|
125
174
|
#ifdef SYS_pidfd_open
|
|
126
175
|
int fd = syscall(SYS_pidfd_open, NUM2PIDT(pid_value), 0);
|
|
@@ -135,6 +184,62 @@ static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
|
|
135
184
|
#endif
|
|
136
185
|
}
|
|
137
186
|
|
|
187
|
+
/* Runs after the worker has become its own process-group leader. */
|
|
188
|
+
static void terminate_own_process_group(int signal_number) {
|
|
189
|
+
(void)signal_number;
|
|
190
|
+
kill(0, SIGKILL);
|
|
191
|
+
_exit(0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static VALUE rb_ll_arm_parent_death_process_group(VALUE self, VALUE parent_pid_value) {
|
|
195
|
+
#ifdef __linux__
|
|
196
|
+
pid_t parent_pid = NUM2PIDT(parent_pid_value);
|
|
197
|
+
/* Leave the first two application-visible realtime signals available to callers. */
|
|
198
|
+
int parent_death_signal = SIGRTMIN + 2;
|
|
199
|
+
|
|
200
|
+
struct sigaction action;
|
|
201
|
+
memset(&action, 0, sizeof(action));
|
|
202
|
+
action.sa_handler = terminate_own_process_group;
|
|
203
|
+
sigemptyset(&action.sa_mask);
|
|
204
|
+
if (sigaction(parent_death_signal, &action, NULL) != 0) {
|
|
205
|
+
raise_syscall_error("sigaction(parent death process group)");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
sigset_t signals;
|
|
209
|
+
sigemptyset(&signals);
|
|
210
|
+
sigaddset(&signals, parent_death_signal);
|
|
211
|
+
if (sigprocmask(SIG_UNBLOCK, &signals, NULL) != 0) {
|
|
212
|
+
raise_syscall_error("sigprocmask(parent death process group)");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (prctl(PR_SET_PDEATHSIG, parent_death_signal) != 0) {
|
|
216
|
+
raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (getppid() != parent_pid) {
|
|
220
|
+
terminate_own_process_group(parent_death_signal);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return Qtrue;
|
|
224
|
+
#else
|
|
225
|
+
errno = ENOSYS;
|
|
226
|
+
raise_syscall_error("parent death process group");
|
|
227
|
+
return Qnil;
|
|
228
|
+
#endif
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static VALUE rb_ll_set_parent_death_signal(VALUE self) {
|
|
232
|
+
#ifdef __linux__
|
|
233
|
+
if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) {
|
|
234
|
+
raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
|
|
235
|
+
}
|
|
236
|
+
return Qtrue;
|
|
237
|
+
#else
|
|
238
|
+
errno = ENOSYS;
|
|
239
|
+
raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
|
|
240
|
+
#endif
|
|
241
|
+
}
|
|
242
|
+
|
|
138
243
|
static VALUE rb_ll_seccomp_deny_network(VALUE self) {
|
|
139
244
|
const char *error_message = "seccomp(SECCOMP_SET_MODE_FILTER)";
|
|
140
245
|
if (rb_landlock_seccomp_deny_network(&error_message) != 0) {
|
|
@@ -164,7 +269,12 @@ void Init_landlock(void) {
|
|
|
164
269
|
rb_define_singleton_method(mLandlock, "_add_net_rule", rb_ll_add_net_rule, 3);
|
|
165
270
|
rb_define_singleton_method(mLandlock, "_restrict_self", rb_ll_restrict_self, 1);
|
|
166
271
|
rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
|
|
272
|
+
rb_define_singleton_method(mLandlock, "_close_inherited_fds", rb_ll_close_inherited_fds, 0);
|
|
167
273
|
rb_define_singleton_method(mLandlock, "_pidfd_open", rb_ll_pidfd_open, 1);
|
|
274
|
+
rb_define_singleton_method(mLandlock, "_arm_parent_death_process_group",
|
|
275
|
+
rb_ll_arm_parent_death_process_group, 1);
|
|
276
|
+
rb_define_singleton_method(mLandlock, "_set_parent_death_signal", rb_ll_set_parent_death_signal,
|
|
277
|
+
0);
|
|
168
278
|
rb_define_singleton_method(mLandlock, "seccomp_deny_network!", rb_ll_seccomp_deny_network, 0);
|
|
169
279
|
|
|
170
280
|
rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
|
data/lib/landlock/execution.rb
CHANGED
|
@@ -80,6 +80,36 @@ 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 = Native.abi_version.positive?
|
|
90
|
+
if !enforce_landlock && (on_unsupported == :raise || !RUBY_PLATFORM.include?("linux"))
|
|
91
|
+
raise UnsupportedError, "Linux Landlock is unavailable"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
capture_options = prepare_capture_options(**options, require_landlock: enforce_landlock)
|
|
95
|
+
validate_fallback_restriction!(**capture_options) if !enforce_landlock
|
|
96
|
+
|
|
97
|
+
Runner::Fork.call_block(
|
|
98
|
+
**capture_options,
|
|
99
|
+
enforce_landlock:,
|
|
100
|
+
&block
|
|
101
|
+
)
|
|
102
|
+
rescue OutputTooLargeError => error
|
|
103
|
+
result = error.result
|
|
104
|
+
raise CommandError.new(
|
|
105
|
+
error.message,
|
|
106
|
+
stdout: result&.stdout.to_s,
|
|
107
|
+
stderr: result&.stderr.to_s,
|
|
108
|
+
status: result&.status,
|
|
109
|
+
result:
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
83
113
|
def capture_with(
|
|
84
114
|
argv,
|
|
85
115
|
read: nil,
|
|
@@ -105,31 +135,30 @@ module Landlock
|
|
|
105
135
|
raise_on_failure:
|
|
106
136
|
)
|
|
107
137
|
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,
|
|
138
|
+
options =
|
|
139
|
+
prepare_capture_options(
|
|
140
|
+
read:,
|
|
141
|
+
write:,
|
|
142
|
+
execute:,
|
|
143
|
+
connect_tcp:,
|
|
144
|
+
bind_tcp:,
|
|
145
|
+
paths:,
|
|
146
|
+
scope:,
|
|
121
147
|
chdir:,
|
|
122
148
|
env:,
|
|
123
149
|
unsetenv_others:,
|
|
124
150
|
close_others:,
|
|
151
|
+
allow_all_known:,
|
|
125
152
|
timeout:,
|
|
126
153
|
stdin:,
|
|
127
|
-
rlimits
|
|
154
|
+
rlimits:,
|
|
128
155
|
seccomp_deny_network:,
|
|
129
156
|
max_output_bytes:,
|
|
130
157
|
truncate_output:
|
|
131
158
|
)
|
|
132
159
|
|
|
160
|
+
result = call_with_runner(argv, **options)
|
|
161
|
+
|
|
133
162
|
if raise_on_failure &&
|
|
134
163
|
(result.timed_out? || !result.status.exited? || !success_status_codes.include?(result.status.exitstatus))
|
|
135
164
|
message = [argv.join(" "), failure_message, result.stderr].filter { |part| part.to_s != "" }.join("\n")
|
|
@@ -149,6 +178,51 @@ module Landlock
|
|
|
149
178
|
)
|
|
150
179
|
end
|
|
151
180
|
|
|
181
|
+
def prepare_capture_options(
|
|
182
|
+
read: nil,
|
|
183
|
+
write: nil,
|
|
184
|
+
execute: nil,
|
|
185
|
+
connect_tcp: nil,
|
|
186
|
+
bind_tcp: nil,
|
|
187
|
+
paths: nil,
|
|
188
|
+
scope: nil,
|
|
189
|
+
chdir: nil,
|
|
190
|
+
env: nil,
|
|
191
|
+
unsetenv_others: false,
|
|
192
|
+
close_others: true,
|
|
193
|
+
allow_all_known: false,
|
|
194
|
+
timeout: nil,
|
|
195
|
+
stdin: nil,
|
|
196
|
+
rlimits: {},
|
|
197
|
+
seccomp_deny_network: false,
|
|
198
|
+
max_output_bytes: nil,
|
|
199
|
+
truncate_output: false,
|
|
200
|
+
require_landlock: true
|
|
201
|
+
)
|
|
202
|
+
ensure_landlock_supported! if require_landlock
|
|
203
|
+
max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
|
|
204
|
+
timeout = Validation.validate_timeout!(timeout)
|
|
205
|
+
rlimits = Rlimits.normalize(rlimits)
|
|
206
|
+
env = Env.normalize(env)
|
|
207
|
+
policy =
|
|
208
|
+
prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
|
|
209
|
+
validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
**policy,
|
|
213
|
+
chdir:,
|
|
214
|
+
env:,
|
|
215
|
+
unsetenv_others:,
|
|
216
|
+
close_others:,
|
|
217
|
+
timeout:,
|
|
218
|
+
stdin:,
|
|
219
|
+
rlimits:,
|
|
220
|
+
seccomp_deny_network:,
|
|
221
|
+
max_output_bytes:,
|
|
222
|
+
truncate_output:
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
152
226
|
def spawn_with_runner(argv, **options)
|
|
153
227
|
if Runner::Native.available?
|
|
154
228
|
begin
|
|
@@ -199,6 +273,12 @@ module Landlock
|
|
|
199
273
|
raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
|
|
200
274
|
end
|
|
201
275
|
|
|
276
|
+
def validate_fallback_restriction!(seccomp_deny_network:, rlimits:, **)
|
|
277
|
+
return if seccomp_deny_network || rlimits.any?
|
|
278
|
+
|
|
279
|
+
raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
|
|
280
|
+
end
|
|
281
|
+
|
|
202
282
|
def validate_capture_restriction!(
|
|
203
283
|
read:,
|
|
204
284
|
write:,
|
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
|
|
143
|
+
Landlock::Native.arm_parent_death_process_group!(parent_pid)
|
|
144
|
+
else
|
|
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