landlock 0.4.1 → 0.5

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: 5bfc5c69234e004834818ba52aab192c9219ff0a398d40ada62c4f45be461cdb
4
+ data.tar.gz: 6ec4b746a959a8f68c5ba11e1c4b7342d0a4facda031a8627f05ae6fedc6c6cf
5
5
  SHA512:
6
- metadata.gz: 44ff2dd41500ebfb8751aca02d09582a92a9cbcc5abce973f53ce1981fe579afde5bfe63b739c032404e874654de414ca13c17581a2ec51a34fe9e20c708105f
7
- data.tar.gz: d98885b52ebe803ee29d3d5db0ee83ba44417377ddd7f9f6ce47b41576be326b5246b62ebfbd47723f79ae3d3be7c5608916816c86264e6832e7f1bfb515d793
6
+ metadata.gz: efd09558eb3b207ca4be70d4a6509b39338f9cc6374f6422a0fbeee87f2fb8fbb99dd4095044a27f94355062da6016672719a64ab3af78d8d89f622b3b0bb64b
7
+ data.tar.gz: f7e6d2ebf580fd200fa05217d3266f4a48e93d226757e4a3d9c409d22a844c3f6245fb81956201a4b03c3b8ced3ddd29c169653367efa007a8cd1c71bc833048
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.5] - 2026-09-01
8
+
9
+ ### Added
10
+
11
+ - Add `Landlock.fork` for running a sandboxed Ruby block in a supervised forked child.
12
+
7
13
  ## [0.4.1] - 2026-08-20
8
14
 
9
15
  ### Fixed
data/README.md CHANGED
@@ -135,6 +135,42 @@ 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 Linux 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 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.
169
+
170
+ 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
+
172
+ 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.
173
+
138
174
  ## Restrict current process
139
175
 
140
176
  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,14 @@
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
+ #ifdef __linux__
9
+ #include <dirent.h>
10
+ #include <stdlib.h>
11
+ #endif
12
+
7
13
  static VALUE mLandlock;
8
14
  static VALUE eLandlockError;
9
15
  static VALUE eSyscallError;
@@ -121,6 +127,49 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
121
127
  return Qnil;
122
128
  }
123
129
 
130
+ static VALUE rb_ll_close_inherited_fds(VALUE self) {
131
+ /* The forked child keeps running Ruby, so interpreter-reserved descriptors
132
+ * must survive. This rules out close_range across the entire descriptor table. */
133
+ #ifdef __linux__
134
+ DIR *dir = opendir("/proc/self/fd");
135
+ if (!dir) {
136
+ raise_syscall_error("opendir(/proc/self/fd)");
137
+ }
138
+
139
+ int dir_fd = dirfd(dir);
140
+ struct dirent *entry;
141
+ for (;;) {
142
+ errno = 0;
143
+ entry = readdir(dir);
144
+ if (!entry) {
145
+ if (errno != 0) {
146
+ int saved_errno = errno;
147
+ closedir(dir);
148
+ errno = saved_errno;
149
+ raise_syscall_error("readdir(/proc/self/fd)");
150
+ }
151
+ break;
152
+ }
153
+
154
+ char *end = NULL;
155
+ errno = 0;
156
+ long fd = strtol(entry->d_name, &end, 10);
157
+ if (errno == 0 && end && *end == '\0' && fd >= 3 && fd != dir_fd &&
158
+ !rb_reserved_fd_p((int)fd)) {
159
+ close((int)fd);
160
+ }
161
+ }
162
+ if (closedir(dir) != 0) {
163
+ raise_syscall_error("closedir(/proc/self/fd)");
164
+ }
165
+ return Qtrue;
166
+ #else
167
+ errno = ENOSYS;
168
+ raise_syscall_error("opendir(/proc/self/fd)");
169
+ return Qnil;
170
+ #endif
171
+ }
172
+
124
173
  static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
125
174
  #ifdef SYS_pidfd_open
