landlock 0.1.0 → 0.2

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: 6cf65089e4af85184c67c7a2e6c679932dd21c492882ccf9cf4612d5603cb7a4
4
- data.tar.gz: a987aaefb3e9ec52f69087c533fed5e384157aa4e1caebbc56226030c84e1112
3
+ metadata.gz: baea0cbd5b22406f288880dd5bcec85569c3134b256657d876a502f37e622364
4
+ data.tar.gz: a383e9cb807f51e2fd6e5d15a5d3d22c61d20a9ee2f9dffb046a1a97e24c2a6e
5
5
  SHA512:
6
- metadata.gz: 6de7d6d837c0365fcf6d18e4cf3d41d291d6d043b71a3b05d1501e3c805faf7acd082865e67bad699814f0bdd342859f7790e80c3d58e510b83f8121e3445955
7
- data.tar.gz: f87863bd3de8f8a8d5ce6ffd56382bafcf6cefc7ec74dca9ea9a7e6df7c5f759c556f51a2e687668d0fd30b809cf07a8136b73e26467dd069d021977a5abd4e2
6
+ metadata.gz: c385390bd6b239d5a7b495d74388ba9bb357787900ef73bfbd953d8353c49ce893117df8cc80fe95b0c9b2e8a6c836988a8540b2fbfdbce244ff76a6879d7067
7
+ data.tar.gz: b1d754bbfd7b60ec81cc4d036bc13ab627253fdf2fdb55a752477a43f917381d819f8fa20870abcad264e2a86d0c5635cb327799ccc614800e3adb1088b9dfec
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## Unreleased
6
+
7
+ ### [0.2] - 2026-04-30
8
+
9
+ - Add `Landlock::SafeExec.capture`, backed by a compiled `landlock-safe-exec` helper, for subprocess capture with Landlock, optional seccomp network denial, resource limits, exact environment handling, stdin, timeout handling, process-group cleanup, result metadata, and output limits.
10
+ - Share native Landlock syscall/constant definitions between the Ruby extension and helper binary.
11
+ - Add non-Linux/pass-through SafeExec behavior so integration code can run on platforms without the Linux sandbox backend while warning that sandbox options are ignored.
12
+
13
+ ## [0.1.1] - 2026-04-30
14
+
15
+ ### Security
16
+
17
+ - Require `Landlock.exec` and `Landlock.spawn` commands to be passed as argument arrays. This avoids Ruby's implicit shell execution path for string commands.
18
+ - Execute subprocesses with an explicit `argv[0]` tuple (`[command, command]`) so array commands keep their no-shell behavior.
19
+ - Use `exit! 127` for child setup failures before `exec`, preventing inherited `at_exit` handlers from running in the forked child.
20
+ - Honor `unsetenv_others: true` by passing Ruby's `unsetenv_others` exec option instead of only constructing a reduced environment hash.
21
+ - Add ABI v6 Landlock scoping support via `scope: [:signal, :abstract_unix_socket]` to restrict signalling and abstract Unix-domain socket access outside the sandbox domain.
22
+ - Expose `allow_all_known:` on `Landlock.exec` and `Landlock.spawn` so subprocess sandboxes can deny unlisted filesystem actions without needing dummy allow rules.
23
+
24
+ ### Fixed
25
+
26
+ - Allow high-level `read`, `write`, and `execute` helpers to target individual files by filtering directory-only rights before adding file path rules.
27
+ - Fix fallback Landlock syscall numbers on i386, handle x32, and prefer platform `__NR_*` constants when available.
28
+ - Convert path rule arguments before opening path file descriptors in the native extension to avoid leaking descriptors on argument conversion errors.
29
+
30
+ ### Documentation
31
+
32
+ - Document important sandbox caveats: only handled rights are restricted, TCP rules do not cover UDP/pathname Unix sockets, already-open descriptors remain usable, and `restrict!` applies to the calling thread and future children.
33
+
34
+ ### Tests
35
+
36
+ - Add coverage for no-shell argv validation, child setup failure behavior, `unsetenv_others`, strict filesystem subprocess policies, file-specific path rules, and ABI v6 signal scoping.
37
+
38
+ ## [0.1.0] - 2026-04-30
39
+
40
+ ### Added
41
+
42
+ - Initial Ruby bindings for Linux Landlock rulesets, filesystem path rules, TCP port rules, and safe subprocess helpers.
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # landlock
2
2
 
