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 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
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Landlock
4
+ VERSION = "0.1.0"
5
+ end
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: []