landlock 0.1.1 → 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: 60ae145a444fb3dd4c072fe4e5c8bac7de1836325a6d84e10be894d5b2f9ed2c
4
- data.tar.gz: 983b4da291286f4c903d12cb625508f1a569de918167a2c3f1fab450fcdeb866
3
+ metadata.gz: baea0cbd5b22406f288880dd5bcec85569c3134b256657d876a502f37e622364
4
+ data.tar.gz: a383e9cb807f51e2fd6e5d15a5d3d22c61d20a9ee2f9dffb046a1a97e24c2a6e
5
5
  SHA512:
6
- metadata.gz: ff7628efa5a0f464788e1adfe215d457e77eef7d8c94d1c6ee17a61f651732e3012afe0a1ae04792709b7255e5e8196576f4b5b19a0f7351a719cdfa6e72b49b
7
- data.tar.gz: 596b360c7dbcc2bd5079ea597df5b39d9d57c240ed6298d30ddeace1b955294e34bffe7f58353d3bcf01ae897bf2d738d5527c3cc25af1e485506de5f359d91e
6
+ metadata.gz: c385390bd6b239d5a7b495d74388ba9bb357787900ef73bfbd953d8353c49ce893117df8cc80fe95b0c9b2e8a6c836988a8540b2fbfdbce244ff76a6879d7067
7
+ data.tar.gz: b1d754bbfd7b60ec81cc4d036bc13ab627253fdf2fdb55a752477a43f917381d819f8fa20870abcad264e2a86d0c5635cb327799ccc614800e3adb1088b9dfec
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
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
+
5
13
  ## [0.1.1] - 2026-04-30
6
14
 
7
15
  ### Security
data/README.md CHANGED
@@ -66,6 +66,78 @@ Landlock.exec(
66
66
  )
67
67
  ```
68
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", "") }
108
+ )
109
+ ```
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
+
69
141
  ## Restrict current process
70
142
 
71
143
  This is irreversible for the current thread and its future children. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.