3
- Ruby bindings for Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html): unprivileged, kernel-enforced sandboxing for the current process and its descendants.
3
+ Ruby bindings for Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html): unprivileged, kernel-enforced sandboxing for the calling thread and its future descendants.
4
4
 
5
5
  This gem includes a small native extension around the three Landlock syscalls and a Ruby API for safe subprocess execution.
6
6
 
7
7
  ## Status
8
8
 
9
- Experimental. Filesystem support requires Landlock ABI v1+. TCP network rules require ABI v4+.
9
+ Experimental. Filesystem support requires Landlock ABI v1+. TCP network rules require ABI v4+. Signal and abstract Unix-domain socket scopes require ABI v6+.
10
10
 
11
11
  ```ruby
12
12
  require "landlock"
@@ -15,8 +15,12 @@ puts Landlock.abi_version
15
15
  puts Landlock.supported?
16
16
  ```
17
17
 
18
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
19
+
18
20
  ## Safe subprocess execution
19
21
 
22
+ Pass commands as an argument array. `Landlock.exec` and `Landlock.spawn` do not invoke a shell implicitly; use an explicit shell in the array if that is really required. Both helpers accept `env:` and `unsetenv_others:` and pass them through to `Kernel.exec` so subprocesses can run with a controlled environment.
23
+
20
24
  Allow Ruby to execute and read its runtime, but only allow outbound TCP connections to port 443:
21
25
 
22
26
  ```ruby
@@ -24,7 +28,8 @@ status = Landlock.exec(
24
28
  [RbConfig.ruby, "script.rb"],
25
29
  read: ["/usr", "/lib", "/lib64", "/etc/ssl"],
26
30
  execute: ["/usr", "/lib", "/lib64"],
27
- connect_tcp: [443]
31
+ connect_tcp: [443],
32
+ allow_all_known: true
28
33
  )
29
34
 
30
35
  abort "failed" unless status.success?
@@ -35,12 +40,20 @@ Deny all outbound TCP except the listed ports:
35
40
  ```ruby
36
41
  Landlock.exec(
37
42
  ["curl", "https://example.com"],
38
- read: ["/usr", "/lib", "/lib64", "/etc/ssl", "/etc/resolv.conf", "/etc/hosts"],
43
+ read: [
44
+ "/usr", "/lib", "/lib64",
45
+ "/etc/ssl", "/etc/resolv.conf", "/etc/hosts",
46
+ "/etc/nsswitch.conf", "/etc/gai.conf", "/etc/host.conf",
47
+ "/run/systemd/resolve", "/var/lib/sss"
48
+ ].select { |path| File.exist?(path) },
39
49
  execute: ["/usr", "/lib", "/lib64"],
40
- connect_tcp: [443]
50
+ connect_tcp: [443],
51
+ allow_all_known: true
41
52
  )
42
53
  ```
43
54
 
55
+ TLS and name-resolution dependencies vary by distribution and NSS configuration; add any local CA, DNS, NSS, or resolver paths your system needs.
56
+
44
57
  Allow binding a local TCP port:
45
58
 
46
59
  ```ruby
@@ -48,22 +61,99 @@ Landlock.exec(
48
61
  [RbConfig.ruby, "server.rb"],
49
62
  read: ["/usr", "/lib", "/lib64", Dir.pwd],
50
63
  execute: ["/usr", "/lib", "/lib64"],
51
- bind_tcp: [9292]
64
+ bind_tcp: [9292],
65
+ allow_all_known: true
66
+ )
67
+ ```
68
+
69
+ ## SafeExec helper
70
+
71
+ `Landlock::SafeExec.capture` runs a command through the compiled `landlock-safe-exec` helper. The helper applies Landlock rules, resource limits, and an optional seccomp network-deny filter in the execing process before replacing itself with the target command. This keeps the privileged setup out of Ruby/FFI and avoids running Ruby code in a post-fork child. Use `capture!` when unsuccessful exit statuses should raise.
72
+
73
+ For example, inspect an uploaded video with `ffprobe` while only allowing reads from the upload and system runtime paths, denying network access, and bounding CPU/output:
74
+
75
+ ```ruby
76
+ result = Landlock::SafeExec.capture(
77
+ "ffprobe",
78
+ "-v", "error",
79
+ "-show_format",
80
+ "-show_streams",
81
+ "-of", "json",
82
+ upload_path,
83
+ read: [upload_path, *Landlock::SafeExec.default_read_paths],
84
+ execute: Landlock::SafeExec.default_execute_paths,
85
+ env: { "PATH" => ENV.fetch("PATH", "") },
86
+ rlimits: {
87
+ cpu_seconds: 5,
88
+ memory_bytes: 512 * 1024 * 1024,
89
+ file_size_bytes: 0,
90
+ open_files: 64,
91
+ processes: 0
92
+ },
93
+ seccomp_deny_network: true,
94
+ max_output_bytes: 256 * 1024,
95
+ truncate_output: false
96
+ )
97
+
98
+ metadata = JSON.parse(result.stdout) if result.success?
99
+ ```
100
+
101
+ Pass `stdin:` when a tool should read from standard input instead of a file:
102
+
103
+ ```ruby
104
+ stdout, stderr, status = Landlock::SafeExec.capture(
105
+ "tr", "a-z", "A-Z",
106
+ stdin: "hello",
107
+ env: { "PATH" => ENV.fetch("PATH", "") }
52
108
  )
53
109
  ```
54
110
 
111
+ `capture` returns a `Landlock::SafeExec::Result` with `stdout`, `stderr`, `status`, `success?`, `timed_out?`, and `output_truncated?`, including for unsuccessful exit statuses. It also supports array destructuring:
112
+
113
+ ```ruby
114
+ stdout, stderr, status = Landlock::SafeExec.capture("tool", "arg")
115
+ ```
116
+
117
+ `capture!` has the same return shape for successful commands, but raises `Landlock::SafeExec::CommandError` for unsuccessful statuses. The error also exposes `stdout`, `stderr`, `status`, and `result`.
118
+
119
+ SafeExec options:
120
+
121
+ - `read:`, `write:`, `execute:` — filesystem allowlists. Explicit paths must exist; missing paths raise `ArgumentError` instead of being silently ignored.
122
+ - `connect_tcp:` — allowed outbound TCP ports. If omitted on Landlock ABI v4+, SafeExec denies outbound TCP by installing a dummy allow rule for port `0`. Pass `connect_tcp: []` to leave outbound TCP unrestricted.
123
+ - `bind_tcp:` — allowed TCP bind ports. Binding is unrestricted unless this is provided.
124
+ - `seccomp_deny_network:` — additionally deny common Linux network syscalls with seccomp. This is Linux-specific and intended as defense in depth.
125
+ - `rlimits:` — resource limits. Supported keys are `:cpu_seconds`, `:memory_bytes`, `:file_size_bytes`, `:open_files`, and `:processes`. Values must be non-negative integers.
126
+ - `timeout:` — wall-clock timeout in seconds. On timeout SafeExec terminates the process group and returns/raises with `result.timed_out?` true.
127
+ - `max_output_bytes:` — combined stdout+stderr byte limit. With `truncate_output: false`, exceeding the limit raises. With `truncate_output: true`, output is truncated and `result.output_truncated?` is true.
128
+ - `stdin:` — string or IO-like object to write to the child process stdin.
129
+ - `chdir:` — working directory for the child.
130
+ - `env:` — exact child environment by default.
131
+ - `inherit_env:` — when true, inherit the parent environment and apply `env:` as overrides.
132
+ - `success_status_codes:` — status codes considered successful by `capture!`; defaults to `[0]`.
133
+ - `allow_all_known:` — when filesystem rules are present, handle all Landlock filesystem rights known to the running ABI so unlisted filesystem access is denied. Defaults to `true`.
134
+
135
+ SafeExec uses an exact environment by default: `env:` is the full environment passed to the child, not additions to the parent environment. Use `inherit_env: true` when a command really needs the parent environment plus the supplied `env:` overrides.
136
+
137
+ Use `Landlock::SafeExec.supported?` (or `sandboxing?`) to check whether the Linux helper and Landlock are available. When this is false, SafeExec still runs commands in pass-through mode but does not enforce Landlock/seccomp sandbox options.
138
+
139
+ On non-Linux platforms, or when the compiled helper is unavailable, SafeExec runs as a pass-through compatibility wrapper. Process-management features such as capture, timeout, environment handling, `chdir:`, output limits, `stdin:`, and supported `rlimits:` still apply, but Landlock and seccomp options (`read:`, `write:`, `execute:`, `connect_tcp:`, `bind_tcp:`, `seccomp_deny_network:`) are ignored and a warning is emitted. This makes cross-platform integration easier while keeping the security guarantees explicit: sandboxing is Linux-only.
140
+
55
141
  ## Restrict current process
56
142
 
57
- This is irreversible for the current thread/process. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.
143
+ This is irreversible for the current thread and its future children. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.
58
144
 
59
145
  ```ruby
60
146
  Landlock.restrict!(
61
147
  read: ["/usr", "/app"],
62
148
  write: ["/tmp/my-output"],
63
- connect_tcp: [443]
149
+ connect_tcp: [443],
150
+ scope: [:signal, :abstract_unix_socket],
151
+ allow_all_known: true
64
152
  )
65
153
  ```
66
154
 
155
+ `write:` grants the filesystem rights needed for practical writes under the listed paths, including directory traversal and reads (`read_file`/`read_dir`). If you need exact rights, use `paths:` with an explicit `rights:` list.
156
+
67
157
  ## Lower-level path rules
68
158
 
69
159
  ```ruby
@@ -76,6 +166,48 @@ Landlock.restrict!(
76
166
  )
77
167
  ```
78
168
 
169
+ ## Performance
170
+
171
+ Landlock enforcement is done by the kernel after a ruleset is installed. In normal use the practical cost should be dominated by the one-time sandbox setup and by the work your process already performs, not by Ruby-side wrappers.
172
+
173
+ This repository includes a small benchmark suite that compares common workloads before and after applying a read-only Landlock policy:
174
+
175
+ ```sh
176
+ bundle exec rake bench
177
+ # or
178
+ bundle exec ruby benchmark/landlock_overhead.rb
179
+ ```
180
+
181
+ The suite reports median timings for CPU-only work, file metadata reads, small file reads, directory scans, and the one-time ruleset setup cost. You can tune the run length with environment variables:
182
+
183
+ ```sh
184
+ SAMPLES=15 ITERATIONS=100000 DIR_ITERATIONS=5000 bundle exec rake bench
185
+ ```
186
+
187
+ Sample output looks like:
188
+
189
+ ```text
190
+ workload baseline landlocked delta delta %
191
+ --------------------------------------------------------------------
192
+ cpu_loop 0.650 ms 0.648 ms -0.002 ms -0.31%
193
+ file_stat 42.100 ms 42.300 ms 0.200 ms 0.48%
194
+ file_read 120.500 ms 120.900 ms 0.400 ms 0.33%
195
+ dir_scan 88.000 ms 88.200 ms 0.200 ms 0.23%
196
+
197
+ Setup cost (create ruleset, add read rules, restrict current process):
198
+ median 0.080 ms (25 samples)
199
+ ```
200
+
201
+ Treat small positive or negative deltas as noise and benchmark on the kernel, filesystem, and hardware you deploy on. The expected result is no practical steady-state overhead for typical application work, with a small one-time cost when installing the sandbox.
202
+
79
203
  ## Caveats
80
204
 
81
- Landlock is not a complete container. It does not impose CPU/memory limits, hide already-open file descriptors, or replace seccomp/namespaces/cgroups. For serious untrusted execution, combine it with controlled environment, `close_others`, resource limits, and preferably process isolation.
205
+ Landlock is not a complete container. It does not impose CPU/memory limits, hide already-open file descriptors, or replace seccomp/namespaces/cgroups. For serious untrusted execution, combine it with a controlled environment, `close_others`, resource limits, and preferably process isolation.
206
+
207
+ If a child fails during sandbox setup or `exec`, the helpers print a diagnostic and the child exits 127. That code can collide with a command that legitimately exits 127; unsupported kernels are checked before forking so they raise `Landlock::UnsupportedError` synchronously instead.
208
+
209
+ Path rules follow the kernel's normal path resolution when the rule is installed. Because paths are opened without `O_NOFOLLOW`, a symlink rule applies to the symlink target's inode, not to the symlink path itself.
210
+
211
+ Landlock only restricts access rights included in a ruleset's handled set: omitted categories remain allowed. Use `allow_all_known: true` when you want unlisted filesystem actions denied. The high-level helpers handle the categories you pass (`read`, `write`, `execute`, `connect_tcp`, `bind_tcp`, `scope`). Landlock's TCP rules do not cover UDP or pathname Unix-domain sockets; ABI v6+ scopes can restrict signals and abstract Unix-domain sockets.
212
+
213
+ `Landlock.restrict!` applies to the calling thread and its future children; already-running sibling threads are not retroactively sandboxed. Prefer `Landlock.exec` or `Landlock.spawn` for subprocess sandboxing from a larger Ruby application.
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+ require "tmpdir"
6
+ require "fileutils"
7
+ require "open3"
8
+
9
+ root = File.expand_path("..", __dir__)
10
+ lib_dir = File.join(root, "lib")
11
+ ext_lib_dir = File.join(lib_dir, "landlock")
12
+
13
+ $LOAD_PATH.unshift(lib_dir)
14
+ $LOAD_PATH.unshift(ext_lib_dir)
15
+
16
+ require "landlock"
17
+
18
+ module LandlockBench
19
+ ROOT = File.expand_path("..", __dir__)
20
+ DEFAULT_SAMPLES = Integer(ENV.fetch("SAMPLES", 7))
21
+ DEFAULT_ITERATIONS = Integer(ENV.fetch("ITERATIONS", 25_000))
22
+ DIR_ITERATIONS = Integer(ENV.fetch("DIR_ITERATIONS", 2_000))
23
+ SETUP_SAMPLES = Integer(ENV.fetch("SETUP_SAMPLES", 25))
24
+
25
+ WORKLOADS = [
26
+ ["cpu_loop", "integer arithmetic loop"],
27
+ ["file_stat", "File.stat on an allowed file"],
28
+ ["file_read", "File.binread of a small allowed file"],
29
+ ["dir_scan", "Dir.foreach over an allowed directory"]
30
+ ].freeze
31
+
32
+ module_function
33
+
34
+ def monotonic_ns
35
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
36
+ end
37
+
38
+ def measure
39
+ started = monotonic_ns
40
+ yield
41
+ monotonic_ns - started
42
+ end
43
+
44
+ def runtime_paths
45
+ [
46
+ File.dirname(RbConfig.ruby),
47
+ RbConfig::CONFIG["libdir"],
48
+ RbConfig::CONFIG["archlibdir"],
49
+ "/usr",
50
+ "/lib",
51
+ "/lib64",
52
+ "/etc"
53
+ ].compact.uniq.select { |path| File.exist?(path) }
54
+ end
55
+
56
+ def prepare_workspace
57
+ Dir.mktmpdir("landlock-bench") do |dir|
58
+ file = File.join(dir, "small.txt")
59
+ entries = File.join(dir, "entries")
60
+ Dir.mkdir(entries)
61
+ File.binwrite(file, "x" * 1024)
62
+ 100.times { |index| File.binwrite(File.join(entries, "entry-#{index}.txt"), "x") }
63
+
64
+ yield({ "dir" => dir, "file" => file, "entries" => entries })
65
+ end
66
+ end
67
+
68
+ def child_command(payload)
69
+ [RbConfig.ruby, __FILE__, "--child", JSON.generate(payload)]
70
+ end
71
+
72
+ def run_child(payload)
73
+ stdout, stderr, status = Open3.capture3(*child_command(payload), chdir: ROOT)
74
+ unless status.success?
75
+ abort "bench child failed (#{status.exitstatus})\nSTDOUT:\n#{stdout}\nSTDERR:\n#{stderr}"
76
+ end
77
+
78
+ JSON.parse(stdout)
79
+ end
80
+
81
+ def run_parent
82
+ puts "Landlock performance benchmark"
83
+ puts "Ruby: #{RUBY_DESCRIPTION}"
84
+ puts "Landlock ABI: #{Landlock.abi_version}"
85
+ puts "Samples: #{DEFAULT_SAMPLES}, iterations: #{DEFAULT_ITERATIONS}"
86
+ puts
87
+
88
+ prepare_workspace do |workspace|
89
+ read_paths = (runtime_paths + [workspace.fetch("dir")]).uniq
90
+ common = {
91
+ mode: "workloads",
92
+ iterations: DEFAULT_ITERATIONS,
93
+ dir_iterations: DIR_ITERATIONS,
94
+ read_paths: read_paths,
95
+ workspace: workspace
96
+ }
97
+
98
+ baseline = collect_samples(DEFAULT_SAMPLES) { run_child(common.merge(sandbox: false)) }
99
+
100
+ unless Landlock.supported?
101
+ puts "Landlock is not supported on this host; only baseline timings were collected."
102
+ print_workload_table(baseline, nil)
103
+ return
104
+ end
105
+
106
+ sandbox = collect_samples(DEFAULT_SAMPLES) { run_child(common.merge(sandbox: true)) }
107
+ setup = collect_samples(SETUP_SAMPLES) do
108
+ run_child(mode: "setup", read_paths: read_paths).fetch("setup_ns")
109
+ end
110
+
111
+ print_workload_table(baseline, sandbox)
112
+ puts
113
+ puts "Setup cost (create ruleset, add read rules, restrict current process):"
114
+ puts " median #{format_ms(median(setup))} (#{SETUP_SAMPLES} samples)"
115
+ puts
116
+ puts "Lower is better. Negative deltas mean the sandboxed sample was faster in this run."
117
+ puts "Expect small differences to be noise; compare medians across repeated runs."
118
+ end
119
+ end
120
+
121
+ def collect_samples(count)
122
+ Array.new(count) { yield }
123
+ end
124
+
125
+ def print_workload_table(baseline, sandbox)
126
+ puts format("%-12s %14s %14s %12s %10s", "workload", "baseline", "landlocked", "delta", "delta %")
127
+ puts "-" * 68
128
+
129
+ WORKLOADS.each do |name, description|
130
+ base = median(baseline.map { |sample| sample.fetch(name) })
131
+ if sandbox
132
+ locked = median(sandbox.map { |sample| sample.fetch(name) })
133
+ delta = locked - base
134
+ pct = base.positive? ? (delta.to_f / base * 100.0) : 0.0
135
+ puts format(
136
+ "%-12s %14s %14s %12s %9.2f%%",
137
+ name,
138
+ format_ms(base),
139
+ format_ms(locked),
140
+ format_ms(delta),
141
+ pct
142
+ )
143
+ else
144
+ puts format("%-12s %14s %14s %12s %10s", name, format_ms(base), "n/a", "n/a", "n/a")
145
+ end
146
+ puts " #{description}"
147
+ end
148
+ end
149
+
150
+ def median(values)
151
+ sorted = values.sort
152
+ midpoint = sorted.length / 2
153
+ return sorted.fetch(midpoint) if sorted.length.odd?
154
+
155
+ (sorted.fetch(midpoint - 1) + sorted.fetch(midpoint)) / 2.0
156
+ end
157
+
158
+ def format_ms(ns)
159
+ format("%.3f ms", ns.to_f / 1_000_000.0)
160
+ end
161
+
162
+ def run_child_mode(payload)
163
+ case payload.fetch("mode")
164
+ when "setup"
165
+ puts JSON.generate("setup_ns" => measure { restrict_for(payload) })
166
+ when "workloads"
167
+ restrict_for(payload) if payload.fetch("sandbox")
168
+ puts JSON.generate(run_workloads(payload))
169
+ else
170
+ raise ArgumentError, "unknown child mode: #{payload.fetch("mode").inspect}"
171
+ end
172
+ end
173
+
174
+ def restrict_for(payload)
175
+ Landlock.restrict!(read: payload.fetch("read_paths"))
176
+ end
177
+
178
+ def run_workloads(payload)
179
+ iterations = Integer(payload.fetch("iterations"))
180
+ dir_iterations = Integer(payload.fetch("dir_iterations"))
181
+ workspace = payload.fetch("workspace")
182
+ file = workspace.fetch("file")
183
+ entries = workspace.fetch("entries")
184
+ sink = 0
185
+
186
+ GC.disable
187
+ {
188
+ "cpu_loop" => measure do
189
+ iterations.times { |index| sink ^= ((index * 31) & 0xffff) }
190
+ end,
191
+ "file_stat" => measure do
192
+ iterations.times { sink ^= File.stat(file).size }
193
+ end,
194
+ "file_read" => measure do
195
+ iterations.times { sink ^= File.binread(file).bytesize }
196
+ end,
197
+ "dir_scan" => measure do
198
+ dir_iterations.times do
199
+ Dir.foreach(entries) { |entry| sink ^= entry.bytesize }
200
+ end
201
+ end
202
+ }.merge("sink" => sink)
203
+ ensure
204
+ GC.enable
205
+ end
206
+ end
207
+
208
+ if ARGV.first == "--child"
209
+ LandlockBench.run_child_mode(JSON.parse(ARGV.fetch(1)))
210
+ else
211
+ LandlockBench.run_parent
212
+ end