landlock 0.4.1 → 0.5.1

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: 06c677a70d7c9573f8900255447f3407b917a32d20b431ddf3639660ae410847
4
- data.tar.gz: 07a99cefc054bbc33693f9d72531ef2639e8b926895de8f5972cd62a40974413
3
+ metadata.gz: 212a2155cecade152248158abbbfea18f6592f187f1338a2faf442c5b727a5b4
4
+ data.tar.gz: 110062bf45e0f020d44112964f2b140c8eac867f8c502171a66c5257c19c2ba6
5
5
  SHA512:
6
- metadata.gz: 44ff2dd41500ebfb8751aca02d09582a92a9cbcc5abce973f53ce1981fe579afde5bfe63b739c032404e874654de414ca13c17581a2ec51a34fe9e20c708105f
7
- data.tar.gz: d98885b52ebe803ee29d3d5db0ee83ba44417377ddd7f9f6ce47b41576be326b5246b62ebfbd47723f79ae3d3be7c5608916816c86264e6832e7f1bfb515d793
6
+ metadata.gz: 711002205ed3686d42d411d86cda9b18c6f435d467a1732ac5cd97fdb7404549b01a2617232bf7ac480b10f10a07e212331ac57c73f74128ddc5c8bbb17961b2
7
+ data.tar.gz: 42cf382fe1cd2b29b3f23a5425522eb7a198dd19de3d59fc4ce2be33fb07483ba9ae53b199af6f8f126d960756850552f329b8e3f6dead5e0b08764e5a3d488b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.5.1] - 2026-09-09
8
+
9
+ ### Fixed
10
+
11
+ - Allow `Landlock.fork(on_unsupported: :run_without_landlock)` to fall back when the support check is blocked or unavailable, including on macOS.
12
+
13
+ ## [0.5] - 2026-09-01
14
+
15
+ ### Added
16
+
17
+ - Add `Landlock.fork` for running a sandboxed Ruby block in a supervised forked child.
18
+
7
19
  ## [0.4.1] - 2026-08-20
8
20
 
9
21
  ### Fixed
data/README.md CHANGED
@@ -135,6 +135,44 @@ Capture options:
135
135
  - `success_status_codes:` and `failure_message:` — `capture!` failure handling options.
136
136
  - `allow_all_known:` — when filesystem rules are present, handle all Landlock filesystem rights known to the running ABI so unlisted filesystem access is denied.
137
137
 
138
+ ## Forking a Ruby block
139
+
140
+ `Landlock.fork` is a supervised, synchronous fork. It forks the current Ruby process, applies the requested restrictions in the child, runs the block there, waits for it, and returns a `Landlock::CaptureResult`. It is intended for applications that need to reuse initialized Ruby state without executing a new command:
141
+
142
+ ```ruby
143
+ result = Landlock.fork(
144
+ read: [input_path],
145
+ timeout: 5,
146
+ rlimits: { cpu_seconds: 5, memory_bytes: 512 * 1024 * 1024 },
147
+ seccomp_deny_network: true
148
+ ) do |stdout, _stderr|
149
+ stdout.write(calculate_dominant_color(input_path))
150
+ end
151
+
152
+ color = result.stdout if result.success?
153
+ ```
154
+
155
+ The block receives its child-side stdout and stderr streams. Write response data to stdout and diagnostics to stderr, then inspect them through the capture result in the parent. The block's return value is discarded. An exception makes the child exit with status 1 and writes a diagnostic to stderr. `fork` accepts the capture options listed above except `success_status_codes:` and `failure_message:`, which only apply to `capture!`.
156
+
157
+ By default, `Landlock.fork` requires Linux Landlock support and raises `Landlock::UnsupportedError` before forking when the Landlock ABI is unavailable. A caller that explicitly accepts running without Landlock filesystem, TCP, and scope enforcement can opt in to the fallback:
158
+
159
+ ```ruby
160
+ result = Landlock.fork(
161
+ on_unsupported: :run_without_landlock,
162
+ timeout: 5,
163
+ rlimits: { memory_bytes: 512 * 1024 * 1024 },
164
+ seccomp_deny_network: true
165
+ ) { |stdout, _stderr| stdout.write(run_plugin) }
166
+ ```
167
+
168
+ This fallback is used 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
+
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.
171
+
172
+ 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
+
174
+ Fork only from a process whose loaded libraries and runtime state are safe to use after `fork`. `Landlock.fork` cannot make an unsafe parent fork-safe, and the block must not depend on threads that exist only in the parent.
175
+
138
176
  ## Restrict current process