@@ -0,0 +1,369 @@
1
+ #include "../landlock_native.h"
2
+
3
+ #include <stdio.h>
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+ #include <limits.h>
7
+ #include <sys/resource.h>
8
+ #include <sys/stat.h>
9
+
10
+ #ifdef __linux__
11
+ #include <linux/audit.h>
12
+ #include <linux/filter.h>
13
+ #include <linux/seccomp.h>
14
+ #endif
15
+
16
+ #ifndef SECCOMP_RET_ALLOW
17
+ #define SECCOMP_RET_ALLOW 0x7fff0000U
18
+ #endif
19
+ #ifndef SECCOMP_RET_ERRNO
20
+ #define SECCOMP_RET_ERRNO 0x00050000U
21
+ #endif
22
+ #ifndef SECCOMP_SET_MODE_FILTER
23
+ #define SECCOMP_SET_MODE_FILTER 1
24
+ #endif
25
+
26
+ typedef struct {
27
+ char **items;
28
+ size_t len;
29
+ size_t cap;
30
+ } string_list;
31
+
32
+ typedef struct {
33
+ unsigned long long *items;
34
+ size_t len;
35
+ size_t cap;
36
+ } ull_list;
37
+
38
+ static void die(const char *message) {
39
+ perror(message);
40
+ _exit(126);
41
+ }
42
+
43
+ static void die_msg(const char *message) {
44
+ fprintf(stderr, "landlock-safe-exec: %s\n", message);
45
+ _exit(126);
46
+ }
47
+
48
+ static void string_list_push(string_list *list, char *value) {
49
+ if (list->len == list->cap) {
50
+ size_t cap = list->cap ? list->cap * 2 : 8;
51
+ char **items = realloc(list->items, cap * sizeof(char *));
52
+ if (!items) die("realloc");
53
+ list->items = items;
54
+ list->cap = cap;
55
+ }
56
+ list->items[list->len++] = value;
57
+ }
58
+
59
+ static void ull_list_push(ull_list *list, unsigned long long value) {
60
+ if (list->len == list->cap) {
61
+ size_t cap = list->cap ? list->cap * 2 : 8;
62
+ unsigned long long *items = realloc(list->items, cap * sizeof(unsigned long long));
63
+ if (!items) die("realloc");
64
+ list->items = items;
65
+ list->cap = cap;
66
+ }
67
+ list->items[list->len++] = value;
68
+ }
69
+
70
+ static unsigned long long parse_ull(const char *value, const char *name) {
71
+ if (!value || value[0] == '\0' || value[0] == '-') die_msg(name);
72
+
73
+ errno = 0;
74
+ char *end = NULL;
75
+ unsigned long long parsed = strtoull(value, &end, 10);
76
+ if (errno == ERANGE || !end || *end != '\0') die_msg(name);
77
+
78
+ return parsed;
79
+ }
80
+
81
+ static unsigned long long parse_port(const char *value) {
82
+ unsigned long long port = parse_ull(value, "TCP port must be an integer between 0 and 65535");
83
+ if (port > 65535ULL) die_msg("TCP port must be between 0 and 65535");
84
+ return port;
85
+ }
86
+
87
+ static int abi_version(void) {
88
+ long abi = ll_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
89
+ if (abi < 0 && (errno == ENOSYS || errno == EOPNOTSUPP)) return 0;
90
+ if (abi < 0) die("landlock_create_ruleset(version)");
91
+ return (int)abi;
92
+ }
93
+
94
+ static uint64_t known_fs_rights_for_abi(int abi) {
95
+ uint64_t rights = LANDLOCK_ACCESS_FS_EXECUTE |
96
+ LANDLOCK_ACCESS_FS_WRITE_FILE |
97
+ LANDLOCK_ACCESS_FS_READ_FILE |
98
+ LANDLOCK_ACCESS_FS_READ_DIR |
99
+ LANDLOCK_ACCESS_FS_REMOVE_DIR |
100
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
101
+ LANDLOCK_ACCESS_FS_MAKE_CHAR |
102
+ LANDLOCK_ACCESS_FS_MAKE_DIR |
103
+ LANDLOCK_ACCESS_FS_MAKE_REG |
104
+ LANDLOCK_ACCESS_FS_MAKE_SOCK |
105
+ LANDLOCK_ACCESS_FS_MAKE_FIFO |
106
+ LANDLOCK_ACCESS_FS_MAKE_BLOCK |
107
+ LANDLOCK_ACCESS_FS_MAKE_SYM;
108
+ if (abi >= 2) rights |= LANDLOCK_ACCESS_FS_REFER;
109
+ if (abi >= 3) rights |= LANDLOCK_ACCESS_FS_TRUNCATE;
110
+ if (abi >= 5) rights |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
111
+ return rights;
112
+ }
113
+
114
+ static uint64_t read_rights(void) {
115
+ return LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
116
+ }
117
+
118
+ static uint64_t execute_rights(void) {
119
+ return LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
120
+ }
121
+
122
+ static uint64_t write_rights(int abi) {
123
+ return known_fs_rights_for_abi(abi) & ~LANDLOCK_ACCESS_FS_EXECUTE;
124
+ }
125
+
126
+ static uint64_t file_path_rights(void) {
127
+ return LANDLOCK_ACCESS_FS_EXECUTE |
128
+ LANDLOCK_ACCESS_FS_WRITE_FILE |
129
+ LANDLOCK_ACCESS_FS_READ_FILE |
130
+ LANDLOCK_ACCESS_FS_TRUNCATE |
131
+ LANDLOCK_ACCESS_FS_IOCTL_DEV;
132
+ }
133
+
134
+ static void add_path_rule(int fd, const char *path, uint64_t rights) {
135
+ int parent_fd = open(path, O_PATH | O_CLOEXEC);
136
+ if (parent_fd < 0) die("open(path rule)");
137
+
138
+ struct stat st;
139
+ if (fstat(parent_fd, &st) != 0) die("fstat(path rule)");
140
+ if (!S_ISDIR(st.st_mode)) rights &= file_path_rights();
141
+
142
+ struct rb_landlock_path_beneath_attr rule;
143
+ memset(&rule, 0, sizeof(rule));
144
+ rule.allowed_access = rights;
145
+ rule.parent_fd = parent_fd;
146
+
147
+ long ret = ll_add_rule(fd, LANDLOCK_RULE_PATH_BENEATH, &rule, 0);
148
+ int saved_errno = errno;
149
+ close(parent_fd);
150
+ if (ret < 0) {
151
+ errno = saved_errno;
152
+ die("landlock_add_rule(path_beneath)");
153
+ }
154
+ }
155
+
156
+ static void add_net_rule(int fd, unsigned long long port, uint64_t rights) {
157
+ if (port > 65535ULL) die_msg("TCP port must be between 0 and 65535");
158
+ struct rb_landlock_net_port_attr rule;
159
+ memset(&rule, 0, sizeof(rule));
160
+ rule.allowed_access = rights;
161
+ rule.port = port;
162
+ if (ll_add_rule(fd, LANDLOCK_RULE_NET_PORT, &rule, 0) < 0) die("landlock_add_rule(net_port)");
163
+ }
164
+
165
+ static void apply_landlock(string_list *read_paths, string_list *write_paths, string_list *execute_paths,
166
+ ull_list *connect_ports, ull_list *bind_ports, int allow_all_known) {
167
+ int need_fs = read_paths->len || write_paths->len || execute_paths->len || allow_all_known;
168
+ int need_net = connect_ports->len || bind_ports->len;
169
+ if (!need_fs && !need_net) return;
170
+
171
+ int abi = abi_version();
172
+ if (abi <= 0) die_msg("Linux Landlock is unavailable");
173
+ if (need_net && abi < 4) die_msg("Landlock network rules require ABI v4+");
174
+
175
+ uint64_t fs_handled = allow_all_known ? known_fs_rights_for_abi(abi) : 0;
176
+ if (!allow_all_known) {
177
+ if (read_paths->len) fs_handled |= read_rights();
178
+ if (execute_paths->len) fs_handled |= execute_rights();
179
+ if (write_paths->len) fs_handled |= write_rights(abi);
180
+ }
181
+ uint64_t net_handled = 0;
182
+ if (bind_ports->len) net_handled |= LANDLOCK_ACCESS_NET_BIND_TCP;
183
+ if (connect_ports->len) net_handled |= LANDLOCK_ACCESS_NET_CONNECT_TCP;
184
+
185
+ struct rb_landlock_ruleset_attr attr;
186
+ memset(&attr, 0, sizeof(attr));
187
+ attr.handled_access_fs = fs_handled;
188
+ attr.handled_access_net = net_handled;
189
+
190
+ size_t attr_size = net_handled ? offsetof(struct rb_landlock_ruleset_attr, scoped) : offsetof(struct rb_landlock_ruleset_attr, handled_access_net);
191
+ int fd = (int)ll_create_ruleset(&attr, attr_size, 0);
192
+ if (fd < 0) die("landlock_create_ruleset");
193
+
194
+ for (size_t i = 0; i < read_paths->len; i++) add_path_rule(fd, read_paths->items[i], read_rights());
195
+ for (size_t i = 0; i < execute_paths->len; i++) add_path_rule(fd, execute_paths->items[i], execute_rights());
196
+ for (size_t i = 0; i < write_paths->len; i++) add_path_rule(fd, write_paths->items[i], write_rights(abi));
197
+ for (size_t i = 0; i < connect_ports->len; i++) add_net_rule(fd, connect_ports->items[i], LANDLOCK_ACCESS_NET_CONNECT_TCP);
198
+ for (size_t i = 0; i < bind_ports->len; i++) add_net_rule(fd, bind_ports->items[i], LANDLOCK_ACCESS_NET_BIND_TCP);
199
+
200
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) die("prctl(PR_SET_NO_NEW_PRIVS)");
201
+ if (ll_restrict_self(fd, 0) < 0) die("landlock_restrict_self");
202
+ close(fd);
203
+ }
204
+
205
+ static void apply_rlimit(const char *spec) {
206
+ char *copy = strdup(spec);
207
+ if (!copy) die("strdup");
208
+ char *eq = strchr(copy, '=');
209
+ if (!eq) die_msg("rlimit must be name=value");
210
+ *eq = '\0';
211
+ unsigned long long value = parse_ull(eq + 1, "rlimit value must be a non-negative integer");
212
+ int resource = -1;
213
+
214
+ if (strcmp(copy, "cpu_seconds") == 0) resource = RLIMIT_CPU;
215
+ #ifdef RLIMIT_AS
216
+ else if (strcmp(copy, "memory_bytes") == 0) resource = RLIMIT_AS;
217
+ #endif
218
+ else if (strcmp(copy, "file_size_bytes") == 0) resource = RLIMIT_FSIZE;
219
+ else if (strcmp(copy, "open_files") == 0) resource = RLIMIT_NOFILE;
220
+ #ifdef RLIMIT_NPROC
221
+ else if (strcmp(copy, "processes") == 0) resource = RLIMIT_NPROC;
222
+ #endif
223
+ else die_msg("unknown rlimit");
224
+
225
+ struct rlimit limit;
226
+ limit.rlim_cur = (rlim_t)value;
227
+ limit.rlim_max = (rlim_t)value;
228
+ if (setrlimit(resource, &limit) != 0) die("setrlimit");
229
+ free(copy);
230
+ }
231
+
232
+ static int deny_syscalls[] = {
233
+ #ifdef __NR_socket
234
+ __NR_socket,
235
+ #endif
236
+ #ifdef __NR_socketpair
237
+ __NR_socketpair,
238
+ #endif
239
+ #ifdef __NR_connect
240
+ __NR_connect,
241
+ #endif
242
+ #ifdef __NR_bind
243
+ __NR_bind,
244
+ #endif
245
+ #ifdef __NR_listen
246
+ __NR_listen,
247
+ #endif
248
+ #ifdef __NR_accept
249
+ __NR_accept,
250
+ #endif
251
+ #ifdef __NR_accept4
252
+ __NR_accept4,
253
+ #endif
254
+ #ifdef __NR_sendto
255
+ __NR_sendto,
256
+ #endif
257
+ #ifdef __NR_sendmsg
258
+ __NR_sendmsg,
259
+ #endif
260
+ #ifdef __NR_sendmmsg
261
+ __NR_sendmmsg,
262
+ #endif
263
+ #ifdef __NR_recvfrom
264
+ __NR_recvfrom,
265
+ #endif
266
+ #ifdef __NR_recvmsg
267
+ __NR_recvmsg,
268
+ #endif
269
+ #ifdef __NR_recvmmsg
270
+ __NR_recvmmsg,
271
+ #endif
272
+ #ifdef __NR_socketcall
273
+ __NR_socketcall,
274
+ #endif
275
+ };
276
+
277
+ #if defined(__x86_64__) && defined(AUDIT_ARCH_X86_64)
278
+ #define EXPECTED_AUDIT_ARCH AUDIT_ARCH_X86_64
279
+ #elif defined(__aarch64__) && defined(AUDIT_ARCH_AARCH64)
280
+ #define EXPECTED_AUDIT_ARCH AUDIT_ARCH_AARCH64
281
+ #elif defined(__i386__) && defined(AUDIT_ARCH_I386)
282
+ #define EXPECTED_AUDIT_ARCH AUDIT_ARCH_I386
283
+ #endif
284
+
285
+ #ifndef SECCOMP_RET_KILL_PROCESS
286
+ #define SECCOMP_RET_KILL_PROCESS 0x80000000U
287
+ #endif
288
+
289
+ static void apply_seccomp_deny_network(void) {
290
+ size_t count = sizeof(deny_syscalls) / sizeof(deny_syscalls[0]);
291
+ if (count == 0) return;
292
+
293
+ size_t len = 1 + (2 * count) + 1;
294
+ #ifdef EXPECTED_AUDIT_ARCH
295
+ len += 3;
296
+ #endif
297
+ struct sock_filter *filter = calloc(len, sizeof(struct sock_filter));
298
+ if (!filter) die("calloc");
299
+
300
+ size_t pc = 0;
301
+ #ifdef EXPECTED_AUDIT_ARCH
302
+ filter[pc++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch));
303
+ filter[pc++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, EXPECTED_AUDIT_ARCH, 1, 0);
304
+ filter[pc++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
305
+ #endif
306
+ filter[pc++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
307
+ for (size_t i = 0; i < count; i++) {
308
+ filter[pc++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)deny_syscalls[i], 0, 1);
309
+ filter[pc++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM);
310
+ }
311
+ filter[pc++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
312
+
313
+ struct sock_fprog prog;
314
+ prog.len = (unsigned short)pc;
315
+ prog.filter = filter;
316
+
317
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) die("prctl(PR_SET_NO_NEW_PRIVS)");
318
+ #ifdef SYS_seccomp
319
+ if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog) != 0)
320
+ #endif
321
+ {
322
+ if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) die("seccomp(SECCOMP_SET_MODE_FILTER)");
323
+ }
324
+ free(filter);
325
+ }
326
+
327
+ static char *require_arg(int argc, char **argv, int *i) {
328
+ if (*i + 1 >= argc) die_msg("missing option argument");
329
+ (*i)++;
330
+ return argv[*i];
331
+ }
332
+
333
+ int main(int argc, char **argv) {
334
+ string_list read_paths = {0}, write_paths = {0}, execute_paths = {0}, env_vars = {0};
335
+ ull_list connect_ports = {0}, bind_ports = {0};
336
+ int unsetenv_others = 0, seccomp_deny_network = 0, allow_all_known = 0;
337
+ char *chdir_path = NULL;
338
+ int command_index = -1;
339
+
340
+ for (int i = 1; i < argc; i++) {
341
+ if (strcmp(argv[i], "--") == 0) { command_index = i + 1; break; }
342
+ if (strcmp(argv[i], "--read") == 0) string_list_push(&read_paths, require_arg(argc, argv, &i));
343
+ else if (strcmp(argv[i], "--write") == 0) string_list_push(&write_paths, require_arg(argc, argv, &i));
344
+ else if (strcmp(argv[i], "--execute") == 0) string_list_push(&execute_paths, require_arg(argc, argv, &i));
345
+ else if (strcmp(argv[i], "--connect-tcp") == 0) ull_list_push(&connect_ports, parse_port(require_arg(argc, argv, &i)));
346
+ else if (strcmp(argv[i], "--bind-tcp") == 0) ull_list_push(&bind_ports, parse_port(require_arg(argc, argv, &i)));
347
+ else if (strcmp(argv[i], "--chdir") == 0) chdir_path = require_arg(argc, argv, &i);
348
+ else if (strcmp(argv[i], "--env") == 0) string_list_push(&env_vars, require_arg(argc, argv, &i));
349
+ else if (strcmp(argv[i], "--unsetenv-others") == 0) unsetenv_others = 1;
350
+ else if (strcmp(argv[i], "--rlimit") == 0) apply_rlimit(require_arg(argc, argv, &i));
351
+ else if (strcmp(argv[i], "--seccomp-deny-network") == 0) seccomp_deny_network = 1;
352
+ else if (strcmp(argv[i], "--allow-all-known") == 0) allow_all_known = 1;
353
+ else die_msg("unknown option");
354
+ }
355
+
356
+ if (command_index < 0 || command_index >= argc) die_msg("missing command after --");
357
+
358
+ if (chdir_path && chdir(chdir_path) != 0) die("chdir");
359
+ if (unsetenv_others && clearenv() != 0) die("clearenv");
360
+ for (size_t i = 0; i < env_vars.len; i++) {
361
+ if (putenv(env_vars.items[i]) != 0) die("putenv");
362
+ }
363
+
364
+ apply_landlock(&read_paths, &write_paths, &execute_paths, &connect_ports, &bind_ports, allow_all_known);
365
+ if (seccomp_deny_network) apply_seccomp_deny_network();
366
+
367
+ execvp(argv[command_index], &argv[command_index]);
368
+ die("execvp");
369
+ }
@@ -5,8 +5,38 @@ require "mkmf"
5
5
  abort "missing ruby headers" unless have_header("ruby.h")