126
175
  int fd = syscall(SYS_pidfd_open, NUM2PIDT(pid_value), 0);
@@ -135,6 +184,62 @@ static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
135
184
  #endif
136
185
  }
137
186
 
187
+ /* Runs after the worker has become its own process-group leader. */
188
+ static void terminate_own_process_group(int signal_number) {
189
+ (void)signal_number;
190
+ kill(0, SIGKILL);
191
+ _exit(0);
192
+ }
193
+
194
+ static VALUE rb_ll_arm_parent_death_process_group(VALUE self, VALUE parent_pid_value) {
195
+ #ifdef __linux__
196
+ pid_t parent_pid = NUM2PIDT(parent_pid_value);
197
+ /* Leave the first two application-visible realtime signals available to callers. */
198
+ int parent_death_signal = SIGRTMIN + 2;
199
+
200
+ struct sigaction action;
201
+ memset(&action, 0, sizeof(action));
202
+ action.sa_handler = terminate_own_process_group;
203
+ sigemptyset(&action.sa_mask);
204
+ if (sigaction(parent_death_signal, &action, NULL) != 0) {
205
+ raise_syscall_error("sigaction(parent death process group)");
206
+ }
207
+
208
+ sigset_t signals;
209
+ sigemptyset(&signals);
210
+ sigaddset(&signals, parent_death_signal);
211
+ if (sigprocmask(SIG_UNBLOCK, &signals, NULL) != 0) {
212
+ raise_syscall_error("sigprocmask(parent death process group)");
213
+ }
214
+
215
+ if (prctl(PR_SET_PDEATHSIG, parent_death_signal) != 0) {
216
+ raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
217
+ }
218
+
219
+ if (getppid() != parent_pid) {
220
+ terminate_own_process_group(parent_death_signal);
221
+ }
222
+
223
+ return Qtrue;
224
+ #else
225
+ errno = ENOSYS;
226
+ raise_syscall_error("parent death process group");
227
+ return Qnil;
228
+ #endif
229
+ }
230
+
231
+ static VALUE rb_ll_set_parent_death_signal(VALUE self) {
232
+ #ifdef __linux__
233
+ if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) {
234
+ raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
235
+ }
236
+ return Qtrue;
237
+ #else
238
+ errno = ENOSYS;
239
+ raise_syscall_error("prctl(PR_SET_PDEATHSIG)");
240
+ #endif
241
+ }
242
+
138
243
  static VALUE rb_ll_seccomp_deny_network(VALUE self) {
139
244
  const char *error_message = "seccomp(SECCOMP_SET_MODE_FILTER)";
140
245
  if (rb_landlock_seccomp_deny_network(&error_message) != 0) {
@@ -164,7 +269,12 @@ void Init_landlock(void) {
164
269
  rb_define_singleton_method(mLandlock, "_add_net_rule", rb_ll_add_net_rule, 3);
165
270
  rb_define_singleton_method(mLandlock, "_restrict_self", rb_ll_restrict_self, 1);
166
271
  rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
272
+ rb_define_singleton_method(mLandlock, "_close_inherited_fds", rb_ll_close_inherited_fds, 0);
167
273
  rb_define_singleton_method(mLandlock, "_pidfd_open", rb_ll_pidfd_open, 1);
274
+ rb_define_singleton_method(mLandlock, "_arm_parent_death_process_group",
275
+ rb_ll_arm_parent_death_process_group, 1);
276
+ rb_define_singleton_method(mLandlock, "_set_parent_death_signal", rb_ll_set_parent_death_signal,
277
+ 0);
168
278
  rb_define_singleton_method(mLandlock, "seccomp_deny_network!", rb_ll_seccomp_deny_network, 0);
169
279
 
170
280
  rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
@@ -80,6 +80,36 @@ 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 = Native.abi_version.positive?
90
+ if !enforce_landlock && (on_unsupported == :raise || !RUBY_PLATFORM.include?("linux"))
91
+ raise UnsupportedError, "Linux Landlock is unavailable"
92
+ end
93
+
94
+ capture_options = prepare_capture_options(**options, require_landlock: enforce_landlock)
95
+ validate_fallback_restriction!(**capture_options) if !enforce_landlock
96
+
97
+ Runner::Fork.call_block(
98
+ **capture_options,
99
+ enforce_landlock:,
100
+ &block
101
+ )
102
+ rescue OutputTooLargeError => error
103
+ result = error.result
104
+ raise CommandError.new(
105
+ error.message,
106
+ stdout: result&.stdout.to_s,
107
+ stderr: result&.stderr.to_s,
108
+ status: result&.status,
109
+ result:
110
+ )
111
+ end
112
+
83
113
  def capture_with(
84
114
  argv,
85
115
  read: nil,
@@ -105,31 +135,30 @@ module Landlock
105
135
  raise_on_failure:
106
136
  )
107
137
  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,
138
+ options =
139
+ prepare_capture_options(
140
+ read:,
141
+ write:,
142
+ execute:,
143
+ connect_tcp:,
144
+ bind_tcp:,
145
+ paths:,
146
+ scope:,
121
147
  chdir:,
122
148
  env:,
123
149
  unsetenv_others:,
124
150
  close_others:,
151
+ allow_all_known:,
125
152
  timeout:,
126
153
  stdin:,
127
- rlimits: normalized_rlimits,
154
+ rlimits:,
128
155
  seccomp_deny_network:,
129
156
  max_output_bytes:,
130
157
  truncate_output:
131
158
  )
132
159
 
160
+ result = call_with_runner(argv, **options)
161
+
133
162
  if raise_on_failure &&
134
163
  (result.timed_out? || !result.status.exited? || !success_status_codes.include?(result.status.exitstatus))
135
164
  message = [argv.join(" "), failure_message, result.stderr].filter { |part| part.to_s != "" }.join("\n")
@@ -149,6 +178,51 @@ module Landlock
149
178
  )
150
179
  end
151
180
 
181
+ def prepare_capture_options(
182
+ read: nil,
183
+ write: nil,
184
+ execute: nil,
185
+ connect_tcp: nil,
186
+ bind_tcp: nil,
187
+ paths: nil,
188
+ scope: nil,
189
+ chdir: nil,
190
+ env: nil,
191
+ unsetenv_others: false,
192
+ close_others: true,
193
+ allow_all_known: false,
194
+ timeout: nil,
195
+ stdin: nil,
196
+ rlimits: {},
197
+ seccomp_deny_network: false,
198
+ max_output_bytes: nil,
199
+ truncate_output: false,
200
+ require_landlock: true
201
+ )
202
+ ensure_landlock_supported! if require_landlock
203
+ max_output_bytes = Validation.validate_output_limit!(max_output_bytes)
204
+ timeout = Validation.validate_timeout!(timeout)
205
+ rlimits = Rlimits.normalize(rlimits)
206
+ env = Env.normalize(env)
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:)
210
+
211
+ {
212
+ **policy,
213
+ chdir:,
214
+ env:,
215
+ unsetenv_others:,
216
+ close_others:,
217
+ timeout:,
218
+ stdin:,
219
+ rlimits:,
220
+ seccomp_deny_network:,
221
+ max_output_bytes:,
222
+ truncate_output:
223
+ }
224
+ end
225
+
152
226
  def spawn_with_runner(argv, **options)
153
227
  if Runner::Native.available?
154
228
  begin
@@ -199,6 +273,12 @@ module Landlock
199
273
  raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
200
274
  end
201
275
 
276
+ def validate_fallback_restriction!(seccomp_deny_network:, rlimits:, **)
277
+ return if seccomp_deny_network || rlimits.any?
278
+
279
+ raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
280
+ end
281
+
202
282
  def validate_capture_restriction!(
203
283
  read:,
204
284
  write:,
@@ -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
143
+ Landlock::Native.arm_parent_death_process_group!(parent_pid)
144
+ else
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"
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'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron