landlock 0.5 → 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: 5bfc5c69234e004834818ba52aab192c9219ff0a398d40ada62c4f45be461cdb
4
- data.tar.gz: 6ec4b746a959a8f68c5ba11e1c4b7342d0a4facda031a8627f05ae6fedc6c6cf
3
+ metadata.gz: 212a2155cecade152248158abbbfea18f6592f187f1338a2faf442c5b727a5b4
4
+ data.tar.gz: 110062bf45e0f020d44112964f2b140c8eac867f8c502171a66c5257c19c2ba6
5
5
  SHA512:
6
- metadata.gz: efd09558eb3b207ca4be70d4a6509b39338f9cc6374f6422a0fbeee87f2fb8fbb99dd4095044a27f94355062da6016672719a64ab3af78d8d89f622b3b0bb64b
7
- data.tar.gz: f7e6d2ebf580fd200fa05217d3266f4a48e93d226757e4a3d9c409d22a844c3f6245fb81956201a4b03c3b8ced3ddd29c169653367efa007a8cd1c71bc833048
6
+ metadata.gz: 711002205ed3686d42d411d86cda9b18c6f435d467a1732ac5cd97fdb7404549b01a2617232bf7ac480b10f10a07e212331ac57c73f74128ddc5c8bbb17961b2
7
+ data.tar.gz: 42cf382fe1cd2b29b3f23a5425522eb7a198dd19de3d59fc4ce2be33fb07483ba9ae53b199af6f8f126d960756850552f329b8e3f6dead5e0b08764e5a3d488b
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.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
+
7
13
  ## [0.5] - 2026-09-01
8
14
 
9
15
  ### Added
data/README.md CHANGED
@@ -154,7 +154,7 @@ color = result.stdout if result.success?
154
154
 
155
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
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:
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
158
 
159
159
  ```ruby
160
160
  result = Landlock.fork(
@@ -165,7 +165,9 @@ result = Landlock.fork(
165
165
  ) { |stdout, _stderr| stdout.write(run_plugin) }
166
166
  ```
167
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.
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.
169
171
 
170
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.
171
173
 
@@ -5,10 +5,8 @@
5
5
  #include <signal.h>
6
6
  #include <string.h>
7
7
 
8
- #ifdef __linux__
9
8
  #include <dirent.h>
10
9
  #include <stdlib.h>
11
- #endif
12
10
 
13
11
  static VALUE mLandlock;
14
12
  static VALUE eLandlockError;
@@ -127,13 +125,18 @@ static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
127
125
  return Qnil;
128
126
  }
129
127
 
128
+ #ifdef __linux__
129
+ #define LL_DESCRIPTOR_DIRECTORY "/proc/self/fd"
130
+ #else
131
+ #define LL_DESCRIPTOR_DIRECTORY "/dev/fd"
132
+ #endif
133
+
130
134
  static VALUE rb_ll_close_inherited_fds(VALUE self) {
131
135
  /* The forked child keeps running Ruby, so interpreter-reserved descriptors
132
136
  * must survive. This rules out close_range across the entire descriptor table. */
133
- #ifdef __linux__
134
- DIR *dir = opendir("/proc/self/fd");
137
+ DIR *dir = opendir(LL_DESCRIPTOR_DIRECTORY);
135
138
  if (!dir) {
136
- raise_syscall_error("opendir(/proc/self/fd)");
139
+ raise_syscall_error("opendir(" LL_DESCRIPTOR_DIRECTORY ")");
137
140
  }
138
141
 
139
142
  int dir_fd = dirfd(dir);
@@ -146,7 +149,7 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
146
149
  int saved_errno = errno;
147
150
  closedir(dir);
148
151
  errno = saved_errno;
149
- raise_syscall_error("readdir(/proc/self/fd)");
152
+ raise_syscall_error("readdir(" LL_DESCRIPTOR_DIRECTORY ")");
150
153
  }
151
154
  break;
152
155
  }
@@ -160,14 +163,9 @@ static VALUE rb_ll_close_inherited_fds(VALUE self) {
160
163
  }
161
164
  }
162
165
  if (closedir(dir) != 0) {
163
- raise_syscall_error("closedir(/proc/self/fd)");
166
+ raise_syscall_error("closedir(" LL_DESCRIPTOR_DIRECTORY ")");
164
167
  }
165
168
  return Qtrue;
166
- #else
167
- errno = ENOSYS;
168
- raise_syscall_error("opendir(/proc/self/fd)");
169
- return Qnil;
170
- #endif
171
169
  }
172
170
 
173
171
  static VALUE rb_ll_pidfd_open(VALUE self, VALUE pid_value) {
@@ -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(
@@ -205,7 +199,18 @@ module Landlock
205
199
  rlimits = Rlimits.normalize(rlimits)
206
200
  env = Env.normalize(env)
207
201
  policy =
208
- prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
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
+ )
209
214
  validate_capture_restriction!(**policy, seccomp_deny_network:, rlimits:)
210
215
 
211
216
  {
@@ -251,10 +256,21 @@ module Landlock
251
256
  raise UnsupportedError, "Linux Landlock is unavailable" unless Native.abi_version.positive?
252
257
  end
253
258
 
254
- 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
+ )
255
271
  connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
256
272
  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:)
273
+ read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
258
274
  { read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
259
275
  end
260
276
 
@@ -274,6 +290,10 @@ module Landlock
274
290
  end
275
291
 
276
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
+
277
297
  return if seccomp_deny_network || rlimits.any?
278
298
 
279
299
  raise ArgumentError, "Landlock fallback requires seccomp_deny_network or rlimits"
@@ -298,9 +318,8 @@ module Landlock
298
318
  raise ArgumentError, "empty capture policy: provide Landlock rules, seccomp_deny_network, or rlimits"
299
319
  end
300
320
 
301
- def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
321
+ def validate_policy_paths!(read:, write:, execute:, paths:, chdir:, abi:)
302
322
  base = chdir ? File.expand_path(chdir) : Dir.pwd
303
- abi = Native.abi_version
304
323
  read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
305
324
  write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
306
325
  execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
@@ -139,9 +139,9 @@ module Landlock
139
139
  begin
140
140
  # Arm group cleanup only after leaving the supervisor's process group.
141
141
  ::Process.setpgrp
142
- if kill_process_group_on_parent_death
142
+ if kill_process_group_on_parent_death && RUBY_PLATFORM.include?("linux")
143
143
  Landlock::Native.arm_parent_death_process_group!(parent_pid)
144
- else
144
+ elsif RUBY_PLATFORM.include?("linux")
145
145
  Landlock::Native.set_parent_death_signal!
146
146
  exit! 1 if ::Process.ppid != parent_pid
147
147
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Landlock
4
- VERSION = "0.5"
4
+ VERSION = "0.5.1"
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.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron