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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 212a2155cecade152248158abbbfea18f6592f187f1338a2faf442c5b727a5b4
4
- data.tar.gz: 110062bf45e0f020d44112964f2b140c8eac867f8c502171a66c5257c19c2ba6
3
+ metadata.gz: a98542d0874b26516a80943ad75765373f5d244278753a2da572c6455f5f104f
4
+ data.tar.gz: c3c872446787663432a661431584718c91e4407e051854ed46cdfe90888f83b4
5
5
  SHA512:
6
- metadata.gz: 711002205ed3686d42d411d86cda9b18c6f435d467a1732ac5cd97fdb7404549b01a2617232bf7ac480b10f10a07e212331ac57c73f74128ddc5c8bbb17961b2
7
- data.tar.gz: 42cf382fe1cd2b29b3f23a5425522eb7a198dd19de3d59fc4ce2be33fb07483ba9ae53b199af6f8f126d960756850552f329b8e3f6dead5e0b08764e5a3d488b
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);
@@ -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,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
@@ -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:)
@@ -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)
@@ -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.1"
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.1
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