Class: Sandbox::Landlock

Inherits:
LinuxBackend show all
Includes:
Utils::Output::Mixin
Defined in:
extend/os/linux/sandbox/landlock.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::Output::Mixin

#issue_reporting_message, #odebug, #odeprecated, #odie, #odisabled, #ofail, #oh1, #oh1_title, #ohai, #ohai_title, #onoe, #opoo, #opoo_outside_github_actions, #opoo_without_github_actions_annotation, #pretty_deprecated, #pretty_disabled, #pretty_duration, #pretty_install_status, #pretty_installed, #pretty_uninstalled, #pretty_unmarked, #pretty_upgradable, #pretty_warning

Methods inherited from LinuxBackend

#run, #writable_paths

Constructor Details

#initialize(profile) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

  • profile (SandboxProfile)


276
277
278
279
280
281
282
283
# File 'extend/os/linux/sandbox/landlock.rb', line 276

def initialize(profile)
  super
  @writable_paths = T.let([], T::Array[String])
  @readable_paths = T.let([], T::Array[String])
  @error_pipe_path = T.let(nil, T.nilable(String))
  @deny_all_network = T.let(false, T::Boolean)
  @deny_read = T.let(false, T::Boolean)
end

Class Method Details

.abi_versionInteger?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



114
115
116
117
# File 'extend/os/linux/sandbox/landlock.rb', line 114

def abi_version
  state
  @abi_version
end

.available?Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

  • (Boolean)


104
105
106
# File 'extend/os/linux/sandbox/landlock.rb', line 104

def available?
  state == :available
end

.close_file_descriptor(file_descriptor) ⇒ Integer

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



228
229
230
231
232
233
234
235
236
237
238
# File 'extend/os/linux/sandbox/landlock.rb', line 228

def close_file_descriptor(file_descriptor)
  @close ||= T.let(
    Fiddle::Function.new(
      Fiddle.dlopen(nil)["close"],
      [Fiddle::TYPE_INT],
      Fiddle::TYPE_INT,
    ),
    T.nilable(Fiddle::Function),
  )
  @close.call(file_descriptor)
end

.failure_reasonString?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'extend/os/linux/sandbox/landlock.rb', line 132

def failure_reason
  case state
  when :available
    nil
  when :config_disabled
    "Landlock cannot be used because Linux sandboxing is disabled."
  when :missing_fiddle
    "Landlock requires Ruby's bundled Fiddle library."
  when :unsupported
    "Landlock is not supported by this Linux kernel."
  when :disabled
    "Landlock is disabled by this Linux kernel."
  when :unsupported_abi
    abi = @abi_version
    if abi
      "Landlock ABI #{MINIMUM_ABI} or later is required; found ABI #{abi}."
    else
      "Landlock ABI #{MINIMUM_ABI} or later is required."
    end
  else
    "Landlock is not available."
  end
end

.full_write_isolation?Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Landlock cannot restrict chmod, chown, extended attributes or timestamp changes. Callers requiring full write isolation must compensate for these limitations: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#filesystem-flags

Returns:

  • (Boolean)


101
# File 'extend/os/linux/sandbox/landlock.rb', line 101

def full_write_isolation? = false

.kernel_abi_versionInteger?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

The Landlock ABI version provided by the running kernel, regardless of whether Homebrew's Linux sandbox is enabled or usable.

Returns:



122
123
124
125
126
127
128
129
# File 'extend/os/linux/sandbox/landlock.rb', line 122

def kernel_abi_version
  require "fiddle"

  version = landlock_create_ruleset(nil, 0, CREATE_RULESET_VERSION)
  version if version.positive?
rescue LoadError, Fiddle::DLError
  nil
end

.landlock_add_rule(ruleset_fd, type, attributes, flags) ⇒ Integer

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



176
177
178
179
180
181
182
183
184
185
186
# File 'extend/os/linux/sandbox/landlock.rb', line 176

def landlock_add_rule(ruleset_fd, type, attributes, flags)
  @landlock_add_rule ||= T.let(
    Fiddle::Function.new(
      Fiddle.dlopen(nil)["syscall"],
      [Fiddle::TYPE_LONG, Fiddle::TYPE_INT, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT],
      Fiddle::TYPE_LONG,
    ),
    T.nilable(Fiddle::Function),
  )
  @landlock_add_rule.call(ADD_RULE_SYSCALL, ruleset_fd, type, attributes, flags)
end

.landlock_create_ruleset(attributes, size, flags) ⇒ Integer

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



163
164
165
166
167
168
169
170
171
172
173
# File 'extend/os/linux/sandbox/landlock.rb', line 163