6
6
 
7
7
  have_header("linux/landlock.h")
8
+ have_header("linux/seccomp.h")
9
+ have_header("linux/filter.h")
8
10
  have_header("sys/prctl.h")
9
11
  have_header("sys/syscall.h")
12
+ have_header("sys/resource.h")
10
13
  have_header("fcntl.h")
11
14
 
12
15
  create_makefile("landlock/landlock")
16
+
17
+ if RUBY_PLATFORM.include?("linux")
18
+ helper = "landlock-safe-exec"
19
+ helper_src = "$(srcdir)/bin/safe_exec_helper.c"
20
+ helper_dest = "$(RUBYARCHDIR)/#{helper}"
21
+
22
+ File.open("Makefile", "a") do |makefile|
23
+ makefile.puts <<~MAKE
24
+
25
+ all: #{helper}
26
+
27
+ #{helper}: #{helper_src}
28
+ \t$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) #{helper_src} -o #{helper} $(LIBS)
29
+
30
+ install: install-#{helper}
31
+
32
+ install-#{helper}: #{helper}
33
+ \t$(MAKEDIRS) $(RUBYARCHDIR)
34
+ \t$(INSTALL_PROG) #{helper} #{helper_dest}
35
+
36
+ clean-local::
37
+ \t$(Q)$(RM) #{helper}
38
+
39
+ clean: clean-local
40
+ MAKE
41
+ end
42
+ end