landlock 0.5 → 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 +16 -0
- data/README.md +8 -4
- data/ext/landlock/bin/safe_exec_helper.c +11 -0
- data/ext/landlock/extconf.rb +2 -2
- data/ext/landlock/landlock.c +38 -12
- data/ext/landlock/seccomp_deny_child_processes.h +52 -0
- data/lib/landlock/execution.rb +47 -20
- data/lib/landlock/native.rb +8 -0
- data/lib/landlock/process_io.rb +14 -22
- data/lib/landlock/runner/fork.rb +10 -4
- 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,22 @@ 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
|
+
|
|
17
|
+
## [0.5.1] - 2026-09-09
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Allow `Landlock.fork(on_unsupported: :run_without_landlock)` to fall back when the support check is blocked or unavailable, including on macOS.
|
|
22
|
+
|
|
7
23
|
## [0.5] - 2026-09-01
|
|
8
24
|
|
|
9
25
|
### Added
|
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
|
|
@@ -154,7 +156,7 @@ color = result.stdout if result.success?
|
|
|
154
156
|
|
|
155
157
|
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
158
|
|
|
157
|
-
By default, `Landlock.fork` requires Linux Landlock support and raises `Landlock::UnsupportedError` before forking when the Landlock ABI is unavailable. A
|
|
159
|
+
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
160
|
|
|
159
161
|
```ruby
|
|
160
162
|
result = Landlock.fork(
|
|
@@ -165,7 +167,9 @@ result = Landlock.fork(
|
|
|
165
167
|
) { |stdout, _stderr| stdout.write(run_plugin) }
|
|
166
168
|
```
|
|
167
169
|
|
|
168
|
-
This fallback is used
|
|
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.
|
|
171
|
+
|
|
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.
|
|
169
173
|
|
|
170
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.
|
|
171
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,14 +1,14 @@
|
|
|
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
|
-
#ifdef __linux__
|
|
9
10
|
#include <dirent.h>
|
|
10
11
|
#include <stdlib.h>
|
|
11
|
-
#endif
|
|
12
12
|
|
|
13
13
|
static VALUE mLandlock;
|
|
14
14
|
static VALUE eLandlockError;
|
|
@@ -127,13 +127,18 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
|
|
|
127
127
|
return Qnil;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
#ifdef __linux__
|
|
131
|
+
#define LL_DESCRIPTOR_DIRECTORY "/proc/self/fd"
|
|
132
|
+
#else
|
|
133
|
+
#define LL_DESCRIPTOR_DIRECTORY "/dev/fd"
|
|
134
|
+
#endif
|
|
135
|
+
|
|
130
136
|
static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
131
137
|
/* The forked child keeps running Ruby, so interpreter-reserved descriptors
|
|
132
138
|
* must survive. This rules out close_range across the entire descriptor table. */
|
|
133
|
-
|
|
134
|
-
DIR *dir = opendir("/proc/self/fd");
|
|
139
|
+
DIR *dir = opendir(LL_DESCRIPTOR_DIRECTORY);
|
|
135
140
|
if (!dir) {
|
|
136
|
-
raise_syscall_error("opendir(
|
|
141
|
+
raise_syscall_error("opendir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
int dir_fd = dirfd(dir);
|
|
@@ -146,7 +151,7 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
|
146
151
|
int saved_errno = errno;
|
|
147
152
|
closedir(dir);
|
|
148
153
|
errno = saved_errno;
|
|
149
|
-
raise_syscall_error("readdir(
|
|
154
|
+
raise_syscall_error("readdir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
150
155
|
}
|
|
151
156
|
break;
|
|
152
157
|
}
|
|
@@ -160,14 +165,24 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
|
|
|
160
165
|
}
|
|
161
166
|
}
|
|
162
167
|
if (closedir(dir) != 0) {
|
|
163
|
-
raise_syscall_error("closedir(
|
|
168
|
+
raise_syscall_error("closedir(" LL_DESCRIPTOR_DIRECTORY ")");
|
|
164
169
|
}
|
|
165
170
|
return Qtrue;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
}
|
|
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;
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
|
|
@@ -248,6 +263,14 @@ static VALUE rb_ll_seccomp_deny_network(VALUE self) {
|
|
|
248
263
|
return Qtrue;
|
|
249
264
|
}
|
|
250
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
|
+
|
|
251
274
|
void Init_landlock(void) {
|
|
252
275
|
mLandlock = rb_define_module("Landlock");
|
|
253
276
|
|
|
@@ -271,11 +294,14 @@ void Init_landlock(void) {
|
|
|
271
294
|
rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
|
|
272
295
|
rb_define_singleton_method(mLandlock, "_close_inherited_fds", rb_ll_close_inherited_fds, 0);
|
|
273
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);
|
|
274
298
|
rb_define_singleton_method(mLandlock, "_arm_parent_death_process_group",
|
|
275
299
|
rb_ll_arm_parent_death_process_group, 1);
|
|
276
300
|
rb_define_singleton_method(mLandlock, "_set_parent_death_signal", rb_ll_set_parent_death_signal,
|
|
277
301
|
0);
|
|
278
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);
|
|
279
305
|
|
|
280
306
|
rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
|
|
281
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
|
@@ -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(
|
|
@@ -128,6 +122,7 @@ module Landlock
|
|
|
128
122
|
stdin: nil,
|
|
129
123
|
rlimits: {},
|
|
130
124
|
seccomp_deny_network: false,
|
|
125
|
+
seccomp_deny_child_processes: false,
|
|
131
126
|
max_output_bytes: nil,
|
|
132
127
|
truncate_output: false,
|
|
133
128
|
success_status_codes: [0],
|
|
@@ -153,6 +148,7 @@ module Landlock
|
|
|
153
148
|
stdin:,
|
|
154
149
|
rlimits:,
|
|
155
150
|
seccomp_deny_network:,
|
|
151
|
+
seccomp_deny_child_processes:,
|
|
156
152
|
max_output_bytes:,
|
|
157
153
|
truncate_output:
|
|
158
154
|
)
|
|
@@ -195,18 +191,33 @@ module Landlock
|
|
|
195
191
|
stdin: nil,
|
|
196
192
|
rlimits: {},
|
|
197
193
|
seccomp_deny_network: false,
|
|
194
|
+
seccomp_deny_child_processes: false,
|
|
198
195
|
max_output_bytes: nil,
|
|
199
196
|
truncate_output: false,
|
|
200
197
|
require_landlock: true
|
|
201
198
|
)
|
|
202
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
|
|
203
203
|
max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
|
|
204
204
|
timeout = Validation.validate_timeout!(timeout)
|
|
205
205
|
rlimits = Rlimits.normalize(rlimits)
|
|
206
206
|
env = Env.normalize(env)
|
|
207
207
|
policy =
|
|
208
|
-
prepare_policy(
|
|
209
|
-
|
|
208
|
+
prepare_policy(
|
|
209
|
+
read:,
|
|
210
|
+
write:,
|
|
211
|
+
execute:,
|
|
212
|
+
connect_tcp:,
|
|
213
|
+
bind_tcp:,
|
|
214
|
+
paths:,
|
|
215
|
+
scope:,
|
|
216
|
+
chdir:,
|
|
217
|
+
allow_all_known:,
|
|
218
|
+
abi: require_landlock ? Native.abi_version : 0
|
|
219
|
+
)
|
|
220
|
+
validate_capture_restriction!(**policy, seccomp_deny_network:, seccomp_deny_child_processes:, rlimits:)
|
|
210
221
|
|
|
211
222
|
{
|
|
212
223
|
**policy,
|
|
@@ -218,6 +229,7 @@ module Landlock
|
|
|
218
229
|
stdin:,
|
|
219
230
|
rlimits:,
|
|
220
231
|
seccomp_deny_network:,
|
|
232
|
+
seccomp_deny_child_processes:,
|
|
221
233
|
max_output_bytes:,
|
|
222
234
|
truncate_output:
|
|
223
235
|
}
|
|
@@ -251,10 +263,21 @@ module Landlock
|
|
|
251
263
|
raise UnsupportedError, "Linux Landlock is unavailable" unless Native.abi_version.positive?
|
|
252
264
|
end
|
|
253
265
|
|
|
254
|
-
def prepare_policy(
|
|
266
|
+
def prepare_policy(
|
|
267
|
+
read:,
|
|
268
|
+
write:,
|
|
269
|
+
execute:,
|
|
270
|
+
connect_tcp:,
|
|
271
|
+
bind_tcp:,
|
|
272
|
+
paths:,
|
|
273
|
+
scope:,
|
|
274
|
+
chdir:,
|
|
275
|
+
allow_all_known:,
|
|
276
|
+
abi: Native.abi_version
|
|
277
|
+
)
|
|
255
278
|
connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
|
|
256
279
|
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:)
|
|
280
|
+
read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
|
|
258
281
|
{ read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
|
|
259
282
|
end
|
|
260
283
|
|
|
@@ -273,10 +296,14 @@ module Landlock
|
|
|
273
296
|
raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
|
|
274
297
|
end
|
|
275
298
|
|
|
276
|
-
def validate_fallback_restriction!(seccomp_deny_network:, rlimits:, **)
|
|
277
|
-
|
|
299
|
+
def validate_fallback_restriction!(seccomp_deny_network:, seccomp_deny_child_processes:, rlimits:, **)
|
|
300
|
+
if seccomp_deny_network && !RUBY_PLATFORM.include?("linux")
|
|
301
|
+
raise UnsupportedError, "seccomp_deny_network requires Linux"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
return if seccomp_deny_network || seccomp_deny_child_processes || rlimits.any?
|
|
278
305
|
|
|
279
|
-
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"
|
|
280
307
|
end
|
|
281
308
|
|
|
282
309
|
def validate_capture_restriction!(
|
|
@@ -289,18 +316,18 @@ module Landlock
|
|
|
289
316
|
scope:,
|
|
290
317
|
allow_all_known:,
|
|
291
318
|
seccomp_deny_network:,
|
|
319
|
+
seccomp_deny_child_processes:,
|
|
292
320
|
rlimits:
|
|
293
321
|
)
|
|
294
322
|
return if Policy.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
|
|
295
|
-
return if seccomp_deny_network
|
|
323
|
+
return if seccomp_deny_network || seccomp_deny_child_processes
|
|
296
324
|
return if Array(rlimits).any?
|
|
297
325
|
|
|
298
|
-
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"
|
|
299
327
|
end
|
|
300
328
|
|
|
301
|
-
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
|
|
329
|
+
def validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
|
|
302
330
|
base = chdir ? File.expand_path(chdir) : Dir.pwd
|
|
303
|
-
abi = Native.abi_version
|
|
304
331
|
read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
|
|
305
332
|
write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
|
|
306
333
|
execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
|
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)
|
|
@@ -139,9 +141,9 @@ module Landlock
|
|
|
139
141
|
begin
|
|
140
142
|
# Arm group cleanup only after leaving the supervisor's process group.
|
|
141
143
|
::Process.setpgrp
|
|
142
|
-
if kill_process_group_on_parent_death
|
|
144
|
+
if kill_process_group_on_parent_death && RUBY_PLATFORM.include?("linux")
|
|
143
145
|
Landlock::Native.arm_parent_death_process_group!(parent_pid)
|
|
144
|
-
|
|
146
|
+
elsif RUBY_PLATFORM.include?("linux")
|
|
145
147
|
Landlock::Native.set_parent_death_signal!
|
|
146
148
|
exit! 1 if ::Process.ppid != parent_pid
|
|
147
149
|
end
|
|
@@ -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:
|
|
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
|