Module: Landlock
- Defined in:
- lib/landlock.rb,
lib/landlock/version.rb,
lib/landlock/safe_exec.rb,
ext/landlock/landlock.c
Defined Under Namespace
Classes: Error, SafeExec, SyscallError, UnsupportedError
Constant Summary collapse
- FS_RIGHTS =
{ execute: ACCESS_FS_EXECUTE, write_file: ACCESS_FS_WRITE_FILE, read_file: ACCESS_FS_READ_FILE, read_dir: ACCESS_FS_READ_DIR, remove_dir: ACCESS_FS_REMOVE_DIR, remove_file: ACCESS_FS_REMOVE_FILE, make_char: ACCESS_FS_MAKE_CHAR, make_dir: ACCESS_FS_MAKE_DIR, make_reg: ACCESS_FS_MAKE_REG, make_sock: ACCESS_FS_MAKE_SOCK, make_fifo: ACCESS_FS_MAKE_FIFO, make_block: ACCESS_FS_MAKE_BLOCK, make_sym: ACCESS_FS_MAKE_SYM, refer: ACCESS_FS_REFER, truncate: ACCESS_FS_TRUNCATE, ioctl_dev: ACCESS_FS_IOCTL_DEV }.freeze
- NET_RIGHTS =
{ bind_tcp: ACCESS_NET_BIND_TCP, connect_tcp: ACCESS_NET_CONNECT_TCP }.freeze
- SCOPE_FLAGS =
{ abstract_unix_socket: SCOPE_ABSTRACT_UNIX_SOCKET, signal: SCOPE_SIGNAL }.freeze
- READ_RIGHTS =
%i[read_file read_dir].freeze
- EXEC_RIGHTS =
%i[execute read_file read_dir].freeze
- WRITE_RIGHTS =
%i[ read_file read_dir write_file truncate remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym refer ].freeze
- FILE_PATH_RIGHTS =
%i[execute write_file read_file truncate ioctl_dev].freeze
- VERSION =
"0.2"- ACCESS_FS_EXECUTE =
ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE)
- ACCESS_FS_WRITE_FILE =
ULL2NUM(LANDLOCK_ACCESS_FS_WRITE_FILE)
- ACCESS_FS_READ_FILE =
ULL2NUM(LANDLOCK_ACCESS_FS_READ_FILE)
- ACCESS_FS_READ_DIR =
ULL2NUM(LANDLOCK_ACCESS_FS_READ_DIR)
- ACCESS_FS_REMOVE_DIR =
ULL2NUM(LANDLOCK_ACCESS_FS_REMOVE_DIR)
- ACCESS_FS_REMOVE_FILE =
ULL2NUM(LANDLOCK_ACCESS_FS_REMOVE_FILE)
- ACCESS_FS_MAKE_CHAR =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_CHAR)
- ACCESS_FS_MAKE_DIR =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_DIR)
- ACCESS_FS_MAKE_REG =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_REG)
- ACCESS_FS_MAKE_SOCK =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_SOCK)
- ACCESS_FS_MAKE_FIFO =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_FIFO)
- ACCESS_FS_MAKE_BLOCK =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_BLOCK)
- ACCESS_FS_MAKE_SYM =
ULL2NUM(LANDLOCK_ACCESS_FS_MAKE_SYM)
- ACCESS_FS_REFER =
ULL2NUM(LANDLOCK_ACCESS_FS_REFER)
- ACCESS_FS_TRUNCATE =
ULL2NUM(LANDLOCK_ACCESS_FS_TRUNCATE)
- ACCESS_FS_IOCTL_DEV =
ULL2NUM(LANDLOCK_ACCESS_FS_IOCTL_DEV)
- ACCESS_NET_BIND_TCP =
ULL2NUM(LANDLOCK_ACCESS_NET_BIND_TCP)
- ACCESS_NET_CONNECT_TCP =
ULL2NUM(LANDLOCK_ACCESS_NET_CONNECT_TCP)
- SCOPE_ABSTRACT_UNIX_SOCKET =
ULL2NUM(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)
- SCOPE_SIGNAL =
ULL2NUM(LANDLOCK_SCOPE_SIGNAL)
Class Method Summary collapse
- ._add_net_rule(ruleset_fd, port, access_bits) ⇒ Object
- ._add_path_rule(ruleset_fd, path, access_bits) ⇒ Object
- ._close_fd(fd_value) ⇒ Object
- ._create_ruleset(*args) ⇒ Object
- ._fs_rights_for_abi(abi) ⇒ Object
- ._restrict_self(ruleset_fd) ⇒ Object
- .abi_version ⇒ Object
- .exec(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false) ⇒ Object
- .restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], allow_all_known: false) ⇒ Object
- .spawn(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false) ⇒ Object
- .supported? ⇒ Boolean
Class Method Details
._add_net_rule(ruleset_fd, port, access_bits) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'ext/landlock/landlock.c', line 75
static VALUE rb_ll_add_net_rule(VALUE self, VALUE ruleset_fd, VALUE port, VALUE access_bits) {
unsigned long long p = NUM2ULL(port);
if (p > 65535ULL) rb_raise(rb_eArgError, "TCP port must be between 0 and 65535");
struct rb_landlock_net_port_attr rule;
memset(&rule, 0, sizeof(rule));
rule.allowed_access = NUM2ULL(access_bits);
rule.port = p;
long ret = ll_add_rule(NUM2INT(ruleset_fd), LANDLOCK_RULE_NET_PORT, &rule, 0);
if (ret < 0) raise_syscall_error("landlock_add_rule(net_port)");
return Qtrue;
}
|
._add_path_rule(ruleset_fd, path, access_bits) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'ext/landlock/landlock.c', line 52
static VALUE rb_ll_add_path_rule(VALUE self, VALUE ruleset_fd, VALUE path, VALUE access_bits) {
int ruleset = NUM2INT(ruleset_fd);
uint64_t allowed_access = NUM2ULL(access_bits);
Check_Type(path, T_STRING);
const char *cpath = StringValueCStr(path);
int parent_fd = open(cpath, O_PATH | O_CLOEXEC);
if (parent_fd < 0) raise_syscall_error("open");
struct rb_landlock_path_beneath_attr rule;
memset(&rule, 0, sizeof(rule));
rule.allowed_access = allowed_access;
rule.parent_fd = parent_fd;
long ret = ll_add_rule(ruleset, LANDLOCK_RULE_PATH_BENEATH, &rule, 0);
int saved_errno = errno;
close(parent_fd);
if (ret < 0) {
errno = saved_errno;
raise_syscall_error("landlock_add_rule(path_beneath)");
}
return Qtrue;
}
|
._close_fd(fd_value) ⇒ Object
104 105 106 107 108 |
# File 'ext/landlock/landlock.c', line 104
static VALUE rb_ll_close_fd(VALUE self, VALUE fd_value) {
int fd = NUM2INT(fd_value);
if (fd >= 0) close(fd);
return Qnil;
}
|
._create_ruleset(*args) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'ext/landlock/landlock.c', line 28
static VALUE rb_ll_create_ruleset(int argc, VALUE *argv, VALUE self) {
VALUE fs_bits, net_bits, scoped_bits;
rb_scan_args(argc, argv, "21", &fs_bits, &net_bits, &scoped_bits);
struct rb_landlock_ruleset_attr attr;
uint64_t handled_access_net = NUM2ULL(net_bits);
uint64_t scoped = NIL_P(scoped_bits) ? 0 : NUM2ULL(scoped_bits);
size_t attr_size = offsetof(struct rb_landlock_ruleset_attr, handled_access_net);
if (scoped != 0) {
attr_size = sizeof(struct rb_landlock_ruleset_attr);
} else if (handled_access_net != 0) {
attr_size = offsetof(struct rb_landlock_ruleset_attr, scoped);
}
memset(&attr, 0, sizeof(attr));
attr.handled_access_fs = NUM2ULL(fs_bits);
attr.handled_access_net = handled_access_net;
attr.scoped = scoped;
long fd = ll_create_ruleset(&attr, attr_size, 0);
if (fd < 0) raise_syscall_error("landlock_create_ruleset");
return INT2NUM(fd);
}
|
._fs_rights_for_abi(abi) ⇒ Object
228 229 230 231 232 233 234 |
# File 'lib/landlock.rb', line 228 def _fs_rights_for_abi(abi) rights = FS_RIGHTS.values.reduce(0, :|) rights &= ~ACCESS_FS_REFER if abi < 2 rights &= ~ACCESS_FS_TRUNCATE if abi < 3 rights &= ~ACCESS_FS_IOCTL_DEV if abi < 5 rights end |
._restrict_self(ruleset_fd) ⇒ Object
89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'ext/landlock/landlock.c', line 89
static VALUE rb_ll_restrict_self(VALUE self, VALUE ruleset_fd) {
#ifdef __linux__
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
raise_syscall_error("prctl(PR_SET_NO_NEW_PRIVS)");
}
long ret = ll_restrict_self(NUM2INT(ruleset_fd), 0);
if (ret < 0) raise_syscall_error("landlock_restrict_self");
return Qtrue;
#else
errno = ENOSYS;
raise_syscall_error("landlock_restrict_self");
#endif
}
|
.abi_version ⇒ Object
19 20 21 22 23 24 25 26 |
# File 'ext/landlock/landlock.c', line 19
static VALUE rb_ll_abi_version(VALUE self) {
long abi = ll_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
if (errno == ENOSYS || errno == EOPNOTSUPP) return INT2FIX(0);
raise_syscall_error("landlock_create_ruleset");
}
return LONG2NUM(abi);
}
|
.exec(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/landlock.rb', line 103 def exec(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false) argv = normalize_argv(argv) ensure_landlock_supported! pid = fork do begin # Safe after fork: this runs only in the child process before exec. Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:) Kernel.exec(*kernel_exec_args(argv, env, unsetenv_others:, close_others:)) rescue Exception => error exit_child!(error) end end _, status = Process.wait2(pid) status end |
.restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], allow_all_known: false) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/landlock.rb', line 66 def restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], allow_all_known: false) abi = abi_version raise UnsupportedError, "Linux Landlock is unavailable" unless abi.positive? fs_handled = allow_all_known ? _fs_rights_for_abi(abi) : _handled_fs_for(read:, write:, execute:, paths:, abi:) net_handled = _handled_net_for(connect_tcp:, bind_tcp:, abi:) scoped = _scope_for(scope:, abi:) if fs_handled.zero? && net_handled.zero? && scoped.zero? raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes" end fd = _create_ruleset(fs_handled, net_handled, scoped) begin add_path_rules(fd, read, READ_RIGHTS, abi) add_path_rules(fd, execute, EXEC_RIGHTS, abi) add_path_rules(fd, write, WRITE_RIGHTS, abi) paths.each do |rule| path, rights = normalize_path_rule(rule) access_mask = mask(rights, FS_RIGHTS, abi) next if access_mask.zero? _add_path_rule(fd, File.(path), access_mask) end add_net_rules(fd, connect_tcp, [:connect_tcp], abi) add_net_rules(fd, bind_tcp, [:bind_tcp], abi) _restrict_self(fd) ensure _close_fd(fd) if fd && fd >= 0 end true end |
.spawn(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/landlock.rb', line 123 def spawn(argv, read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], chdir: nil, env: nil, unsetenv_others: false, close_others: true, allow_all_known: false) argv = normalize_argv(argv) ensure_landlock_supported! fork do begin # Safe after fork: this runs only in the child process before exec. Dir.chdir(chdir) if chdir # rubocop:disable Discourse/NoChdir restrict!(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:) Kernel.exec(*kernel_exec_args(argv, env, unsetenv_others:, close_others:)) rescue Exception => error exit_child!(error) end end end |
.supported? ⇒ Boolean
60 61 62 63 64 |
# File 'lib/landlock.rb', line 60 def supported? abi_version.positive? rescue Error false end |