def landlock_create_ruleset(attributes, size, flags)
  @landlock_create_ruleset ||= T.let(
    Fiddle::Function.new(
      Fiddle.dlopen(nil)["syscall"],
      [Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_UINT],
      Fiddle::TYPE_LONG,
    ),
    T.nilable(Fiddle::Function),
  )
  @landlock_create_ruleset.call(CREATE_RULESET_SYSCALL, attributes, size, flags)
end

.landlock_restrict_self(ruleset_fd, flags) ⇒ Integer

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



189
190
191
192
193
194
195
196
197
198
199
# File 'extend/os/linux/sandbox/landlock.rb', line 189

def landlock_restrict_self(ruleset_fd, flags)
  @landlock_restrict_self ||= T.let(
    Fiddle::Function.new(
      Fiddle.dlopen(nil)["syscall"],
      [Fiddle::TYPE_LONG, Fiddle::TYPE_INT, Fiddle::TYPE_UINT],
      Fiddle::TYPE_LONG,
    ),
    T.nilable(Fiddle::Function),
  )
  @landlock_restrict_self.call(RESTRICT_SELF_SYSCALL, ruleset_fd, flags)
end

.last_errorInteger

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



241
242
243
# File 'extend/os/linux/sandbox/landlock.rb', line 241

def last_error
  Fiddle.last_error
end

.open_path(path) ⇒ Integer

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



215
216
217
218
219
220
221
222
223
224
225
# File 'extend/os/linux/sandbox/landlock.rb', line 215

def open_path(path)
  @open ||= T.let(
    Fiddle::Function.new(
      Fiddle.dlopen(nil)["open"],
      [Fiddle::TYPE_CONST_STRING, Fiddle::TYPE_INT],
      Fiddle::TYPE_INT,
    ),
    T.nilable(Fiddle::Function),
  )
  @open.call(path, O_PATH | O_CLOEXEC)
end

.reset_state!void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



157
158
159
160
# File 'extend/os/linux/sandbox/landlock.rb', line 157

def reset_state!
  @state = T.let(nil, T.nilable(Symbol))
  @abi_version = T.let(nil, T.nilable(Integer))
end

.set_no_new_privilegesInteger

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



202
203
204
205
206
207
208
209
210
211
212
# File 'extend/os/linux/sandbox/landlock.rb', line 202