139
177
 
140
178
  This is irreversible for the current thread and its future children. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.
@@ -2,8 +2,12 @@
2
2
  #include "landlock_native.h"
3
3
  #include "seccomp_deny_network.h"
4
4
 
5
+ #include <signal.h>
5
6
  #include <string.h>
6
7
 
8
+ #include <dirent.h>
9
+ #include <stdlib.h>
10
+
7
11
  static VALUE mLandlock;
8
12
  static VALUE eLandlockError;
9
13
  static VALUE eSyscallError;
@@ -121,6 +125,49 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
121
125
  return Qnil;
122
126
  }
123
127
 
128
+ #ifdef __linux__
129
+ #define LL_DESCRIPTOR_DIRECTORY "/proc/self/fd"
130
+ #else
131
+ #define LL_DESCRIPTOR_DIRECTORY "/dev/fd"
132
+ #endif
133
+
134
+ static VALUE rb_ll_close_inherited_fds(VALUE self) {
135
+ /* The forked child keeps running Ruby, so interpreter-reserved descriptors
136
+ * must survive. This rules out close_range across the entire descriptor table. */
137
+ DIR *dir = opendir(LL_DESCRIPTOR_DIRECTORY);
138
+ if (!dir) {
139
+ raise_syscall_error("opendir(" LL_DESCRIPTOR_DIRECTORY ")");
140
+ }
141
+
142
+ int dir_fd = dirfd(dir);
143
+ struct dirent *entry;
144
+ for (;;) {
145
+ errno = 0;
146
+ entry = readdir(dir);
147
+ if (!entry) {
148
+ if (errno != 0) {
149
+ int saved_errno = errno;
150
+ closedir(dir);
151
+ errno = saved_errno;
152
+ raise_syscall_error("readdir(" LL_DESCRIPTOR_DIRECTORY ")");
153
+ }
154
+ break;
155
+ }
156
+
157
+ char *end = NULL;
158
+ errno = 0;
159
+ long fd = strtol(entry->d_name, &end, 10);
160
+ if (errno == 0 && end && *end == '\0' && fd >= 3 && fd != dir_fd &&
161
+ !rb_reserved_fd_p((int)fd)) {
162
+ close((int)fd);
163
+ }
164
+ }
165
+ if (closedir(dir) != 0) {
166
+ raise_syscall_error("closedir(" LL_DESCRIPTOR_DIRECTORY ")");
167
+ }
168
+ return Qtrue;
169
+ }
170
+
124
171
  static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
125
172
  #ifdef SYS_pidfd_open
126
173
  int fd = syscall(SYS_pidfd_open, NUM2PIDT(pid_value), 0);
@@ -135,6 +182,62 @@ static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
135
182
  #endif
136
183
  }
137
184
 
