landlock 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/ext/landlock/extconf.rb +12 -0
- data/ext/landlock/landlock.c +280 -0
- data/lib/landlock/version.rb +5 -0
- data/lib/landlock.rb +195 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6cf65089e4af85184c67c7a2e6c679932dd21c492882ccf9cf4612d5603cb7a4
|
|
4
|
+
data.tar.gz: a987aaefb3e9ec52f69087c533fed5e384157aa4e1caebbc56226030c84e1112
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6de7d6d837c0365fcf6d18e4cf3d41d291d6d043b71a3b05d1501e3c805faf7acd082865e67bad699814f0bdd342859f7790e80c3d58e510b83f8121e3445955
|
|
7
|
+
data.tar.gz: f87863bd3de8f8a8d5ce6ffd56382bafcf6cefc7ec74dca9ea9a7e6df7c5f759c556f51a2e687668d0fd30b809cf07a8136b73e26467dd069d021977a5abd4e2
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sam Saffron
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# landlock
|
|
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.
|
|
4
|
+
|
|
5
|
+
This gem includes a small native extension around the three Landlock syscalls and a Ruby API for safe subprocess execution.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Experimental. Filesystem support requires Landlock ABI v1+. TCP network rules require ABI v4+.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "landlock"
|
|
13
|
+
|
|
14
|
+
puts Landlock.abi_version
|
|
15
|
+
puts Landlock.supported?
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Safe subprocess execution
|
|
19
|
+
|
|
20
|
+
Allow Ruby to execute and read its runtime, but only allow outbound TCP connections to port 443:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
status = Landlock.exec(
|
|
24
|
+
[RbConfig.ruby, "script.rb"],
|
|
25
|
+
read: ["/usr", "/lib", "/lib64", "/etc/ssl"],
|
|
26
|
+
execute: ["/usr", "/lib", "/lib64"],
|
|
27
|
+
connect_tcp: [443]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
abort "failed" unless status.success?
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Deny all outbound TCP except the listed ports:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Landlock.exec(
|
|
37
|
+
["curl", "https://example.com"],
|
|
38
|
+
read: ["/usr", "/lib", "/lib64", "/etc/ssl", "/etc/resolv.conf", "/etc/hosts"],
|
|
39
|
+
execute: ["/usr", "/lib", "/lib64"],
|
|
40
|
+
connect_tcp: [443]
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Allow binding a local TCP port:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Landlock.exec(
|
|
48
|
+
[RbConfig.ruby, "server.rb"],
|
|
49
|
+
read: ["/usr", "/lib", "/lib64", Dir.pwd],
|
|
50
|
+
execute: ["/usr", "/lib", "/lib64"],
|
|
51
|
+
bind_tcp: [9292]
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Restrict current process
|
|
56
|
+
|
|
57
|
+
This is irreversible for the current thread/process. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
Landlock.restrict!(
|
|
61
|
+
read: ["/usr", "/app"],
|
|
62
|
+
write: ["/tmp/my-output"],
|
|
63
|
+
connect_tcp: [443]
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Lower-level path rules
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Landlock.restrict!(
|
|
71
|
+
paths: [
|
|
72
|
+
{ path: "/usr", rights: %i[read_file read_dir execute] },
|
|
73
|
+
{ path: "/tmp/out", rights: %i[read_file read_dir write_file truncate make_reg remove_file] }
|
|
74
|
+
],
|
|
75
|
+
connect_tcp: [443]
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Caveats
|
|
80
|
+
|
|
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.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
|
|
5
|
+
abort "missing ruby headers" unless have_header("ruby.h")
|
|
6
|
+
|
|
7
|
+
have_header("linux/landlock.h")
|
|
8
|
+
have_header("sys/prctl.h")
|
|
9
|
+
have_header("sys/syscall.h")
|
|
10
|
+
have_header("fcntl.h")
|
|
11
|
+
|
|
12
|
+
create_makefile("landlock/landlock")
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#include "ruby.h"
|
|
2
|
+
|
|
3
|
+
#include <errno.h>
|
|
4
|
+
#include <fcntl.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
#include <stddef.h>
|
|
7
|
+
#include <string.h>
|
|
8
|
+
#include <unistd.h>
|
|
9
|
+
#include <sys/prctl.h>
|
|
10
|
+
#include <sys/syscall.h>
|
|
11
|
+
|
|
12
|
+
#ifdef HAVE_LINUX_LANDLOCK_H
|
|
13
|
+
#include <linux/landlock.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
#ifndef SYS_landlock_create_ruleset
|
|
17
|
+
# if defined(__x86_64__)
|
|
18
|
+
# define SYS_landlock_create_ruleset 444
|
|
19
|
+
# define SYS_landlock_add_rule 445
|
|
20
|
+
# define SYS_landlock_restrict_self 446
|
|
21
|
+
# elif defined(__aarch64__)
|
|
22
|
+
# define SYS_landlock_create_ruleset 444
|
|
23
|
+
# define SYS_landlock_add_rule 445
|
|
24
|
+
# define SYS_landlock_restrict_self 446
|
|
25
|
+
# elif defined(__i386__)
|
|
26
|
+
# define SYS_landlock_create_ruleset 451
|
|
27
|
+
# define SYS_landlock_add_rule 452
|
|
28
|
+
# define SYS_landlock_restrict_self 453
|
|
29
|
+
# endif
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
#ifndef LANDLOCK_CREATE_RULESET_VERSION
|
|
33
|
+
#define LANDLOCK_CREATE_RULESET_VERSION (1U << 0)
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
#ifndef LANDLOCK_RULE_PATH_BENEATH
|
|
37
|
+
#define LANDLOCK_RULE_PATH_BENEATH 1
|
|
38
|
+
#endif
|
|
39
|
+
|
|
40
|
+
#ifndef LANDLOCK_RULE_NET_PORT
|
|
41
|
+
#define LANDLOCK_RULE_NET_PORT 2
|
|
42
|
+
#endif
|
|
43
|
+
|
|
44
|
+
#ifndef LANDLOCK_ACCESS_FS_EXECUTE
|
|
45
|
+
#define LANDLOCK_ACCESS_FS_EXECUTE (1ULL << 0)
|
|
46
|
+
#endif
|
|
47
|
+
#ifndef LANDLOCK_ACCESS_FS_WRITE_FILE
|
|
48
|
+
#define LANDLOCK_ACCESS_FS_WRITE_FILE (1ULL << 1)
|
|
49
|
+
#endif
|
|
50
|
+
#ifndef LANDLOCK_ACCESS_FS_READ_FILE
|
|
51
|
+
#define LANDLOCK_ACCESS_FS_READ_FILE (1ULL << 2)
|
|
52
|
+
#endif
|
|
53
|
+
#ifndef LANDLOCK_ACCESS_FS_READ_DIR
|
|
54
|
+
#define LANDLOCK_ACCESS_FS_READ_DIR (1ULL << 3)
|
|
55
|
+
#endif
|
|
56
|
+
#ifndef LANDLOCK_ACCESS_FS_REMOVE_DIR
|
|
57
|
+
#define LANDLOCK_ACCESS_FS_REMOVE_DIR (1ULL << 4)
|
|
58
|
+
#endif
|
|
59
|
+
#ifndef LANDLOCK_ACCESS_FS_REMOVE_FILE
|
|
60
|
+
#define LANDLOCK_ACCESS_FS_REMOVE_FILE (1ULL << 5)
|
|
61
|
+
#endif
|
|
62
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_CHAR
|
|
63
|
+
#define LANDLOCK_ACCESS_FS_MAKE_CHAR (1ULL << 6)
|
|
64
|
+
#endif
|
|
65
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_DIR
|
|
66
|
+
#define LANDLOCK_ACCESS_FS_MAKE_DIR (1ULL << 7)
|
|
67
|
+
#endif
|
|
68
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_REG
|
|
69
|
+
#define LANDLOCK_ACCESS_FS_MAKE_REG (1ULL << 8)
|
|
70
|
+
#endif
|
|
71
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_SOCK
|
|
72
|
+
#define LANDLOCK_ACCESS_FS_MAKE_SOCK (1ULL << 9)
|
|
73
|
+
#endif
|
|
74
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_FIFO
|
|
75
|
+
#define LANDLOCK_ACCESS_FS_MAKE_FIFO (1ULL << 10)
|
|
76
|
+
#endif
|
|
77
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_BLOCK
|
|
78
|
+
#define LANDLOCK_ACCESS_FS_MAKE_BLOCK (1ULL << 11)
|
|
79
|
+
#endif
|
|
80
|
+
#ifndef LANDLOCK_ACCESS_FS_MAKE_SYM
|
|
81
|
+
#define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12)
|
|
82
|
+
#endif
|
|
83
|
+
#ifndef LANDLOCK_ACCESS_FS_REFER
|
|
84
|
+
#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
|
|
85
|
+
#endif
|
|
86
|
+
#ifndef LANDLOCK_ACCESS_FS_TRUNCATE
|
|
87
|
+
#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
|
|
88
|
+
#endif
|
|
89
|
+
#ifndef LANDLOCK_ACCESS_FS_IOCTL_DEV
|
|
90
|
+
#define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15)
|
|
91
|
+
#endif
|
|
92
|
+
|
|
93
|
+
#ifndef LANDLOCK_ACCESS_NET_BIND_TCP
|
|
94
|
+
#define LANDLOCK_ACCESS_NET_BIND_TCP (1ULL << 0)
|
|
95
|
+
#endif
|
|
96
|
+
#ifndef LANDLOCK_ACCESS_NET_CONNECT_TCP
|
|
97
|
+
#define LANDLOCK_ACCESS_NET_CONNECT_TCP (1ULL << 1)
|
|
98
|
+
#endif
|
|
99
|
+
|
|
100
|
+
#ifndef O_PATH
|
|
101
|
+
#define O_PATH 010000000
|
|
102
|
+
#endif
|
|
103
|
+
|
|
104
|
+
#ifndef O_CLOEXEC
|
|
105
|
+
#define O_CLOEXEC 02000000
|
|
106
|
+
#endif
|
|
107
|
+
|
|
108
|
+
struct rb_landlock_ruleset_attr {
|
|
109
|
+
uint64_t handled_access_fs;
|
|
110
|
+
uint64_t handled_access_net;
|
|
111
|
+
uint64_t scoped;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
struct rb_landlock_path_beneath_attr {
|
|
115
|
+
uint64_t allowed_access;
|
|
116
|
+
int32_t parent_fd;
|
|
117
|
+
} __attribute__((packed));
|
|
118
|
+
|
|
119
|
+
struct rb_landlock_net_port_attr {
|
|
120
|
+
uint64_t allowed_access;
|
|
121
|
+
uint64_t port;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
static VALUE mLandlock;
|
|
125
|
+
static VALUE eLandlockError;
|
|
126
|
+
static VALUE eSyscallError;
|
|
127
|
+
|
|
128
|
+
static long ll_create_ruleset(const void *attr, size_t size, uint32_t flags) {
|
|
129
|
+
#ifdef SYS_landlock_create_ruleset
|
|
130
|
+
return syscall(SYS_landlock_create_ruleset, attr, size, flags);
|
|
131
|
+
#else
|
|
132
|
+
errno = ENOSYS;
|
|
133
|
+
return -1;
|
|
134
|
+
#endif
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static long ll_add_rule(int ruleset_fd, int rule_type, const void *rule_attr, uint32_t flags) {
|
|
138
|
+
#ifdef SYS_landlock_add_rule
|
|
139
|
+
return syscall(SYS_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
|
|
140
|
+
#else
|
|
141
|
+
errno = ENOSYS;
|
|
142
|
+
return -1;
|
|
143
|
+
#endif
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static long ll_restrict_self(int ruleset_fd, uint32_t flags) {
|
|
147
|
+
#ifdef SYS_landlock_restrict_self
|
|
148
|
+
return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
|
|
149
|
+
#else
|
|
150
|
+
errno = ENOSYS;
|
|
151
|
+
return -1;
|
|
152
|
+
#endif
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static void raise_syscall_error(const char *syscall_name) {
|
|
156
|
+
int saved_errno = errno;
|
|
157
|
+
VALUE err = rb_funcall(eSyscallError, rb_intern("new"), 3,
|
|
158
|
+
rb_str_new_cstr(syscall_name),
|
|
159
|
+
INT2NUM(saved_errno),
|
|
160
|
+
rb_sprintf("%s failed: %s", syscall_name, strerror(saved_errno)));
|
|
161
|
+
rb_exc_raise(err);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static VALUE rb_ll_abi_version(VALUE self) {
|
|
165
|
+
long abi = ll_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
|
|
166
|
+
if (abi < 0) {
|
|
167
|
+
if (errno == ENOSYS || errno == EOPNOTSUPP) return INT2FIX(0);
|
|
168
|
+
raise_syscall_error("landlock_create_ruleset");
|
|
169
|
+
}
|
|
170
|
+
return LONG2NUM(abi);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static VALUE rb_ll_create_ruleset(VALUE self, VALUE fs_bits, VALUE net_bits) {
|
|
174
|
+
struct rb_landlock_ruleset_attr attr;
|
|
175
|
+
uint64_t handled_access_net = NUM2ULL(net_bits);
|
|
176
|
+
size_t attr_size = handled_access_net == 0 ?
|
|
177
|
+
offsetof(struct rb_landlock_ruleset_attr, handled_access_net) :
|
|
178
|
+
offsetof(struct rb_landlock_ruleset_attr, scoped);
|
|
179
|
+
|
|
180
|
+
memset(&attr, 0, sizeof(attr));
|
|
181
|
+
attr.handled_access_fs = NUM2ULL(fs_bits);
|
|
182
|
+
attr.handled_access_net = handled_access_net;
|
|
183
|
+
|
|
184
|
+
long fd = ll_create_ruleset(&attr, attr_size, 0);
|
|
185
|
+
if (fd < 0) raise_syscall_error("landlock_create_ruleset");
|
|
186
|
+
return INT2NUM(fd);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static VALUE rb_ll_add_path_rule(VALUE self, VALUE ruleset_fd, VALUE path, VALUE access_bits) {
|
|
190
|
+
Check_Type(path, T_STRING);
|
|
191
|
+
const char *cpath = StringValueCStr(path);
|
|
192
|
+
int parent_fd = open(cpath, O_PATH | O_CLOEXEC);
|
|
193
|
+
if (parent_fd < 0) raise_syscall_error("open");
|
|
194
|
+
|
|
195
|
+
struct rb_landlock_path_beneath_attr rule;
|
|
196
|
+
memset(&rule, 0, sizeof(rule));
|
|
197
|
+
rule.allowed_access = NUM2ULL(access_bits);
|
|
198
|
+
rule.parent_fd = parent_fd;
|
|
199
|
+
|
|
200
|
+
long ret = ll_add_rule(NUM2INT(ruleset_fd), LANDLOCK_RULE_PATH_BENEATH, &rule, 0);
|
|
201
|
+
int saved_errno = errno;
|
|
202
|
+
close(parent_fd);
|
|
203
|
+
if (ret < 0) {
|
|
204
|
+
errno = saved_errno;
|
|
205
|
+
raise_syscall_error("landlock_add_rule(path_beneath)");
|
|
206
|
+
}
|
|
207
|
+
return Qtrue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static VALUE rb_ll_add_net_rule(VALUE self, VALUE ruleset_fd, VALUE port, VALUE access_bits) {
|
|
211
|
+
unsigned long long p = NUM2ULL(port);
|
|
212
|
+
if (p > 65535ULL) rb_raise(rb_eArgError, "TCP port must be between 0 and 65535");
|
|
213
|
+
|
|
214
|
+
struct rb_landlock_net_port_attr rule;
|
|
215
|
+
memset(&rule, 0, sizeof(rule));
|
|
216
|
+
rule.allowed_access = NUM2ULL(access_bits);
|
|
217
|
+
rule.port = p;
|
|
218
|
+
|
|
219
|
+
long ret = ll_add_rule(NUM2INT(ruleset_fd), LANDLOCK_RULE_NET_PORT, &rule, 0);
|
|
220
|
+
if (ret < 0) raise_syscall_error("landlock_add_rule(net_port)");
|
|
221
|
+
return Qtrue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static VALUE rb_ll_restrict_self(VALUE self, VALUE ruleset_fd) {
|
|
225
|
+
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
|
|
226
|
+
raise_syscall_error("prctl(PR_SET_NO_NEW_PRIVS)");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
long ret = ll_restrict_self(NUM2INT(ruleset_fd), 0);
|
|
230
|
+
if (ret < 0) raise_syscall_error("landlock_restrict_self");
|
|
231
|
+
return Qtrue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
|
|
235
|
+
int fd = NUM2INT(fd_value);
|
|
236
|
+
if (fd >= 0) close(fd);
|
|
237
|
+
return Qnil;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
void Init_landlock(void) {
|
|
241
|
+
mLandlock = rb_define_module("Landlock");
|
|
242
|
+
|
|
243
|
+
if (rb_const_defined(mLandlock, rb_intern("Error"))) {
|
|
244
|
+
eLandlockError = rb_const_get(mLandlock, rb_intern("Error"));
|
|
245
|
+
} else {
|
|
246
|
+
eLandlockError = rb_define_class_under(mLandlock, "Error", rb_eStandardError);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (rb_const_defined(mLandlock, rb_intern("SyscallError"))) {
|
|
250
|
+
eSyscallError = rb_const_get(mLandlock, rb_intern("SyscallError"));
|
|
251
|
+
} else {
|
|
252
|
+
eSyscallError = rb_define_class_under(mLandlock, "SyscallError", eLandlockError);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
rb_define_singleton_method(mLandlock, "abi_version", rb_ll_abi_version, 0);
|
|
256
|
+
rb_define_singleton_method(mLandlock, "_create_ruleset", rb_ll_create_ruleset, 2);
|
|
257
|
+
rb_define_singleton_method(mLandlock, "_add_path_rule", rb_ll_add_path_rule, 3);
|
|
258
|
+
rb_define_singleton_method(mLandlock, "_add_net_rule", rb_ll_add_net_rule, 3);
|
|
259
|
+
rb_define_singleton_method(mLandlock, "_restrict_self", rb_ll_restrict_self, 1);
|
|
260
|
+
rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
|
|
261
|
+
|
|
262
|
+
rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
|
|
263
|
+
rb_define_const(mLandlock, "ACCESS_FS_WRITE_FILE", ULL2NUM(LANDLOCK_ACCESS_FS_WRITE_FILE));
|
|
264
|
+
rb_define_const(mLandlock, "ACCESS_FS_READ_FILE", ULL2NUM(LANDLOCK_ACCESS_FS_READ_FILE));
|
|
265
|
+
rb_define_const(mLandlock, "ACCESS_FS_READ_DIR", ULL2NUM(LANDLOCK_ACCESS_FS_READ_DIR));
|
|
266
|
+
rb_define_const(mLandlock, "ACCESS_FS_REMOVE_DIR", ULL2NUM(LANDLOCK_ACCESS_FS_REMOVE_DIR));
|
|
267
|
+
rb_define_const(mLandlock, "ACCESS_FS_REMOVE_FILE", ULL2NUM(LANDLOCK_ACCESS_FS_REMOVE_FILE));
|
|
268
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_CHAR", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_CHAR));
|
|
269
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_DIR", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_DIR));
|
|
270
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_REG", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_REG));
|
|
271
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_SOCK", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_SOCK));
|
|
272
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_FIFO", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_FIFO));
|
|
273
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_BLOCK", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_BLOCK));
|
|
274
|
+
rb_define_const(mLandlock, "ACCESS_FS_MAKE_SYM", ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_SYM));
|
|
275
|
+
rb_define_const(mLandlock, "ACCESS_FS_REFER", ULL2NUM(LANDLOCK_ACCESS_FS_REFER));
|
|
276
|
+
rb_define_const(mLandlock, "ACCESS_FS_TRUNCATE", ULL2NUM(LANDLOCK_ACCESS_FS_TRUNCATE));
|
|
277
|
+
rb_define_const(mLandlock, "ACCESS_FS_IOCTL_DEV", ULL2NUM(LANDLOCK_ACCESS_FS_IOCTL_DEV));
|
|
278
|
+
rb_define_const(mLandlock, "ACCESS_NET_BIND_TCP", ULL2NUM(LANDLOCK_ACCESS_NET_BIND_TCP));
|
|
279
|
+
rb_define_const(mLandlock, "ACCESS_NET_CONNECT_TCP", ULL2NUM(LANDLOCK_ACCESS_NET_CONNECT_TCP));
|
|
280
|
+
}
|
data/lib/landlock.rb
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "landlock/version"
|
|
4
|
+
require_relative "landlock/landlock"
|
|
5
|
+
|
|
6
|
+
module Landlock
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
class UnsupportedError < Error; end
|
|
9
|
+
|
|
10
|
+
class SyscallError < Error
|
|
11
|
+
attr_reader :errno, :syscall
|
|
12
|
+
|
|
13
|
+
def initialize(syscall, errno, message = nil)
|
|
14
|
+
@syscall = syscall
|
|
15
|
+
@errno = errno
|
|
16
|
+
super(message || "#{syscall} failed: #{errno}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
FS_RIGHTS = {
|
|
21
|
+
execute: ACCESS_FS_EXECUTE,
|
|
22
|
+
write_file: ACCESS_FS_WRITE_FILE,
|
|
23
|
+
read_file: ACCESS_FS_READ_FILE,
|
|
24
|
+
read_dir: ACCESS_FS_READ_DIR,
|
|
25
|
+
remove_dir: ACCESS_FS_REMOVE_DIR,
|
|
26
|
+
remove_file: ACCESS_FS_REMOVE_FILE,
|
|
27
|
+
make_char: ACCESS_FS_MAKE_CHAR,
|
|
28
|
+
make_dir: ACCESS_FS_MAKE_DIR,
|
|
29
|
+
make_reg: ACCESS_FS_MAKE_REG,
|
|
30
|
+
make_sock: ACCESS_FS_MAKE_SOCK,
|
|
31
|
+
make_fifo: ACCESS_FS_MAKE_FIFO,
|
|
32
|
+
make_block: ACCESS_FS_MAKE_BLOCK,
|
|
33
|
+
make_sym: ACCESS_FS_MAKE_SYM,
|
|
34
|
+
refer: ACCESS_FS_REFER,
|
|
35
|
+
truncate: ACCESS_FS_TRUNCATE,
|
|
36
|
+
ioctl_dev: ACCESS_FS_IOCTL_DEV
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
NET_RIGHTS = {
|
|
40
|
+
bind_tcp: ACCESS_NET_BIND_TCP,
|
|
41
|
+
connect_tcp: ACCESS_NET_CONNECT_TCP
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
READ_RIGHTS = %i[read_file read_dir].freeze
|
|
45
|
+
EXEC_RIGHTS = %i[execute read_file read_dir].freeze
|
|
46
|
+
WRITE_RIGHTS = %i[
|
|
47
|
+
read_file read_dir write_file truncate remove_dir remove_file make_char
|
|
48
|
+
make_dir make_reg make_sock make_fifo make_block make_sym refer
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
def supported?
|
|
54
|
+
abi_version.positive?
|
|
55
|
+
rescue Error
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], allow_all_known: false)
|
|
60
|
+
abi = abi_version
|
|
61
|
+
raise UnsupportedError, "Linux Landlock is unavailable" unless abi.positive?
|
|
62
|
+
|
|
63
|
+
fs_handled = allow_all_known ? _fs_rights_for_abi(abi) : _handled_fs_for(read:, write:, execute:, paths:, abi:)
|
|
64
|
+
net_handled = _handled_net_for(connect_tcp:, bind_tcp:, abi:)
|
|
65
|
+
|
|
66
|
+
if fs_handled.zero? && net_handled.zero?
|
|
67
|
+
raise ArgumentError, "empty Landlock policy: provide filesystem paths or TCP ports"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
fd = _create_ruleset(fs_handled, net_handled)
|
|
71
|
+
begin
|
|
72
|
+
add_path_rules(fd, read, READ_RIGHTS, abi)
|
|
73
|
+
add_path_rules(fd, execute, EXEC_RIGHTS, abi)
|
|
74
|
+
add_path_rules(fd, write, WRITE_RIGHTS, abi)
|
|
75
|
+
|
|
76
|
+
paths.each do |rule|
|
|
77
|
+
path, rights = normalize_path_rule(rule)
|
|
78
|
+
_add_path_rule(fd, File.expand_path(path), mask(rights, FS_RIGHTS, abi))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
add_net_rules(fd, connect_tcp, [:connect_tcp], abi)
|
|
82
|
+
add_net_rules(fd, bind_tcp, [:bind_tcp], abi)
|
|
83
|
+
|
|
84
|
+
_restrict_self(fd)
|
|
85
|
+
ensure
|
|
86
|
+
_close_fd(fd) if fd && fd >= 0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def exec(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true)
|
|
93
|
+
argv = Array(argv)
|
|
94
|
+
raise ArgumentError, "argv must not be empty" if argv.empty?
|
|
95
|
+
|
|
96
|
+
pid = fork do
|
|
97
|
+
# Safe after fork: this runs only in the child process before exec.
|
|
98
|
+
Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir
|
|
99
|
+
restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:)
|
|
100
|
+
|
|
101
|
+
if env
|
|
102
|
+
exec_env = unsetenv_others ? env : ENV.to_h.merge(env)
|
|
103
|
+
Kernel.exec(exec_env, *argv, close_others: close_others)
|
|
104
|
+
else
|
|
105
|
+
Kernel.exec(*argv, close_others: close_others)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
_, status = Process.wait2(pid)
|
|
110
|
+
status
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def spawn(argv, **opts)
|
|
114
|
+
argv = Array(argv)
|
|
115
|
+
raise ArgumentError, "argv must not be empty" if argv.empty?
|
|
116
|
+
|
|
117
|
+
fork do
|
|
118
|
+
# Safe after fork: this runs only in the child process before exec.
|
|
119
|
+
Dir.chdir(opts[:chdir]) if opts[:chdir] # rubocop:disable Discourse/NoChdir
|
|
120
|
+
restrict!(
|
|
121
|
+
read: opts.fetch(:read, []),
|
|
122
|
+
write: opts.fetch(:write, []),
|
|
123
|
+
execute: opts.fetch(:execute, []),
|
|
124
|
+
connect_tcp: opts.fetch(:connect_tcp, []),
|
|
125
|
+
bind_tcp: opts.fetch(:bind_tcp, []),
|
|
126
|
+
paths: opts.fetch(:paths, [])
|
|
127
|
+
)
|
|
128
|
+
Kernel.exec(*argv, close_others: opts.fetch(:close_others, true))
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def add_path_rules(fd, paths, rights, abi)
|
|
133
|
+
Array(paths).each { |path| _add_path_rule(fd, File.expand_path(path), mask(rights, FS_RIGHTS, abi)) }
|
|
134
|
+
end
|
|
135
|
+
private_class_method :add_path_rules
|
|
136
|
+
|
|
137
|
+
def add_net_rules(fd, ports, rights, abi)
|
|
138
|
+
return if Array(ports).empty?
|
|
139
|
+
raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
|
|
140
|
+
|
|
141
|
+
Array(ports).each { |port| _add_net_rule(fd, Integer(port), mask(rights, NET_RIGHTS, abi)) }
|
|
142
|
+
end
|
|
143
|
+
private_class_method :add_net_rules
|
|
144
|
+
|
|
145
|
+
def normalize_path_rule(rule)
|
|
146
|
+
case rule
|
|
147
|
+
when Hash
|
|
148
|
+
[rule.fetch(:path), Array(rule.fetch(:rights))]
|
|
149
|
+
when Array
|
|
150
|
+
[rule.fetch(0), Array(rule.fetch(1))]
|
|
151
|
+
else
|
|
152
|
+
raise ArgumentError, "path rule must be {path:, rights:} or [path, rights]"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
private_class_method :normalize_path_rule
|
|
156
|
+
|
|
157
|
+
def mask(names, table, abi)
|
|
158
|
+
Array(names).reduce(0) do |bits, name|
|
|
159
|
+
bit = table.fetch(name.to_sym) { raise ArgumentError, "unknown Landlock right: #{name.inspect}" }
|
|
160
|
+
next bits if bit == ACCESS_FS_REFER && abi < 2
|
|
161
|
+
next bits if bit == ACCESS_FS_TRUNCATE && abi < 3
|
|
162
|
+
next bits if bit == ACCESS_FS_IOCTL_DEV && abi < 5
|
|
163
|
+
bits | bit
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :mask
|
|
167
|
+
|
|
168
|
+
def _fs_rights_for_abi(abi)
|
|
169
|
+
rights = FS_RIGHTS.values.reduce(0, :|)
|
|
170
|
+
rights &= ~ACCESS_FS_REFER if abi < 2
|
|
171
|
+
rights &= ~ACCESS_FS_TRUNCATE if abi < 3
|
|
172
|
+
rights &= ~ACCESS_FS_IOCTL_DEV if abi < 5
|
|
173
|
+
rights
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def _handled_fs_for(read:, write:, execute:, paths:, abi:)
|
|
177
|
+
bits = 0
|
|
178
|
+
bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless Array(read).empty?
|
|
179
|
+
bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless Array(execute).empty?
|
|
180
|
+
bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless Array(write).empty?
|
|
181
|
+
Array(paths).each { |rule| bits |= mask(normalize_path_rule(rule).last, FS_RIGHTS, abi) }
|
|
182
|
+
bits
|
|
183
|
+
end
|
|
184
|
+
private_class_method :_handled_fs_for
|
|
185
|
+
|
|
186
|
+
def _handled_net_for(connect_tcp:, bind_tcp:, abi:)
|
|
187
|
+
bits = 0
|
|
188
|
+
bits |= ACCESS_NET_CONNECT_TCP unless Array(connect_tcp).empty?
|
|
189
|
+
bits |= ACCESS_NET_BIND_TCP unless Array(bind_tcp).empty?
|
|
190
|
+
return 0 if bits.zero?
|
|
191
|
+
raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
|
|
192
|
+
bits
|
|
193
|
+
end
|
|
194
|
+
private_class_method :_handled_net_for
|
|
195
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: landlock
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sam Saffron
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake-compiler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.2'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop-discourse
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.9'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.9'
|
|
68
|
+
description: Native Ruby wrappers for Linux Landlock with filesystem and TCP port
|
|
69
|
+
restrictions for safe subprocess execution.
|
|
70
|
+
email:
|
|
71
|
+
- sam.saffron@gmail.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions:
|
|
74
|
+
- ext/landlock/extconf.rb
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE.txt
|
|
78
|
+
- README.md
|
|
79
|
+
- ext/landlock/extconf.rb
|
|
80
|
+
- ext/landlock/landlock.c
|
|
81
|
+
- lib/landlock.rb
|
|
82
|
+
- lib/landlock/version.rb
|
|
83
|
+
homepage: https://github.com/discourse/ruby-landlock
|
|
84
|
+
licenses:
|
|
85
|
+
- MIT
|
|
86
|
+
metadata:
|
|
87
|
+
homepage_uri: https://github.com/discourse/ruby-landlock
|
|
88
|
+
source_code_uri: https://github.com/discourse/ruby-landlock
|
|
89
|
+
rubygems_mfa_required: 'true'
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '3.3'
|
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
requirements: []
|
|
104
|
+
rubygems_version: 4.0.6
|
|
105
|
+
specification_version: 4
|
|
106
|
+
summary: Ruby bindings for Linux Landlock sandboxing
|
|
107
|
+
test_files: []
|