Module: Landlock

Defined in:
lib/landlock.rb,
lib/landlock/version.rb,
ext/landlock/landlock.c

Defined Under Namespace

Classes: Error, 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.1.1"
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

Class Method Details

._add_net_rule(ruleset_fd, port, access_bits) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'ext/landlock/landlock.c', line 238

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



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'ext/landlock/landlock.c', line 215

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



262
263
264
265
266
# File 'ext/landlock/landlock.c', line 262

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



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'ext/landlock/landlock.c', line 191

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



227
228
229
230
231
232
233
# File 'lib/landlock.rb', line 227

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



252
253
254
255
256
257
258
259
260
# File 'ext/landlock/landlock.c', line 252

static VALUE rb_ll_restrict_self(VALUE self, VALUE ruleset_fd) {
  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;
}

.abi_versionObject



182
183
184
185
186
187
188
189
# File 'ext/landlock/landlock.c', line 182

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



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/landlock.rb', line 102

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

Raises:



65
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
# File 'lib/landlock.rb', line 65

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.expand_path(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



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/landlock.rb', line 122

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

Returns:

  • (Boolean)


59
60
61
62
63
# File 'lib/landlock.rb', line 59

def supported?
  abi_version.positive?
rescue Error
  false
end