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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +72 -0
- data/ext/landlock/bin/safe_exec_helper.c +369 -0
- data/ext/landlock/extconf.rb +30 -0
- data/ext/landlock/landlock.c +6 -164
- data/ext/landlock/landlock_native.h +167 -0
- data/lib/landlock/safe_exec.rb +522 -0
- data/lib/landlock/version.rb +1 -1
- data/lib/landlock.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: baea0cbd5b22406f288880dd5bcec85569c3134b256657d876a502f37e622364
|
|
4
|
+
data.tar.gz: a383e9cb807f51e2fd6e5d15a5d3d22c61d20a9ee2f9dffb046a1a97e24c2a6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+
}
|
data/ext/landlock/extconf.rb
CHANGED
|
@@ -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
|