def set_no_new_privileges
  @prctl ||= T.let(
    Fiddle::Function.new(
      Fiddle.dlopen(nil)["prctl"],
      [Fiddle::TYPE_INT, Fiddle::TYPE_ULONG, Fiddle::TYPE_ULONG, Fiddle::TYPE_ULONG, Fiddle::TYPE_ULONG],
      Fiddle::TYPE_INT,
    ),
    T.nilable(Fiddle::Function),
  )
  @prctl.call(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
end

.stateSymbol

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



109
110
111
# File 'extend/os/linux/sandbox/landlock.rb', line 109

def state
  @state ||= T.let(compute_state, T.nilable(Symbol))
end

Instance Method Details

#apply!void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'extend/os/linux/sandbox/landlock.rb', line 299

def apply!
  abi = self.class.abi_version
  if !abi || abi < MINIMUM_ABI
    raise self.class.failure_reason || "Landlock ABI #{MINIMUM_ABI} or later is required."
  end

  if @deny_all_network && abi < MINIMUM_FULL_NETWORK_ABI
    network_restrictions = if abi >= MINIMUM_NETWORK_ABI
      "Applying the network restrictions supported by this kernel."
    else
      "This kernel cannot restrict network access."
    end
    opoo "Landlock ABI #{MINIMUM_FULL_NETWORK_ABI} or later is required to deny all network access; " \
         "found ABI #{abi}. #{network_restrictions}"
  end

  attributes, handled_access_fs, allowed_write_access_fs = ruleset_attributes(abi)
  ruleset_fd = self.class.landlock_create_ruleset(attributes, attributes.bytesize, 0)
  raise_system_call_error("landlock_create_ruleset") if ruleset_fd.negative?

  begin
    # PTY allocation opens `/dev/ptmx` read-write, then configures its
    # dynamically allocated `/dev/pts/*` slave with device ioctls:
    # https://github.com/torvalds/linux/blob/master/drivers/tty/pty.c
    # https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#ioctl-support
    pty_access = ACCESS_FS_WRITE_FILE
    pty_access |= ACCESS_FS_IOCTL_DEV if abi >= 5
    pty_access |= ACCESS_FS_READ_FILE if @deny_read

    # `/dev/full` is Linux's standard ENOSPC test device. Opening it with
    # `fopen(..., "w")` also requires Landlock's truncate right:
    # https://github.com/torvalds/linux/blob/master/drivers/char/mem.c
    # POSIX shared memory and message queues use `/dev/shm` and
    # `/dev/mqueue`. These grants retain normal kernel permissions but do
    # not provide a private IPC namespace:
    # https://github.com/bminor/glibc/blob/master/sysdeps/posix/shm-directory.c
    # https://www.kernel.org/doc/html/latest/filesystems/mqueue.html
    device_path_rules = T.let({
      "/dev/full"   => FILE_WRITE_ACCESS_FS,
      "/dev/mqueue" => allowed_write_access_fs | (@deny_read ? ACCESS_FS_READ_FILE : 0),
      "/dev/ptmx"   => pty_access,
      "/dev/pts"    => pty_access,
      "/dev/shm"    => allowed_write_access_fs,
      "/dev/tty"    => pty_access,
    }, T::Hash[String, Integer])
    device_path_rules.each do |path, allowed_access|
      next unless File.exist?(path)

      add_path_rule(ruleset_fd, path, allowed_access & handled_access_fs)
    end

    error_pipe_path = @error_pipe_path
    if @deny_all_network && abi >= 9 && error_pipe_path
      add_path_rule(ruleset_fd, error_pipe_path, ACCESS_FS_RESOLVE_UNIX)
    end
    @readable_paths.each do |path|
      allowed_access = File.directory?(path) ? READ_ACCESS_FS : FILE_READ_ACCESS_FS
      add_path_rule(ruleset_fd, path, allowed_access)
    end
    @writable_paths.each do |path|
      allowed_access = if File.directory?(path)
        allowed_write_access_fs
      else
        allowed_write_access_fs & FILE_WRITE_ACCESS_FS
      end
      add_path_rule(ruleset_fd, path, allowed_access & handled_access_fs)
    end

    raise_system_call_error("prctl") if self.class.set_no_new_privileges.negative?
    if self.class.landlock_restrict_self(ruleset_fd, 0).negative?
      raise_system_call_error("landlock_restrict_self")
    end
  ensure
    close_file_descriptor(ruleset_fd)
  end
end

#command(args, tmpdir) ⇒ Array<String, ::Pathname>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



286
287
288
289
290
291
292
293
294
295
296
# File 'extend/os/linux/sandbox/landlock.rb', line 286

def command(args, tmpdir)
  paths = writable_paths
  @writable_paths = paths.keys | [File::NULL, tmpdir]
  @writable_paths.each { |path| prepare_writable_path(path, paths.fetch(path, :subpath)) }
  denied_read_paths = self.denied_read_paths
  @readable_paths = readable_paths(denied_read_paths)
  @deny_read = denied_read_paths.any?
  @deny_all_network = deny_all_network?
  @error_pipe_path = File.join(tmpdir, "socket")
  args
end

#readable_paths(denied_paths) ⇒ Array<String>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



403
404
405
406
407
408
409
# File 'extend/os/linux/sandbox/landlock.rb', line 403

def readable_paths(denied_paths)
  return [] if denied_paths.empty? || denied_paths.include?(root_path)

  root_path.children.sort.each_with_object([]) do |path, paths|
    add_readable_path(path, denied_paths, paths)
  end
end

#ruleset_attributes(abi) ⇒ Array<(String, Integer, Integer)>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'extend/os/linux/sandbox/landlock.rb', line 377

def ruleset_attributes(abi)
  allowed_access_fs = WRITE_ACCESS_FS
  allowed_access_fs |= ACCESS_FS_REFER if abi >= MINIMUM_ABI
  allowed_access_fs |= ACCESS_FS_TRUNCATE if abi >= MINIMUM_TRUNCATE_ABI
  handled_access_fs = allowed_access_fs
  # IOCTL_DEV is available from ABI 5 and deliberately remains absent from
  # allowed path rules, denying device ioctls opened inside the sandbox:
  # https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#ioctl-support
  handled_access_fs |= ACCESS_FS_IOCTL_DEV if abi >= 5
  handled_access_fs |= READ_ACCESS_FS if @deny_read
  handled_access_fs |= ACCESS_FS_RESOLVE_UNIX if @deny_all_network && abi >= 9

  # Optional ruleset fields are appended as `__u64` members, so only pass
  # the prefix needed for features supported by the running kernel.
  attributes = [handled_access_fs]
  if @deny_all_network && abi >= MINIMUM_NETWORK_ABI
    handled_access_net = ACCESS_NET_BIND_TCP | ACCESS_NET_CONNECT_TCP
    handled_access_net |= ACCESS_NET_BIND_UDP | ACCESS_NET_CONNECT_SEND_UDP if abi >= MINIMUM_FULL_NETWORK_ABI
    attributes << handled_access_net
    attributes << SCOPE_ABSTRACT_UNIX_SOCKET if abi >= 6
  end

  [attributes.pack("Q*"), handled_access_fs, allowed_access_fs]
end