landlock 0.5.1 → 0.6.0
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 +10 -0
- data/README.md +5 -3
- data/ext/landlock/bin/safe_exec_helper.c +11 -0
- data/ext/landlock/extconf.rb +2 -2
- data/ext/landlock/landlock.c +28 -0
- data/ext/landlock/seccomp_deny_child_processes.h +52 -0
- data/lib/landlock/execution.rb +14 -6
- data/lib/landlock/native.rb +8 -0
- data/lib/landlock/process_io.rb +14 -22
- data/lib/landlock/runner/fork.rb +8 -2
- data/lib/landlock/runner/native.rb +6 -0
- data/lib/landlock/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a98542d0874b26516a80943ad75765373f5d244278753a2da572c6455f5f104f
|
|
4
|
+
data.tar.gz: c3c872446787663432a661431584718c91e4407e051854ed46cdfe90888f83b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13445eecb00b8d57ec553f99db67084608ab9aac280b27071f8b1c3dcddf50918b0a2b6f2c86f0f5629b56e59679615d796944dcfb4f779f59ba73c5f0175d52
|
|
7
|
+
data.tar.gz: fe97fb721314537abb9dd71c58426443165488b967c5d28f165f8da624ceff5522f3a18f5ebc48e1da384bf928b1f8c58b32be739963881056159cc4b9e92420
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Add `seccomp_deny_child_processes: true` to `capture`, `capture!`, and `fork` to deny child processes.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Clean up remaining process-group members before `Landlock.capture`, `Landlock.capture!`, or `Landlock.fork` returns after normal completion.
|
|
16
|
+
|
|
7
17
|
## [0.5.1] - 2026-09-09
|
|
8
18
|
|
|
9
19
|
### Fixed
|
data/README.md
CHANGED
|
@@ -104,7 +104,7 @@ stdout, stderr, status = Landlock.capture(
|
|
|
104
104
|
|
|
105
105
|
`Landlock.capture!` has the same return shape for successful commands, but raises `Landlock::CommandError` for unsuccessful statuses. The error also exposes `stdout`, `stderr`, `status`, and `result`.
|
|
106
106
|
|
|
107
|
-
`Landlock.capture` requires an actual restriction: provide Landlock rules, `seccomp_deny_network: true`, or `rlimits:`. This avoids accidentally running a command completely unsandboxed when a dynamically built policy is empty. It also requires Linux Landlock support and raises `Landlock::UnsupportedError` when unavailable; it does not fall back to running the command unsandboxed.
|
|
107
|
+
`Landlock.capture` requires an actual restriction: provide Landlock rules, `seccomp_deny_network: true`, `seccomp_deny_child_processes: true`, or `rlimits:`. This avoids accidentally running a command completely unsandboxed when a dynamically built policy is empty. It also requires Linux Landlock support and raises `Landlock::UnsupportedError` when unavailable; it does not fall back to running the command unsandboxed.
|
|
108
108
|
|
|
109
109
|
Pass `stdin:` when a tool should read from standard input instead of a file:
|
|
110
110
|
|
|
@@ -125,6 +125,7 @@ Capture options:
|
|
|
125
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.
|
|
126
126
|
- `scope:` — Landlock ABI v6+ scopes such as `:signal` and `:abstract_unix_socket`.
|
|
127
127
|
- `seccomp_deny_network:` — additionally deny common Linux network syscalls with seccomp. This is Linux-specific and intended as defense in depth.
|
|
128
|
+
- `seccomp_deny_child_processes:` — deny process creation with seccomp. Defaults to `false`. This denies `fork`, `vfork`, and `clone` without `CLONE_THREAD`. `clone3` returns `ENOSYS` so compatible runtimes can fall back to `clone`. Runtimes that require `clone3` without fallback are unsupported. Filter installation failure prevents the command or block from running.
|
|
128
129
|
- `rlimits:` — resource limits. Supported keys are `:cpu_seconds`, `:memory_bytes`, `:file_size_bytes`, `:open_files`, and `:processes`. Values must be non-negative integers.
|
|
129
130
|
- `timeout:` — wall-clock timeout in seconds. On timeout capture terminates the process group and returns/raises with `result.timed_out?` true.
|
|
130
131
|
- `max_output_bytes:` — combined stdout+stderr byte limit. With `truncate_output: false`, exceeding the limit raises `Landlock::CommandError` with the partial output captured before termination. With `truncate_output: true`, output is truncated and `result.output_truncated?` is true.
|
|
@@ -144,7 +145,8 @@ result = Landlock.fork(
|
|
|
144
145
|
read: [input_path],
|
|
145
146
|
timeout: 5,
|
|
146
147
|
rlimits: { cpu_seconds: 5, memory_bytes: 512 * 1024 * 1024 },
|
|
147
|
-
seccomp_deny_network: true
|
|
148
|
+
seccomp_deny_network: true,
|
|
149
|
+
seccomp_deny_child_processes: true
|
|
148
150
|
) do |stdout, _stderr|
|
|
149
151
|
stdout.write(calculate_dominant_color(input_path))
|
|
150
152
|
end
|
|
@@ -167,7 +169,7 @@ result = Landlock.fork(
|
|
|
167
169
|
|
|
168
170
|
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
171
|
|
|
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.
|
|
172
|
+
Fallback is never selected implicitly. When active, the call must include `seccomp_deny_network: true`, `seccomp_deny_child_processes: 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
173
|
|
|
172
174
|
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
175
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "../landlock_native.h"
|
|
2
2
|
#include "../seccomp_deny_network.h"
|
|
3
|
+
#include "../seccomp_deny_child_processes.h"
|
|
3
4
|
|
|
4
5
|
#include <stdio.h>
|
|
5
6
|
#include <stdlib.h>
|
|
@@ -549,6 +550,7 @@ int main(int argc, char **argv) {
|
|
|
549
550
|
string_list read_paths = {0}, write_paths = {0}, execute_paths = {0}, rlimit_specs = {0};
|
|
550
551
|
path_rule_list path_rules = {0};
|
|
551
552
|
ull_list connect_ports = {0}, bind_ports = {0};
|
|
553
|
+
int seccomp_deny_child_processes = 0;
|
|
552
554
|
int seccomp_deny_network = 0, allow_all_known = 0, close_others = 1;
|
|
553
555
|
int handled_read = 0, handled_write = 0, handled_execute = 0;
|
|
554
556
|
int handled_connect = 0, handled_bind = 0;
|
|
@@ -582,6 +584,8 @@ int main(int argc, char **argv) {
|
|
|
582
584
|
chdir_path = require_arg(argc, argv, &i);
|
|
583
585
|
} else if (strcmp(argv[i], "--rlimit") == 0) {
|
|
584
586
|
string_list_push(&rlimit_specs, require_arg(argc, argv, &i));
|
|
587
|
+
} else if (strcmp(argv[i], "--seccomp-deny-child-processes") == 0) {
|
|
588
|
+
seccomp_deny_child_processes = 1;
|
|
585
589
|
} else if (strcmp(argv[i], "--seccomp-deny-network") == 0) {
|
|
586
590
|
seccomp_deny_network = 1;
|
|
587
591
|
} else if (strcmp(argv[i], "--allow-all-known") == 0) {
|
|
@@ -616,6 +620,13 @@ int main(int argc, char **argv) {
|
|
|
616
620
|
apply_rlimit(rlimit_specs.items[i]);
|
|
617
621
|
}
|
|
618
622
|
|
|
623
|
+
if (seccomp_deny_child_processes) {
|
|
624
|
+
const char *error_message;
|
|
625
|
+
if (rb_landlock_seccomp_deny_child_processes(&error_message) != 0) {
|
|
626
|
+
die(error_message);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
619
630
|
execvp(command_argv[0], command_argv);
|
|
620
631
|
perror("execvp");
|
|
621
632
|
_exit(127);
|
data/ext/landlock/extconf.rb
CHANGED
|
@@ -19,7 +19,7 @@ create_makefile("landlock/landlock")
|
|
|
19
19
|
if RUBY_PLATFORM.include?("linux")
|
|
20
20
|
helper = "landlock-safe-exec"
|
|
21
21
|
helper_src = "$(srcdir)/bin/safe_exec_helper.c"
|
|
22
|
-
helper_headers = "$(srcdir)/landlock_native.h $(srcdir)/seccomp_deny_network.h"
|
|
22
|
+
helper_headers = "$(srcdir)/landlock_native.h $(srcdir)/seccomp_deny_network.h $(srcdir)/seccomp_deny_child_processes.h"
|
|
23
23
|
helper_dest = "$(RUBYARCHDIR)/#{helper}"
|
|
24
24
|
|
|
25
25
|
File.open("Makefile", "a") do |makefile|
|
|
@@ -27,7 +27,7 @@ if RUBY_PLATFORM.include?("linux")
|
|
|
27
27
|
|
|
28
28
|
all: #{helper}
|
|
29
29
|
|
|
30
|
-
#{helper}: #{helper_src}
|
|
30
|
+
#{helper}: #{helper_src} #{helper_headers}
|
|
31
31
|
\t$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) #{helper_src} -o #{helper}
|
|
32
32
|
|
|
33
33
|
install: install-#{helper}
|
data/ext/landlock/landlock.c
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#include "ruby.h"
|
|
2
2
|
#include "landlock_native.h"
|
|
3
3
|
#include "seccomp_deny_network.h"
|
|
4
|
+
#include "seccomp_deny_child_processes.h"
|
|
4
5
|
|
|
5
6
|
#include <signal.h>
|
|
6
7
|
#include <string.h>
|
|
8
|
+
#include <sys/wait.h>
|
|
7
9
|
|
|
8
10
|
#include <dirent.h>
|
|
9
11
|
#include <stdlib.h>
|
|
@@ -168,6 +170,21 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
|
168
170
|
return Qtrue;
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
static VALUE rb_ll_child_exited(VALUE self, VALUE pid_value) {
|
|
174
|
+
siginfo_t info = {0};
|
|
175
|
+
int result;
|
|
176
|
+
do {
|
|
177
|
+
result = waitid(P_PID, NUM2PIDT(pid_value), &info, WEXITED | WNOHANG | WNOWAIT);
|
|
178
|
+
} while (result < 0 && errno == EINTR);
|
|
179
|
+
if (result < 0 && errno == ECHILD) {
|
|
180
|
+
return Qnil;
|
|
181
|
+
}
|
|
182
|
+
if (result < 0) {
|
|
183
|
+
raise_syscall_error("waitid");
|
|
184
|
+
}
|
|
185
|
+
return info.si_pid == 0 ? Qfalse : Qtrue;
|
|
186
|
+
}
|
|
187
|
+
|
|
171
188
|
static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
|
172
189
|
#ifdef SYS_pidfd_open
|
|
173
190
|
int fd = syscall(SYS_pidfd_open, NUM2PIDT(pid_value), 0);
|
|
@@ -246,6 +263,14 @@ static VALUE rb_ll_seccomp_deny_network(VALUE self) {
|
|
|
246
263
|
return Qtrue;
|
|
247
264
|
}
|
|
248
265
|
|
|
266
|
+
static VALUE rb_ll_seccomp_deny_child_processes(VALUE self) {
|
|
267
|
+
const char *error_message;
|
|
268
|
+
if (rb_landlock_seccomp_deny_child_processes(&error_message) != 0) {
|
|
269
|
+
raise_syscall_error(error_message);
|
|
270
|
+
}
|
|
271
|
+
return Qtrue;
|
|
272
|
+
}
|
|
273
|
+
|
|
249
274
|
void Init_landlock(void) {
|
|
250
275
|
mLandlock = rb_define_module("Landlock");
|
|
251
276
|
|
|
@@ -269,11 +294,14 @@ void Init_landlock(void) {
|
|
|
269
294
|
rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
|
|
270
295
|
rb_define_singleton_method(mLandlock, "_close_inherited_fds", rb_ll_close_inherited_fds, 0);
|
|
271
296
|
rb_define_singleton_method(mLandlock, "_pidfd_open", rb_ll_pidfd_open, 1);
|
|
297
|
+
rb_define_singleton_method(mLandlock, "_child_exited", rb_ll_child_exited, 1);
|
|
272
298
|
rb_define_singleton_method(mLandlock, "_arm_parent_death_process_group",
|
|
273
299
|
rb_ll_arm_parent_death_process_group, 1);
|
|
274
300
|
rb_define_singleton_method(mLandlock, "_set_parent_death_signal", rb_ll_set_parent_death_signal,
|
|
275
301
|
0);
|
|
276
302
|
rb_define_singleton_method(mLandlock, "seccomp_deny_network!", rb_ll_seccomp_deny_network, 0);
|
|
303
|
+
rb_define_singleton_method(mLandlock, "seccomp_deny_child_processes!",
|
|
304
|
+
rb_ll_seccomp_deny_child_processes, 0);
|
|
277
305
|
|
|
278
306
|
rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
|
|
279
307
|
rb_define_const(mLandlock, "ACCESS_FS_WRITE_FILE", ULL2NUM(LANDLOCK_ACCESS_FS_WRITE_FILE));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#ifndef RB_LANDLOCK_SECCOMP_DENY_CHILD_PROCESSES_H
|
|
2
|
+
#define RB_LANDLOCK_SECCOMP_DENY_CHILD_PROCESSES_H
|
|
3
|
+
|
|
4
|
+
#include "seccomp_deny_network.h"
|
|
5
|
+
#ifdef __linux__
|
|
6
|
+
#include <sched.h>
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
static int rb_landlock_seccomp_deny_child_processes(const char **error_message) {
|
|
10
|
+
#if defined(__linux__) && defined(RB_LANDLOCK_EXPECTED_AUDIT_ARCH) && defined(__NR_clone3) && \
|
|
11
|
+
!(defined(__x86_64__) && defined(__ILP32__))
|
|
12
|
+
struct sock_filter filter[] = {
|
|
13
|
+
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
|
|
14
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, RB_LANDLOCK_EXPECTED_AUDIT_ARCH, 1, 0),
|
|
15
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
|
|
16
|
+
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
|
|
17
|
+
#ifdef RB_LANDLOCK_DENY_X32_SYSCALLS
|
|
18
|
+
BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, __X32_SYSCALL_BIT, 0, 1),
|
|
19
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
|
|
20
|
+
#endif
|
|
21
|
+
#ifdef __NR_fork
|
|
22
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fork, 0, 1),
|
|
23
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
|
|
24
|
+
#endif
|
|
25
|
+
#ifdef __NR_vfork
|
|
26
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_vfork, 0, 1),
|
|
27
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
|
|
28
|
+
#endif
|
|
29
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone3, 0, 1),
|
|
30
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ENOSYS),
|
|
31
|
+
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone, 1, 0),
|
|
32
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
|
|
33
|
+
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])),
|
|
34
|
+
BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, CLONE_THREAD, 1, 0),
|
|
35
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
|
|
36
|
+
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
|
|
37
|
+
};
|
|
38
|
+
struct sock_fprog program = {.len = sizeof(filter) / sizeof(filter[0]), .filter = filter};
|
|
39
|
+
*error_message = "prctl(PR_SET_NO_NEW_PRIVS)";
|
|
40
|
+
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
*error_message = "seccomp deny child processes";
|
|
44
|
+
return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program);
|
|
45
|
+
#else
|
|
46
|
+
*error_message = "seccomp deny child processes unsupported architecture or syscall headers";
|
|
47
|
+
errno = ENOSYS;
|
|
48
|
+
return -1;
|
|
49
|
+
#endif
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#endif
|
data/lib/landlock/execution.rb
CHANGED
|
@@ -122,6 +122,7 @@ module Landlock
|
|
|
122
122
|
stdin: nil,
|
|
123
123
|
rlimits: {},
|
|
124
124
|
seccomp_deny_network: false,
|
|
125
|
+
seccomp_deny_child_processes: false,
|
|
125
126
|
max_output_bytes: nil,
|
|
126
127
|
truncate_output: false,
|
|
127
128
|
success_status_codes: [0],
|
|
@@ -147,6 +148,7 @@ module Landlock
|
|
|
147
148
|
stdin:,
|
|
148
149
|
rlimits:,
|
|
149
150
|
seccomp_deny_network:,
|
|
151
|
+
seccomp_deny_child_processes:,
|
|
150
152
|
max_output_bytes:,
|
|
151
153
|
truncate_output:
|
|
152
154
|
)
|
|
@@ -189,11 +191,15 @@ module Landlock
|
|
|
189
191
|
stdin: nil,
|
|
190
192
|
rlimits: {},
|
|
191
193
|
seccomp_deny_network: false,
|
|
194
|
+
seccomp_deny_child_processes: false,
|
|
192
195
|
max_output_bytes: nil,
|
|
193
196
|
truncate_output: false,
|
|
194
197
|
require_landlock: true
|
|
195
198
|
)
|
|
196
199
|
ensure_landlock_supported! if require_landlock
|
|
200
|
+
if seccomp_deny_child_processes && !RUBY_PLATFORM.include?("linux")
|
|
201
|
+
raise UnsupportedError, "seccomp_deny_child_processes requires Linux"
|
|
202
|
+
end
|
|
197
203
|
max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
|
|
198
204
|
timeout = Validation.validate_timeout!(timeout)
|
|
199
205
|
rlimits = Rlimits.normalize(rlimits)
|
|
@@ -211,7 +217,7 @@ module Landlock
|
|
|
211
217
|
allow_all_known:,
|
|
212
218
|
abi: require_landlock ? Native.abi_version : 0
|
|
213
219
|
)
|
|
214
|
-
validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
|
|
220
|
+
validate_capture_restriction!(**policy, seccomp_deny_network:, seccomp_deny_child_processes:, rlimits:)
|
|
215
221
|
|
|
216
222
|
{
|
|
217
223
|
**policy,
|
|
@@ -223,6 +229,7 @@ module Landlock
|
|
|
223
229
|
stdin:,
|
|
224
230
|
rlimits:,
|
|
225
231
|
seccomp_deny_network:,
|
|
232
|
+
seccomp_deny_child_processes:,
|
|
226
233
|
max_output_bytes:,
|
|
227
234
|
truncate_output:
|
|
228
235
|
}
|
|
@@ -289,14 +296,14 @@ module Landlock
|
|
|
289
296
|
raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
|
|
290
297
|
end
|
|
291
298
|
|
|
292
|
-
def validate_fallback_restriction!(seccomp_deny_network:, rlimits:, **)
|
|
299
|
+
def validate_fallback_restriction!(seccomp_deny_network:, seccomp_deny_child_processes:, rlimits:, **)
|
|
293
300
|
if seccomp_deny_network && !RUBY_PLATFORM.include?("linux")
|
|
294
301
|
raise UnsupportedError, "seccomp_deny_network requires Linux"
|
|
295
302
|
end
|
|
296
303
|
|
|
297
|
-
return if seccomp_deny_network || rlimits.any?
|
|
304
|
+
return if seccomp_deny_network || seccomp_deny_child_processes || rlimits.any?
|
|
298
305
|
|
|
299
|
-
raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
|
|
306
|
+
raise ArgumentError, "Landlock fallback requires seccomp_deny_network, seccomp_deny_child_processes, or rlimits"
|
|
300
307
|
end
|
|
301
308
|
|
|
302
309
|
def validate_capture_restriction!(
|
|
@@ -309,13 +316,14 @@ module Landlock
|
|
|
309
316
|
scope:,
|
|
310
317
|
allow_all_known:,
|
|
311
318
|
seccomp_deny_network:,
|
|
319
|
+
seccomp_deny_child_processes:,
|
|
312
320
|
rlimits:
|
|
313
321
|
)
|
|
314
322
|
return if Policy.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
315
|
-
return if seccomp_deny_network
|
|
323
|
+
return if seccomp_deny_network || seccomp_deny_child_processes
|
|
316
324
|
return if Array(rlimits).any?
|
|
317
325
|
|
|
318
|
-
raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, or rlimits"
|
|
326
|
+
raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, seccomp_deny_child_processes, or rlimits"
|
|
319
327
|
end
|
|
320
328
|
|
|
321
329
|
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
|
data/lib/landlock/native.rb
CHANGED
|
@@ -35,6 +35,10 @@ module Landlock
|
|
|
35
35
|
Landlock.__send__(:_close_inherited_fds)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def child_exited?(pid)
|
|
39
|
+
Landlock.__send__(:_child_exited, pid)
|
|
40
|
+
end
|
|
41
|
+
|
|
38
42
|
def pidfd_open(pid)
|
|
39
43
|
Landlock.__send__(:_pidfd_open, pid)
|
|
40
44
|
end
|
|
@@ -50,5 +54,9 @@ module Landlock
|
|
|
50
54
|
def seccomp_deny_network!
|
|
51
55
|
Landlock.seccomp_deny_network!
|
|
52
56
|
end
|
|
57
|
+
|
|
58
|
+
def seccomp_deny_child_processes!
|
|
59
|
+
Landlock.seccomp_deny_child_processes!
|
|
60
|
+
end
|
|
53
61
|
end
|
|
54
62
|
end
|
data/lib/landlock/process_io.rb
CHANGED
|
@@ -116,11 +116,7 @@ module Landlock
|
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
status, timed_out = wait_for_pid_until(pid, deadline:)
|
|
121
|
-
else
|
|
122
|
-
status = wait_for_pid(pid)
|
|
123
|
-
end
|
|
119
|
+
status, timed_out = wait_for_pid_until(pid, deadline:)
|
|
124
120
|
|
|
125
121
|
if timed_out
|
|
126
122
|
drain_streams_until(
|
|
@@ -138,8 +134,8 @@ module Landlock
|
|
|
138
134
|
end
|
|
139
135
|
|
|
140
136
|
def wait_for_pid_until(pid, deadline:)
|
|
141
|
-
remaining = deadline - monotonic_time
|
|
142
|
-
if remaining <= 0
|
|
137
|
+
remaining = deadline ? deadline - monotonic_time : nil
|
|
138
|
+
if remaining && remaining <= 0
|
|
143
139
|
terminate_process(pid)
|
|
144
140
|
return wait_for_pid(pid), true
|
|
145
141
|
end
|
|
@@ -147,11 +143,12 @@ module Landlock
|
|
|
147
143
|
pidfd = Native.pidfd_open(pid)
|
|
148
144
|
pid_monitor = IO.for_fd(pidfd, autoclose: false)
|
|
149
145
|
readable, = IO.select([pid_monitor], nil, nil, remaining)
|
|
150
|
-
if !readable || monotonic_time >= deadline
|
|
146
|
+
if !readable || (deadline && monotonic_time >= deadline)
|
|
151
147
|
terminate_process(pid)
|
|
152
148
|
return wait_for_pid(pid), true
|
|
153
149
|
end
|
|
154
150
|
|
|
151
|
+
terminate_process_group(pid) unless Native.child_exited?(pid).nil?
|
|
155
152
|
[wait_for_pid(pid), false]
|
|
156
153
|
rescue Landlock::SyscallError
|
|
157
154
|
wait_for_pid_until_by_polling(pid, deadline:)
|
|
@@ -163,27 +160,22 @@ module Landlock
|
|
|
163
160
|
|
|
164
161
|
def wait_for_pid_until_by_polling(pid, deadline:)
|
|
165
162
|
loop do
|
|
166
|
-
remaining = deadline - monotonic_time
|
|
167
|
-
if remaining <= 0
|
|
163
|
+
remaining = deadline ? deadline - monotonic_time : nil
|
|
164
|
+
if remaining && remaining <= 0
|
|
168
165
|
terminate_process(pid)
|
|
169
166
|
return wait_for_pid(pid), true
|
|
170
167
|
end
|
|
171
168
|
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
return status, false
|
|
169
|
+
exited = Native.child_exited?(pid)
|
|
170
|
+
return nil, false if exited.nil?
|
|
171
|
+
if exited
|
|
172
|
+
timed_out = deadline && monotonic_time >= deadline
|
|
173
|
+
terminate_process_group(pid)
|
|
174
|
+
return wait_for_pid(pid), !!timed_out
|
|
181
175
|
end
|
|
182
176
|
|
|
183
|
-
IO.select(nil, nil, nil, [remaining, PID_WAIT_FALLBACK_INTERVAL_SECONDS].min)
|
|
177
|
+
IO.select(nil, nil, nil, [remaining, PID_WAIT_FALLBACK_INTERVAL_SECONDS].compact.min)
|
|
184
178
|
end
|
|
185
|
-
rescue Errno::ECHILD
|
|
186
|
-
[nil, false]
|
|
187
179
|
end
|
|
188
180
|
private_class_method :wait_for_pid_until_by_polling
|
|
189
181
|
|
data/lib/landlock/runner/fork.rb
CHANGED
|
@@ -69,6 +69,7 @@ module Landlock
|
|
|
69
69
|
stdin:,
|
|
70
70
|
rlimits:,
|
|
71
71
|
seccomp_deny_network:,
|
|
72
|
+
seccomp_deny_child_processes: false,
|
|
72
73
|
max_output_bytes:,
|
|
73
74
|
truncate_output:
|
|
74
75
|
)
|
|
@@ -88,7 +89,8 @@ module Landlock
|
|
|
88
89
|
close_others:,
|
|
89
90
|
allow_all_known:,
|
|
90
91
|
rlimits:,
|
|
91
|
-
seccomp_deny_network
|
|
92
|
+
seccomp_deny_network:,
|
|
93
|
+
seccomp_deny_child_processes:
|
|
92
94
|
)
|
|
93
95
|
rescue Exception => error
|
|
94
96
|
Runner.exit_child!(error)
|
|
@@ -215,13 +217,15 @@ module Landlock
|
|
|
215
217
|
close_others:,
|
|
216
218
|
allow_all_known:,
|
|
217
219
|
rlimits:,
|
|
218
|
-
seccomp_deny_network
|
|
220
|
+
seccomp_deny_network:,
|
|
221
|
+
seccomp_deny_child_processes: false
|
|
219
222
|
)
|
|
220
223
|
Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir
|
|
221
224
|
if Policy.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
222
225
|
Landlock.restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
223
226
|
end
|
|
224
227
|
Landlock::Native.seccomp_deny_network! if seccomp_deny_network
|
|
228
|
+
Landlock::Native.seccomp_deny_child_processes! if seccomp_deny_child_processes
|
|
225
229
|
Rlimits.apply!(rlimits)
|
|
226
230
|
Kernel.exec(*Runner.kernel_exec_args(argv, env, unsetenv_others:, close_others:))
|
|
227
231
|
end
|
|
@@ -233,6 +237,7 @@ module Landlock
|
|
|
233
237
|
close_others:,
|
|
234
238
|
rlimits:,
|
|
235
239
|
seccomp_deny_network:,
|
|
240
|
+
seccomp_deny_child_processes: false,
|
|
236
241
|
enforce_landlock:,
|
|
237
242
|
**policy
|
|
238
243
|
)
|
|
@@ -242,6 +247,7 @@ module Landlock
|
|
|
242
247
|
env&.each { |key, value| value.nil? ? ENV.delete(key) : ENV[key] = value }
|
|
243
248
|
Landlock.restrict!(**policy) if enforce_landlock && Policy.requested?(**policy)
|
|
244
249
|
Landlock::Native.seccomp_deny_network! if seccomp_deny_network
|
|
250
|
+
Landlock::Native.seccomp_deny_child_processes! if seccomp_deny_child_processes
|
|
245
251
|
Rlimits.apply!(rlimits)
|
|
246
252
|
end
|
|
247
253
|
|
|
@@ -81,6 +81,7 @@ module Landlock
|
|
|
81
81
|
stdin:,
|
|
82
82
|
rlimits:,
|
|
83
83
|
seccomp_deny_network:,
|
|
84
|
+
seccomp_deny_child_processes: false,
|
|
84
85
|
max_output_bytes:,
|
|
85
86
|
truncate_output:
|
|
86
87
|
)
|
|
@@ -105,6 +106,7 @@ module Landlock
|
|
|
105
106
|
allow_all_known:,
|
|
106
107
|
rlimits:,
|
|
107
108
|
seccomp_deny_network:,
|
|
109
|
+
seccomp_deny_child_processes:,
|
|
108
110
|
stdin_reader:,
|
|
109
111
|
stdout_writer:,
|
|
110
112
|
stderr_writer:,
|
|
@@ -156,6 +158,7 @@ module Landlock
|
|
|
156
158
|
allow_all_known:,
|
|
157
159
|
rlimits:,
|
|
158
160
|
seccomp_deny_network:,
|
|
161
|
+
seccomp_deny_child_processes: false,
|
|
159
162
|
stdin_reader: nil,
|
|
160
163
|
stdout_writer: nil,
|
|
161
164
|
stderr_writer: nil,
|
|
@@ -182,6 +185,7 @@ module Landlock
|
|
|
182
185
|
allow_all_known:,
|
|
183
186
|
rlimits:,
|
|
184
187
|
seccomp_deny_network:,
|
|
188
|
+
seccomp_deny_child_processes:,
|
|
185
189
|
close_others:
|
|
186
190
|
)
|
|
187
191
|
env ? ::Process.spawn(env, *spawn_args, spawn_options) : ::Process.spawn(*spawn_args, spawn_options)
|
|
@@ -199,6 +203,7 @@ module Landlock
|
|
|
199
203
|
allow_all_known:,
|
|
200
204
|
rlimits:,
|
|
201
205
|
seccomp_deny_network:,
|
|
206
|
+
seccomp_deny_child_processes: false,
|
|
202
207
|
close_others:
|
|
203
208
|
)
|
|
204
209
|
args = [helper_path]
|
|
@@ -215,6 +220,7 @@ module Landlock
|
|
|
215
220
|
Array(rlimits).each { |key, value| args << "--rlimit" << "#{key}=#{value}" }
|
|
216
221
|
args << "--allow-all-known" if allow_all_known
|
|
217
222
|
args << "--seccomp-deny-network" if seccomp_deny_network
|
|
223
|
+
args << "--seccomp-deny-child-processes" if seccomp_deny_child_processes
|
|
218
224
|
args << "--keep-fds" unless close_others
|
|
219
225
|
args << "--"
|
|
220
226
|
args.concat(argv.map(&:to_s))
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- ext/landlock/extconf.rb
|
|
111
111
|
- ext/landlock/landlock.c
|
|
112
112
|
- ext/landlock/landlock_native.h
|
|
113
|
+
- ext/landlock/seccomp_deny_child_processes.h
|
|
113
114
|
- ext/landlock/seccomp_deny_network.h
|
|
114
115
|
- lib/landlock.rb
|
|
115
116
|
- lib/landlock/env.rb
|