185
+ /* Runs after the worker has become its own process-group leader. */
186
+ static void terminate_own_process_group(int signal_number) {
187
+ (void)signal_number;
188
+ kill(0, SIGKILL);
189
+ _exit(0);
190
+ }
191
+
192
+ static VALUE rb_ll_arm_parent_death_process_group(VALUE self, VALUE parent_pid_value) {
193
+ #ifdef __linux__
194
+ pid_t parent_pid = NUM2PIDT(parent_pid_value);
195
+ /* Leave the first two application-visible realtime signals available to callers. */
196
+ int parent_death_signal = SIGRTMIN + 2;
197
+
198
+ struct sigaction action;
199
+ memset(&action, 0, sizeof(action));
200
+ action.sa_handler = terminate_own_process_group;
201
+ sigemptyset(&action.sa_mask);
202
+ if (sigaction(parent_death_signal, &action, NULL) != 0) {
203
+ raise_syscall_error("sigaction(parent death process group)");
204
+ }
205
+
206
+ sigset_t signals;
207
+ sigemptyset(&signals);
208
+ sigaddset(&signals, parent_death_signal);
209
+ if (sigprocmask(SIG_UNBLOCK, &signals, NULL) != 0) {
210
+ raise_syscall_error("sigprocmask(parent death process group)");
211
+ }
212
+
213
+ if (prctl(PR_SET_PDEATHSIG, parent_death_signal) != 0) {
214
+ raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
215
+ }
216
+
217
+ if (getppid() != parent_pid) {
218
+ terminate_own_process_group(parent_death_signal);
219
+ }
220
+
221
+ return Qtrue;
222
+ #else
223
+ errno = ENOSYS;
224
+ raise_syscall_error("parent death process group");
225
+ return Qnil;
226
+ #endif
227
+ }
228
+
229
+ static VALUE rb_ll_set_parent_death_signal(VALUE self) {
230
+ #ifdef __linux__
231
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) {
232
+ raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
233
+ }
234
+ return Qtrue;
235
+ #else
236
+ errno = ENOSYS;
237
+ raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
238
+ #endif
239
+ }
240
+
138
241
  static VALUE rb_ll_seccomp_deny_network(VALUE self) {
139
242
  const char *error_message = "seccomp(SECCOMP_SET_MODE_FILTER)";
140
243
  if (rb_landlock_seccomp_deny_network(&error_message) != 0) {
@@ -164,7 +267,12 @@ void Init_landlock(void) {
164
267
  rb_define_singleton_method(mLandlock, "_add_net_rule", rb_ll_add_net_rule, 3);
165
268
  rb_define_singleton_method(mLandlock, "_restrict_self", rb_ll_restrict_self, 1);
166
269
  rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
270
+ rb_define_singleton_method(mLandlock, "_close_inherited_fds", rb_ll_close_inherited_fds, 0);
167
271
  rb_define_singleton_method(mLandlock, "_pidfd_open", rb_ll_pidfd_open, 1);
272
+ rb_define_singleton_method(mLandlock, "_arm_parent_death_process_group",
273
+ rb_ll_arm_parent_death_process_group, 1);
274
+ rb_define_singleton_method(mLandlock, "_set_parent_death_signal", rb_ll_set_parent_death_signal,
275
+ 0);
168
276
  rb_define_singleton_method(mLandlock, "seccomp_deny_network!", rb_ll_seccomp_deny_network, 0);
169
277
 
170
278
  rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
@@ -80,6 +80,30 @@ module Landlock
80
80
  capture_with(argv, raise_on_failure: true, **options)
81
81
  end
82
82
 
83
+ def fork(on_unsupported: :raise, **options, &block)
84
+ raise ArgumentError, "fork requires a block" if !block
85
+ if !%i[raise run_without_landlock].include?(on_unsupported)
86
+ raise ArgumentError, "on_unsupported must be :raise or :run_without_landlock"
87
+ end
88
+
89
+ enforce_landlock = Landlock.supported?
90
+ raise UnsupportedError, "Linux Landlock is unavailable" if !enforce_landlock && on_unsupported == :raise
91
+
92
+ capture_options = prepare_capture_options(**options, require_landlock: enforce_landlock)
93
+ validate_fallback_restriction!(**capture_options) if !enforce_landlock
94
+
95
+ Runner::Fork.call_block(**capture_options, enforce_landlock:, &block)
96
+ rescue OutputTooLargeError => error
97
+ result = error.result
98
+ raise CommandError.new(
99
+ error.message,
100
+ stdout: result&.stdout.to_s,
101
+ stderr: result&.stderr.to_s,
102
+ status: result&.status,
103
+ result:
104
+ )
105
+ end
106
+
83
107
  def capture_with(
84
108
  argv,
85
109
  read: nil,
@@ -105,31 +129,30 @@ module Landlock
105
129
  raise_on_failure:
106
130
  )
107
131
  argv = Validation.normalize_argv(argv).map(&:to_s)
108
- ensure_landlock_supported!
109
- max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
110
- timeout = Validation.validate_timeout!(timeout)
111
- normalized_rlimits = Rlimits.normalize(rlimits)
112
- env = Env.normalize(env)
113
- policy =
114
- prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
115
- validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits: normalized_rlimits)
116
-
117
- result =
118
- call_with_runner(
119
- argv,
120
- **policy,
132
+ options =
133
+ prepare_capture_options(
134
+ read:,
135
+ write:,
136
+ execute:,
137
+ connect_tcp:,
138
+ bind_tcp:,
139
+ paths:,
140
+ scope:,
121
141
  chdir:,
122
142
  env:,
123
143
  unsetenv_others:,
124
144
  close_others:,
145
+ allow_all_known:,
125
146
  timeout:,
126
147
  stdin:,
127
- rlimits: normalized_rlimits,
148
+ rlimits:,
128
149
  seccomp_deny_network:,
129
150
  max_output_bytes:,
130
151
  truncate_output:
131
152
  )
132
153
 
154
+ result = call_with_runner(argv, **options)
155
+
133
156
  if raise_on_failure &&
134
157
  (result.timed_out? || !result.status.exited? || !success_status_codes.include?(result.status.exitstatus))
135
158
  message = [argv.join(" "), failure_message, result.stderr].filter { |part| part.to_s != "" }.join("\n")
@@ -149,6 +172,62 @@ module Landlock
149
172
  )
150
173
  end
151
174
 
175
+ def prepare_capture_options(
176
+ read: nil,
177
+ write: nil,
178
+ execute: nil,
179
+ connect_tcp: nil,
180
+ bind_tcp: nil,
181
+ paths: nil,
182
+ scope: nil,
183
+ chdir: nil,
184
+ env: nil,
185
+ unsetenv_others: false,
186
+ close_others: true,
187
+ allow_all_known: false,
188
+ timeout: nil,
189
+ stdin: nil,
190
+ rlimits: {},
191
+ seccomp_deny_network: false,
192
+ max_output_bytes: nil,
193
+ truncate_output: false,
194
+ require_landlock: true
195
+ )
196
+ ensure_landlock_supported! if require_landlock
197
+ max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
198
+ timeout = Validation.validate_timeout!(timeout)
199
+ rlimits = Rlimits.normalize(rlimits)
200
+ env = Env.normalize(env)
201
+ policy =
202
+ prepare_policy(
203
+ read:,
204
+ write:,
205
+ execute:,
206
+ connect_tcp:,
207
+ bind_tcp:,
208
+ paths:,
209
+ scope:,
210
+ chdir:,
211
+ allow_all_known:,
212
+ abi: require_landlock ? Native.abi_version : 0
213
+ )
214
+ validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
215
+
216
+ {
217
+ **policy,
218
+ chdir:,
219
+ env:,
220
+ unsetenv_others:,
221
+ close_others:,
222
+ timeout:,
223
+ stdin:,
224
+ rlimits:,
225
+ seccomp_deny_network:,
226
+ max_output_bytes:,
227
+ truncate_output:
228
+ }
229
+ end
230
+
152
231
  def spawn_with_runner(argv, **options)
153
232
  if Runner::Native.available?
154
233
  begin
@@ -177,10 +256,21 @@ module Landlock
177
256
  raise UnsupportedError, "Linux Landlock is unavailable" unless Native.abi_version.positive?
178
257
  end
179
258
 
180
- def prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
259
+ def prepare_policy(
260
+ read:,
261
+ write:,
262
+ execute:,
263
+ connect_tcp:,
264
+ bind_tcp:,
265
+ paths:,
266
+ scope:,
267
+ chdir:,
268
+ allow_all_known:,
269
+ abi: Native.abi_version
270
+ )
181
271
  connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
182
272
  bind_tcp = bind_tcp.nil? ? nil : Validation.normalize_ports(bind_tcp, :bind_tcp)
183
- read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
273
+ read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
184
274
  { read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
185
275
  end
186
276
 
@@ -199,6 +289,16 @@ module Landlock
199
289
  raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
200
290
  end
201
291
 
292
+ def validate_fallback_restriction!(seccomp_deny_network:, rlimits:, **)
293
+ if seccomp_deny_network && !RUBY_PLATFORM.include?("linux")
294
+ raise UnsupportedError, "seccomp_deny_network requires Linux"
295
+ end
296
+
297
+ return if seccomp_deny_network || rlimits.any?
298
+
299
+ raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
300
+ end
301
+
202
302
  def validate_capture_restriction!(
203
303
  read:,
204
304
  write:,
@@ -218,9 +318,8 @@ module Landlock
218
318
  raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, or rlimits"
219
319
  end
220
320
 
221
- def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
321
+ def validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
222
322
  base = chdir ? File.expand_path(chdir) : Dir.pwd
223
- abi = Native.abi_version
224
323
  read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
225
324
  write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
226
325
  execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
@@ -31,10 +31,22 @@ module Landlock
31
31
  Landlock.__send__(:_close_fd, fd)
32
32
  end
33
33
 
34
+ def close_inherited_fds!
35
+ Landlock.__send__(:_close_inherited_fds)
36
+ end
37
+
34
38
  def pidfd_open(pid)
35
39
  Landlock.__send__(:_pidfd_open, pid)
36
40
  end
37
41
 
42
+ def arm_parent_death_process_group!(parent_pid)
43
+ Landlock.__send__(:_arm_parent_death_process_group, parent_pid)
44
+ end
45
+
46
+ def set_parent_death_signal!
47
+ Landlock.__send__(:_set_parent_death_signal)
48
+ end
49
+
38
50
  def seccomp_deny_network!
39
51
  Landlock.seccomp_deny_network!
40
52
  end
@@ -71,44 +71,97 @@ module Landlock
71
71
  seccomp_deny_network:,
72
72
  max_output_bytes:,
73
73
  truncate_output:
74
+ )
75
+ capture_pipes(timeout:, stdin:, max_output_bytes:, truncate_output:) do
76
+ setup_child!(
77
+ argv,
78
+ read:,
79
+ write:,
80
+ execute:,
81
+ connect_tcp:,
82
+ bind_tcp:,
83
+ paths:,
84
+ scope:,
85
+ chdir:,
86
+ env:,
87
+ unsetenv_others:,
88
+ close_others:,
89
+ allow_all_known:,
90
+ rlimits:,
91
+ seccomp_deny_network:
92
+ )
93
+ rescue Exception => error
94
+ Runner.exit_child!(error)
95
+ end
96
+ end
97
+
98
+ def call_block(timeout:, stdin:, max_output_bytes:, truncate_output:, enforce_landlock:, **options, &block)
99
+ capture_pipes(
100
+ timeout:,
101
+ stdin:,
102
+ max_output_bytes:,
103
+ truncate_output:,
104
+ kill_process_group_on_parent_death: true
105
+ ) do
106
+ begin
107
+ prepare_forked_block!(**options, enforce_landlock:)
108
+ rescue SystemExit, SignalException
109
+ raise
110
+ rescue Exception => error
111
+ Runner.exit_child!(error)
112
+ end
113
+
114
+ block.call(STDOUT, STDERR)
115
+ exit! 0
116
+ rescue SystemExit => error
117
+ exit! error.status
118
+ rescue SignalException
119
+ raise
120
+ rescue Exception => error
121
+ Runner.exit_forked_block!(error)
122
+ end
123
+ end
124
+
125
+ def capture_pipes(
126
+ timeout:,
127
+ stdin:,
128
+ max_output_bytes:,
129
+ truncate_output:,
130
+ kill_process_group_on_parent_death: false
74
131
  )
75
132
  stdout_reader, stdout_writer = IO.pipe
76
133
  stderr_reader, stderr_writer = IO.pipe
77
134
  stdin_reader, stdin_writer = IO.pipe
135
+ parent_pid = ::Process.pid
78
136
 
79
137
  pid =
80
138
  fork do
81
139
  begin
140
+ # Arm group cleanup only after leaving the supervisor's process group.
141
+ ::Process.setpgrp
142
+ if kill_process_group_on_parent_death && RUBY_PLATFORM.include?("linux")
143
+ Landlock::Native.arm_parent_death_process_group!(parent_pid)
144
+ elsif RUBY_PLATFORM.include?("linux")
145
+ Landlock::Native.set_parent_death_signal!
146
+ exit! 1 if ::Process.ppid != parent_pid
147
+ end
82
148
  stdout_reader.close
83
149
  stderr_reader.close
84
150
  stdin_writer.close
85
- ::Process.setpgrp
86
151
  STDIN.reopen(stdin_reader)
87
152
  STDOUT.reopen(stdout_writer)
88
153
  STDERR.reopen(stderr_writer)
154
+ STDOUT.sync = true
155
+ STDERR.sync = true
89
156
  stdin_reader.close
90
157
  stdout_writer.close
91
158
  stderr_writer.close
92
159
 
93
- setup_child!(
94
- argv,
95
- read:,
96
- write:,
97
- execute:,
98
- connect_tcp:,
99
- bind_tcp:,
100
- paths:,
101
- scope:,
102
- chdir:,
103
- env:,
104
- unsetenv_others:,
105
- close_others:,
106
- allow_all_known:,
107
- rlimits:,
108
- seccomp_deny_network:
109
- )
160
+ yield
161
+ rescue SystemExit, SignalException
162
+ raise
110
163
  rescue Exception => error
111
- Runner.exit_child!(error)
164
+ Runner.exit_child!(error, stderr: capture_error_stream(stderr_writer))
112
165
  end
113
166
  end
114
167
 
@@ -141,6 +194,12 @@ module Landlock
141
194
  end
142
195
  end
143
196
 
197
+ def capture_error_stream(stderr_writer)
198
+ stderr_writer && !stderr_writer.closed? ? stderr_writer : STDERR
199
+ rescue IOError
200
+ STDERR
201
+ end
202
+
144
203
  def setup_child!(
145
204
  argv,
146
205
  read:,
@@ -166,6 +225,39 @@ module Landlock
166
225
  Rlimits.apply!(rlimits)
167
226
  Kernel.exec(*Runner.kernel_exec_args(argv, env, unsetenv_others:, close_others:))
168
227
  end
228
+
229
+ def prepare_forked_block!(
230
+ chdir:,
231
+ env:,
232
+ unsetenv_others:,
233
+ close_others:,
234
+ rlimits:,
235
+ seccomp_deny_network:,
236
+ enforce_landlock:,
237
+ **policy
238
+ )
239
+ close_inherited_ios if close_others
240
+ Dir.public_send(:chdir, chdir) if chdir
241
+ ENV.clear if unsetenv_others
242
+ env&.each { |key, value| value.nil? ? ENV.delete(key) : ENV[key] = value }
243
+ Landlock.restrict!(**policy) if enforce_landlock && Policy.requested?(**policy)
244
+ Landlock::Native.seccomp_deny_network! if seccomp_deny_network
245
+ Rlimits.apply!(rlimits)
246
+ end
247
+
248
+ def close_inherited_ios
249
+ ObjectSpace
250
+ .each_object(IO)
251
+ .to_a
252
+ .each do |io|
253
+ next if io.closed? || io.fileno <= 2
254
+
255
+ io.close
256
+ rescue IOError
257
+ end
258
+
259
+ Landlock::Native.close_inherited_fds!
260
+ end
169
261
  end
170
262
  end
171
263
  end
@@ -16,11 +16,19 @@ module Landlock
16
16
  env ? [env, *argv_for_exec(argv), exec_options] : [*argv_for_exec(argv), exec_options]
17
17
  end
18
18
 
19
- def exit_child!(error)
20
- warn "Landlock child failed before exec: #{error.class}: #{error.message}"
19
+ def exit_child!(error, stderr: STDERR)
20
+ stderr.puts "Landlock child setup failed: #{error.class}: #{error.message}"
21
+ stderr.flush
21
22
  ensure
22
23
  exit! 127
23
24
  end
25
+
26
+ def exit_forked_block!(error, stderr: STDERR)
27
+ stderr.puts "Landlock forked block failed: #{error.class}: #{error.message}"
28
+ stderr.flush
29
+ ensure
30
+ exit! 1
31
+ end
24
32
  end
25
33
  end
26
34
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Landlock
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/landlock.rb CHANGED
@@ -39,5 +39,9 @@ module Landlock
39
39
  def capture!(...)
40
40
  Execution.capture!(...)
41
41
  end
42
+
43
+ def fork(...)
44
+ Execution.fork(...)
45
+ end
42
46
  end
43
47
  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.4.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron