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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bfc5c69234e004834818ba52aab192c9219ff0a398d40ada62c4f45be461cdb
4
- data.tar.gz: 6ec4b746a959a8f68c5ba11e1c4b7342d0a4facda031a8627f05ae6fedc6c6cf
3
+ metadata.gz: a98542d0874b26516a80943ad75765373f5d244278753a2da572c6455f5f104f
4
+ data.tar.gz: c3c872446787663432a661431584718c91e4407e051854ed46cdfe90888f83b4
5
5
  SHA512:
6
- metadata.gz: efd09558eb3b207ca4be70d4a6509b39338f9cc6374f6422a0fbeee87f2fb8fbb99dd4095044a27f94355062da6016672719a64ab3af78d8d89f622b3b0bb64b
7
- data.tar.gz: f7e6d2ebf580fd200fa05217d3266f4a48e93d226757e4a3d9c409d22a844c3f6245fb81956201a4b03c3b8ced3ddd29c169653367efa007a8cd1c71bc833048
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 Linux caller that explicitly accepts running without Landlock filesystem, TCP, and scope enforcement can opt in to the fallback:
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 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.
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);
@@ -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}
@@ -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
- #ifdef __linux__
134
- DIR *dir = opendir("/proc/self/fd");
139
+ DIR *dir = opendir(LL_DESCRIPTOR_DIRECTORY);
135
140
  if (!dir) {
136
- raise_syscall_error("opendir(/proc/self/fd)");
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(/proc/self/fd)");
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(/proc/self/fd)");
168
+ raise_syscall_error("closedir(" LL_DESCRIPTOR_DIRECTORY ")");
164
169
  }
165
170
  return Qtrue;
166
- #else
167
- errno = ENOSYS;
168
- raise_syscall_error("opendir(/proc/self/fd)");
169
- return Qnil;
170
- #endif
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
@@ -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 = Native.abi_version.positive?
90
- if !enforce_landlock && (on_unsupported == :raise || !RUBY_PLATFORM.include?("linux"))
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(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
209
- validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
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(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
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
- return if seccomp_deny_network || rlimits.any?
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:)
@@ -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
@@ -116,11 +116,7 @@ module Landlock
116
116
  end
117
117
  end
118
118
 
119
- if deadline
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
- result = ::Process.wait2(pid, ::Process::WNOHANG)
173
- if result
174
- status = result.last
175
- if monotonic_time >= deadline
176
- terminate_process_group(pid)
177
- return status, true
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
 
@@ -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
- else
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))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Landlock
4
- VERSION = "0.5"
4
+ VERSION = "0.6.0"
5
5
  end
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.5'
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