mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Remove Bubblewrap, use Landlock for Linux sandbox
- Landlock needs no separate executable, installation or `sysctl` configuration, so use it as the only Linux sandbox implementation rather than an opt-in behind `$HOMEBREW_SANDBOX_LINUX_LANDLOCK`. - Delete `Sandbox::Bubblewrap`, the `brew setup-sandbox` command and the implicit `bubblewrap` dependency, none of which Landlock needs. - Remove the Bubblewrap-era `Sandbox` API (`ensure_sandbox_installed!`, `configure!`, `configuration_commands`, `sandbox_install_command`) and its call sites now that no backend needs installing or configuring. - Simplify `brew doctor`'s `check_linux_sandbox` to report the Landlock failure reason with the `$HOMEBREW_NO_SANDBOX_LINUX` workaround.
This commit is contained in:
@@ -298,7 +298,6 @@ jobs:
|
||||
run: brew tests ${{ matrix.test-flags }}
|
||||
env:
|
||||
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_SANDBOX_LINUX_LANDLOCK: 1
|
||||
# These cannot be queried at the macOS level on GitHub Actions.
|
||||
HOMEBREW_LANGUAGES: en-GB
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -359,7 +358,6 @@ jobs:
|
||||
- name: test-bot (macOS arm64)
|
||||
runs-on: macos-26
|
||||
env:
|
||||
HOMEBREW_SANDBOX_LINUX_LANDLOCK: 1
|
||||
HOMEBREW_TEST_BOT_ANALYTICS: 1
|
||||
steps:
|
||||
- name: Install Homebrew and Homebrew's dependencies
|
||||
|
||||
@@ -29,7 +29,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
&& retry apt-get update --error-on=any \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
acl \
|
||||
bubblewrap \
|
||||
bzip2 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
|
||||
@@ -241,13 +241,6 @@ then
|
||||
export HOMEBREW_COLOR="1"
|
||||
fi
|
||||
|
||||
# This is set by Homebrew's self-hosted runner environment.
|
||||
# shellcheck disable=SC2154
|
||||
if [[ -n "${HOMEBREW_LINUX}" && -n "${GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED}" ]]
|
||||
then
|
||||
export HOMEBREW_SANDBOX_LINUX_LANDLOCK="1"
|
||||
fi
|
||||
|
||||
setup-locale
|
||||
|
||||
#####
|
||||
|
||||
@@ -203,7 +203,6 @@ module Cask
|
||||
|
||||
sig { returns(T.nilable(Sandbox)) }
|
||||
def cask_sandbox
|
||||
Sandbox.ensure_sandbox_installed!
|
||||
return unless Sandbox.available?
|
||||
|
||||
Sandbox.new.tap do |sandbox|
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "abstract_command"
|
||||
require "shell_command"
|
||||
|
||||
module Homebrew
|
||||
module Cmd
|
||||
class SetupSandbox < AbstractCommand
|
||||
include ShellCommand
|
||||
|
||||
cmd_args do
|
||||
description <<~EOS
|
||||
Run any necessary commands to setup the Homebrew sandbox.
|
||||
Must be run with `sudo`. Currently a no-op on non-Linux.
|
||||
EOS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,67 +0,0 @@
|
||||
# Documentation defined in Library/Homebrew/cmd/setup-sandbox.rb
|
||||
|
||||
# This Bubblewrap installation mirrors the package manager approaches in
|
||||
# https://github.com/Homebrew/install and the Homebrew formula fallback in
|
||||
# `ensure_sandbox_installed!` in Library/Homebrew/extend/os/linux/sandbox.rb.
|
||||
|
||||
# `sudo` strips `GITHUB_ACTIONS`, so also detect the runner via `/proc/1/cgroup`
|
||||
# like `check-run-command-as-root` in Library/Homebrew/brew.sh does.
|
||||
homebrew-on-github-actions() {
|
||||
[[ -n "${GITHUB_ACTIONS}" ]] && return 0
|
||||
grep -q "actions_job" /proc/1/cgroup &>/dev/null
|
||||
}
|
||||
|
||||
homebrew-setup-sandbox() {
|
||||
# The sandbox sysctls and Bubblewrap are Linux-only.
|
||||
[[ -z "${HOMEBREW_LINUX}" ]] && return 0
|
||||
|
||||
if homebrew-on-github-actions && ! command -v bwrap &>/dev/null
|
||||
then
|
||||
if command -v apt-get &>/dev/null
|
||||
then
|
||||
apt-get install --yes bubblewrap
|
||||
elif command -v dnf &>/dev/null
|
||||
then
|
||||
dnf install --assumeyes bubblewrap
|
||||
elif command -v yum &>/dev/null
|
||||
then
|
||||
yum install --assumeyes bubblewrap
|
||||
elif command -v pacman &>/dev/null
|
||||
then
|
||||
pacman --sync --noconfirm bubblewrap
|
||||
elif command -v apk &>/dev/null
|
||||
then
|
||||
apk add bubblewrap
|
||||
fi
|
||||
fi
|
||||
|
||||
# These settings mirror SANDBOX_SYSCTL_SETTINGS in
|
||||
# Library/Homebrew/extend/os/linux/sandbox.rb; keep both in sync.
|
||||
local proc_sys_root="${HOMEBREW_PROC_SYS:-/proc/sys}"
|
||||
local sysctl_value
|
||||
local unprivileged_userns_clone_sysctl="${proc_sys_root}/kernel/unprivileged_userns_clone"
|
||||
if [[ -e "${unprivileged_userns_clone_sysctl}" ]] &&
|
||||
sysctl_value="$(sysctl -n "kernel.unprivileged_userns_clone")" &&
|
||||
[[ "${sysctl_value}" != "1" ]] &&
|
||||
[[ -w "${unprivileged_userns_clone_sysctl}" ]]
|
||||
then
|
||||
sysctl -w kernel.unprivileged_userns_clone=1 || true
|
||||
fi
|
||||
local max_user_namespaces_sysctl="${proc_sys_root}/user/max_user_namespaces"
|
||||
if [[ -e "${max_user_namespaces_sysctl}" ]] &&
|
||||
sysctl_value="$(sysctl -n "user.max_user_namespaces")" &&
|
||||
[[ "${sysctl_value}" -lt 28633 ]] &&
|
||||
[[ -w "${max_user_namespaces_sysctl}" ]]
|
||||
then
|
||||
sysctl -w user.max_user_namespaces=28633 || true
|
||||
fi
|
||||
|
||||
local apparmor_restrict_unprivileged_userns_sysctl="${proc_sys_root}/kernel/apparmor_restrict_unprivileged_userns"
|
||||
if [[ -e "${apparmor_restrict_unprivileged_userns_sysctl}" ]] &&
|
||||
sysctl_value="$(sysctl -n "kernel.apparmor_restrict_unprivileged_userns")" &&
|
||||
[[ "${sysctl_value}" != "0" ]] &&
|
||||
[[ -w "${apparmor_restrict_unprivileged_userns_sysctl}" ]]
|
||||
then
|
||||
sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
fi
|
||||
}
|
||||
@@ -99,15 +99,11 @@ class DependencyCollector
|
||||
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
|
||||
def glibc_dep_if_needed(related_formula_names); end
|
||||
|
||||
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
|
||||
def bubblewrap_dep_if_needed(related_formula_names); end
|
||||
|
||||
# Names implicitly added to any formula's deps right now, reusing the same checks
|
||||
# `Formula#add_global_deps_to_spec` uses to inject them onto a real formula.
|
||||
sig { returns(T::Set[String]) }
|
||||
def implicit_dependency_names
|
||||
[
|
||||
bubblewrap_dep_if_needed(Set.new),
|
||||
gcc_dep_if_needed(Set.new),
|
||||
glibc_dep_if_needed(Set.new),
|
||||
].compact.to_set(&:name)
|
||||
|
||||
@@ -82,7 +82,6 @@ module Homebrew
|
||||
|
||||
exec_args << "--HEAD" if f.head?
|
||||
|
||||
Sandbox.ensure_sandbox_installed!
|
||||
if Sandbox.available?
|
||||
sandbox = Sandbox.new
|
||||
f.logs.mkpath
|
||||
|
||||
@@ -658,7 +658,7 @@ module Homebrew
|
||||
default: true,
|
||||
},
|
||||
HOMEBREW_SANDBOX_LINUX: {
|
||||
description: "The `bwrap`(1) sandbox is the default for formula installation and testing " \
|
||||
description: "The Landlock sandbox is the default for formula installation and testing " \
|
||||
"on Linux unless `$HOMEBREW_NO_SANDBOX_LINUX` is set.",
|
||||
boolean: :set,
|
||||
disabled_by: :HOMEBREW_NO_SANDBOX_LINUX,
|
||||
|
||||
@@ -2,22 +2,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "os/linux/glibc"
|
||||
require "sandbox"
|
||||
|
||||
module OS
|
||||
module Linux
|
||||
module DependencyCollector
|
||||
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
|
||||
def bubblewrap_dep_if_needed(related_formula_names)
|
||||
return unless bubblewrap_dependency_needed?
|
||||
return if building_global_dep_tree?
|
||||
return if related_formula_names.include?(BUBBLEWRAP)
|
||||
return if global_dep_tree[BUBBLEWRAP]&.intersect?(related_formula_names)
|
||||
return unless formula_for(BUBBLEWRAP)
|
||||
|
||||
Dependency.new(BUBBLEWRAP, [:implicit])
|
||||
end
|
||||
|
||||
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
|
||||
def gcc_dep_if_needed(related_formula_names)
|
||||
# gcc is required for libgcc_s.so.1 if glibc or gcc are too old
|
||||
@@ -50,31 +38,18 @@ module OS
|
||||
|
||||
GLIBC = "glibc"
|
||||
GCC = OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA
|
||||
BUBBLEWRAP = "bubblewrap"
|
||||
private_constant :GLIBC, :GCC, :BUBBLEWRAP
|
||||
private_constant :GLIBC, :GCC
|
||||
|
||||
sig { void }
|
||||
def init_global_dep_tree_if_needed!
|
||||
return if building_global_dep_tree?
|
||||
|
||||
sandbox_tree_needed = bubblewrap_dependency_needed?
|
||||
build_formulae_tree_needed = ::DevelopmentTools.needs_build_formulae?
|
||||
return if !sandbox_tree_needed && !build_formulae_tree_needed
|
||||
return if (!sandbox_tree_needed || global_dep_tree.key?(BUBBLEWRAP)) &&
|
||||
(!build_formulae_tree_needed || (global_dep_tree.key?(GLIBC) && global_dep_tree.key?(GCC)))
|
||||
return unless ::DevelopmentTools.needs_build_formulae?
|
||||
return if global_dep_tree.key?(GLIBC) && global_dep_tree.key?(GCC)
|
||||
|
||||
building_global_dep_tree!
|
||||
if sandbox_tree_needed
|
||||
include_build = OS.not_tier_one_configuration? || build_formulae_tree_needed
|
||||
global_dep_tree[BUBBLEWRAP] = Set.new(global_deps_for(BUBBLEWRAP, include_build:))
|
||||
end
|
||||
if build_formulae_tree_needed
|
||||
global_dep_tree[GLIBC] = Set.new(global_deps_for(GLIBC))
|
||||
# gcc depends on glibc
|
||||
global_dep_tree[GCC] = Set.new([*global_deps_for(GCC), GLIBC, *@@global_dep_tree[GLIBC]])
|
||||
# bubblewrap depends on gcc
|
||||
global_dep_tree[BUBBLEWRAP]&.merge([GCC, *@@global_dep_tree[GCC]])
|
||||
end
|
||||
global_dep_tree[GLIBC] = Set.new(global_deps_for(GLIBC))
|
||||
# gcc depends on glibc
|
||||
global_dep_tree[GCC] = Set.new([*global_deps_for(GCC), GLIBC, *@@global_dep_tree[GLIBC]])
|
||||
built_global_dep_tree!
|
||||
end
|
||||
|
||||
@@ -86,27 +61,17 @@ module OS
|
||||
nil
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def bubblewrap_dependency_needed?
|
||||
return false unless ::Homebrew::EnvConfig.sandbox_linux?
|
||||
return false if ENV["HOMEBREW_TESTS"]
|
||||
return false if OS::Linux::Sandbox.landlock?
|
||||
|
||||
::Sandbox.executable.blank?
|
||||
end
|
||||
|
||||
sig { params(name: String, include_build: T::Boolean).returns(T::Array[String]) }
|
||||
def global_deps_for(name, include_build: true)
|
||||
sig { params(name: String).returns(T::Array[String]) }
|
||||
def global_deps_for(name)
|
||||
@global_deps_for ||= T.let({}, T.nilable(T::Hash[String, T::Array[String]]))
|
||||
# Always strip out glibc and gcc from all parts of dependency tree when
|
||||
# we're calculating their dependency trees. Other parts of Homebrew will
|
||||
# catch any circular dependencies.
|
||||
@global_deps_for["#{name}|#{include_build}"] ||= if (formula = formula_for(name))
|
||||
@global_deps_for[name] ||= if (formula = formula_for(name))
|
||||
formula.deps.filter_map do |dep|
|
||||
next if dep.test? && !dep.build?
|
||||
next if dep.build? && !include_build
|
||||
|
||||
[dep.name, *global_deps_for(dep.name, include_build:)].compact
|
||||
[dep.name, *global_deps_for(dep.name)].compact
|
||||
end.flatten.uniq
|
||||
else
|
||||
[]
|
||||
|
||||
@@ -17,20 +17,8 @@ module OS
|
||||
return unless Homebrew::EnvConfig.sandbox_linux?
|
||||
|
||||
require "sandbox"
|
||||
return if !::Sandbox.available? && GitHub::Actions.env_set?
|
||||
|
||||
if OS::Linux::Sandbox.landlock?
|
||||
unless ::Sandbox.available?
|
||||
return if GitHub::Actions.env_set?
|
||||
|
||||
::Sandbox.ensure_sandbox_available!
|
||||
end
|
||||
|
||||
::Sandbox.configure!
|
||||
elsif GitHub::Actions.env_set?
|
||||
::Sandbox.configure!
|
||||
else
|
||||
::Sandbox.ensure_sandbox_installed!(install_from_tests: true)
|
||||
end
|
||||
::Sandbox.ensure_sandbox_available!
|
||||
end
|
||||
|
||||
|
||||
@@ -196,64 +196,24 @@ module OS
|
||||
def check_linux_sandbox
|
||||
return unless Homebrew::EnvConfig.sandbox_linux?
|
||||
|
||||
inside_docker = OS::Linux.inside_docker?
|
||||
return if inside_docker && !GitHub::Actions.env_set?
|
||||
return if OS::Linux.inside_docker? && !GitHub::Actions.env_set?
|
||||
|
||||
state = ::Sandbox.state
|
||||
return if state == :available
|
||||
|
||||
reason = ::Sandbox.failure_reason || "The Linux sandbox is not available."
|
||||
state = :landlock if OS::Linux::Sandbox.landlock?
|
||||
reason_append = case state
|
||||
when :setuid
|
||||
"\n\nHomebrew's Linux sandbox requires a rootless `bwrap` executable."
|
||||
when :unavailable
|
||||
"\n\nHomebrew's Linux sandbox requires rootless Bubblewrap and unprivileged user namespaces."
|
||||
fix = if state == :missing_fiddle
|
||||
"Run Homebrew with its vendored Ruby, which includes Fiddle."
|
||||
else
|
||||
""
|
||||
end
|
||||
reason += reason_append
|
||||
|
||||
fix_lines = case state
|
||||
when :missing
|
||||
missing_lines = [
|
||||
reason,
|
||||
"",
|
||||
"Install Bubblewrap and ensure a rootless `bwrap` executable is available on `PATH`.",
|
||||
]
|
||||
if (install_command = ::Sandbox.sandbox_install_command)
|
||||
missing_lines.push("", "On this system, install it with:", " #{install_command}")
|
||||
end
|
||||
missing_lines
|
||||
when :setuid
|
||||
[
|
||||
"Install a non-setuid Bubblewrap or put it earlier on `PATH`.",
|
||||
]
|
||||
when :unavailable
|
||||
[
|
||||
reason,
|
||||
"",
|
||||
"Homebrew's Linux sandbox requires rootless Bubblewrap and unprivileged",
|
||||
"user namespaces. Run `sudo brew setup-sandbox` or check and update this system configuration:",
|
||||
*::Sandbox.configuration_command_messages,
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
if state == :unavailable && inside_docker && GitHub::Actions.env_set?
|
||||
fix_lines.push("",
|
||||
"If this is a GitHub Actions container, add `options: --privileged` to the job's " \
|
||||
"`container` configuration.")
|
||||
"Homebrew's Linux sandbox requires a kernel with Landlock enabled."
|
||||
end
|
||||
|
||||
::Homebrew::Diagnostic::Finding.new(
|
||||
reason,
|
||||
remediation: [
|
||||
*fix_lines,
|
||||
"",
|
||||
"As a final workaround, disable the Linux sandbox:",
|
||||
" export HOMEBREW_NO_SANDBOX_LINUX=1",
|
||||
].join("\n").to_s,
|
||||
::Sandbox.failure_reason || "The Linux sandbox is not available.",
|
||||
remediation: <<~EOS.chomp,
|
||||
#{fix}
|
||||
As a final workaround, disable the Linux sandbox:
|
||||
export HOMEBREW_NO_SANDBOX_LINUX=1
|
||||
EOS
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ module OS
|
||||
related_formula_names.merge(versioned_formulae_names)
|
||||
end
|
||||
[
|
||||
dependency_collector.bubblewrap_dep_if_needed(related_formula_names),
|
||||
dependency_collector.gcc_dep_if_needed(related_formula_names),
|
||||
dependency_collector.glibc_dep_if_needed(related_formula_names),
|
||||
].compact.freeze
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "extend/os/linux/sandbox/bubblewrap"
|
||||
require "extend/os/linux/sandbox/landlock"
|
||||
|
||||
module OS
|
||||
@@ -15,31 +14,6 @@ module OS
|
||||
TIOCSCTTY = 0x540E
|
||||
private_constant :TIOCSCTTY
|
||||
|
||||
sig { returns(::PATH) }
|
||||
def self.bubblewrap_candidate_paths
|
||||
::Sandbox::Bubblewrap.executable_candidate_paths
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(::Pathname)) }
|
||||
def self.bubblewrap_executable
|
||||
::Sandbox::Bubblewrap.executable
|
||||
end
|
||||
|
||||
sig { returns(::Pathname) }
|
||||
def self.bubblewrap_executable!
|
||||
::Sandbox::Bubblewrap.executable!
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def self.landlock?
|
||||
ENV.fetch("HOMEBREW_SANDBOX_LINUX_LANDLOCK", nil) == "1"
|
||||
end
|
||||
|
||||
sig { returns(T.any(T.class_of(::Sandbox::Bubblewrap), T.class_of(::Sandbox::Landlock))) }
|
||||
def self.sandbox_implementation
|
||||
landlock? ? ::Sandbox::Landlock : ::Sandbox::Bubblewrap
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def allow_write_temp_and_cache
|
||||
allow_write_path "/tmp"
|
||||
@@ -67,101 +41,31 @@ module OS
|
||||
|
||||
requires_ancestor { T.class_of(::Sandbox) }
|
||||
|
||||
sig { returns(String) }
|
||||
def executable_name
|
||||
::Sandbox::Bubblewrap.executable_name
|
||||
end
|
||||
|
||||
sig { params(candidate: ::Pathname).returns(T::Boolean) }
|
||||
def executable_usable?(candidate)
|
||||
::Sandbox::Bubblewrap.executable_usable?(candidate)
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def system_bubblewrap_paths
|
||||
::Sandbox::Bubblewrap.system_paths
|
||||
end
|
||||
|
||||
sig { returns(::PATH) }
|
||||
def executable_candidate_paths
|
||||
::Sandbox::Bubblewrap.executable_candidate_paths
|
||||
end
|
||||
|
||||
sig { returns(::PATH) }
|
||||
def bubblewrap_candidate_paths
|
||||
executable_candidate_paths
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(::Pathname)) }
|
||||
def bubblewrap_executable
|
||||
::Sandbox::Bubblewrap.executable
|
||||
end
|
||||
|
||||
sig { returns(::Pathname) }
|
||||
def bubblewrap_executable!
|
||||
::Sandbox::Bubblewrap.executable!
|
||||
end
|
||||
|
||||
sig { params(install_from_tests: T::Boolean).void }
|
||||
def ensure_sandbox_installed!(install_from_tests: false)
|
||||
OS::Linux::Sandbox.sandbox_implementation.ensure_installed!(install_from_tests:)
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def available?
|
||||
OS::Linux::Sandbox.sandbox_implementation.available?
|
||||
::Sandbox::Landlock.available?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def full_write_isolation?
|
||||
OS::Linux::Sandbox.sandbox_implementation.full_write_isolation?
|
||||
end
|
||||
|
||||
# Bubblewrap reports this specific namespace error when an outer
|
||||
# Bubblewrap sandbox prevents Homebrew from creating another rootless
|
||||
# sandbox. The shared `avoid_nested_sandboxing?` only calls this once the
|
||||
# `$HOMEBREW_AVOID_NESTED_SANDBOXING` opt-in is set.
|
||||
sig { returns(T::Boolean) }
|
||||
def nested_sandbox?
|
||||
OS::Linux::Sandbox.sandbox_implementation.nested_sandbox?
|
||||
::Sandbox::Landlock.full_write_isolation?
|
||||
end
|
||||
|
||||
sig { returns(Symbol) }
|
||||
def state
|
||||
OS::Linux::Sandbox.sandbox_implementation.state
|
||||
::Sandbox::Landlock.state
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def reset_state!
|
||||
::Sandbox::Bubblewrap.reset_state!
|
||||
::Sandbox::Landlock.reset_state!
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def configuration_commands
|
||||
OS::Linux::Sandbox.sandbox_implementation.configuration_commands
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def configuration_command_messages
|
||||
OS::Linux::Sandbox.sandbox_implementation.configuration_command_messages
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def configure!
|
||||
OS::Linux::Sandbox.sandbox_implementation.configure!
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def failure_reason
|
||||
return super if self != ::Sandbox
|
||||
|
||||
OS::Linux::Sandbox.sandbox_implementation.failure_reason
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def sandbox_install_command
|
||||
OS::Linux::Sandbox.sandbox_implementation.install_command
|
||||
::Sandbox::Landlock.failure_reason
|
||||
end
|
||||
|
||||
# `ioctl` request used to attach the sandboxed child to a controlling TTY.
|
||||
@@ -173,43 +77,24 @@ module OS
|
||||
|
||||
sig { params(args: T.any(String, ::Pathname)).void }
|
||||
def run(*args)
|
||||
implementation.run { super }
|
||||
end
|
||||
|
||||
sig { params(tmpdir: String).returns(T::Array[String]) }
|
||||
def bubblewrap_args(tmpdir)
|
||||
bubblewrap.arguments(tmpdir)
|
||||
end
|
||||
|
||||
sig { returns(T::Hash[String, Symbol]) }
|
||||
def writable_paths
|
||||
bubblewrap.writable_paths
|
||||
landlock.run { super }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { params(args: T::Array[T.any(String, ::Pathname)], tmpdir: String).returns(T::Array[T.any(String, ::Pathname)]) }
|
||||
def sandbox_command(args, tmpdir)
|
||||
implementation.command(args, tmpdir)
|
||||
landlock.command(args, tmpdir)
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def apply_sandbox
|
||||
sandbox = implementation
|
||||
sandbox.apply! if sandbox.is_a?(::Sandbox::Landlock)
|
||||
landlock.apply!
|
||||
end
|
||||
|
||||
sig { returns(T.any(::Sandbox::Bubblewrap, ::Sandbox::Landlock)) }
|
||||
def implementation
|
||||
@implementation ||= T.let(
|
||||
OS::Linux::Sandbox.sandbox_implementation.new(profile),
|
||||
T.nilable(T.any(::Sandbox::Bubblewrap, ::Sandbox::Landlock)),
|
||||
)
|
||||
end
|
||||
|
||||
sig { returns(::Sandbox::Bubblewrap) }
|
||||
def bubblewrap
|
||||
@bubblewrap ||= T.let(::Sandbox::Bubblewrap.new(profile), T.nilable(::Sandbox::Bubblewrap))
|
||||
sig { returns(::Sandbox::Landlock) }
|
||||
def landlock
|
||||
@landlock ||= T.let(::Sandbox::Landlock.new(profile), T.nilable(::Sandbox::Landlock))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "fileutils"
|
||||
require "env_config"
|
||||
require "system_command"
|
||||
require "utils/popen"
|
||||
require "utils/github/actions"
|
||||
require "extend/os/linux/sandbox/backend"
|
||||
|
||||
class Sandbox
|
||||
class Bubblewrap < LinuxBackend
|
||||
extend SystemCommand::Mixin
|
||||
extend Utils::Output::Mixin
|
||||
|
||||
EXECUTABLE = "bwrap"
|
||||
TEST_ARGS = [
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
"--unshare-uts",
|
||||
"--unshare-cgroup-try",
|
||||
"--ro-bind", "/", "/",
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"true"
|
||||
].freeze
|
||||
SYSTEM_PATHS = %w[
|
||||
/usr/bin
|
||||
/bin
|
||||
].freeze
|
||||
HOMEBREW_PATHS = [
|
||||
"#{HOMEBREW_PREFIX}/bin",
|
||||
].freeze
|
||||
NESTED_ERROR = "Creating new namespace failed: nesting depth or /proc/sys/user/max_*_namespaces exceeded"
|
||||
class SysctlSetting < T::Struct
|
||||
const :assignment, String
|
||||
const :description, T::Array[String]
|
||||
const :optional, T::Boolean, default: false
|
||||
end
|
||||
# These settings mirror the `sysctl` assignments in
|
||||
# Library/Homebrew/cmd/setup-sandbox.sh; keep both in sync.
|
||||
SYSCTL_SETTINGS = T.let([
|
||||
SysctlSetting.new(
|
||||
assignment: "kernel.unprivileged_userns_clone=1",
|
||||
description: [
|
||||
"Allows unprivileged processes to create user namespaces. Rootless",
|
||||
"Bubblewrap needs this to isolate builds without elevated privileges.",
|
||||
],
|
||||
),
|
||||
SysctlSetting.new(
|
||||
assignment: "user.max_user_namespaces=28633",
|
||||
description: [
|
||||
"Allows each user to allocate enough user namespaces. A zero or low",
|
||||
"limit can prevent Bubblewrap from creating its sandbox.",
|
||||
],
|
||||
),
|
||||
SysctlSetting.new(
|
||||
assignment: "kernel.apparmor_restrict_unprivileged_userns=0",
|
||||
description: [
|
||||
"Allows unprivileged user namespaces on AppArmor-enabled systems",
|
||||
"that restrict them by default. Older kernels may not provide this",
|
||||
"setting.",
|
||||
],
|
||||
optional: true,
|
||||
),
|
||||
].freeze, T::Array[SysctlSetting])
|
||||
# Per-distro Bubblewrap install commands, detected by package manager and
|
||||
# checked in priority order. Mirrors the build tools instructions in
|
||||
# `Homebrew/install`'s `install.sh`.
|
||||
INSTALL_COMMANDS = T.let({
|
||||
"apt-get" => "sudo apt-get install bubblewrap",
|
||||
"dnf" => "sudo dnf install bubblewrap",
|
||||
"yum" => "sudo yum install bubblewrap",
|
||||
"pacman" => "sudo pacman -S bubblewrap",
|
||||
"apk" => "sudo apk add bubblewrap",
|
||||
}.freeze, T::Hash[String, String])
|
||||
private_constant :EXECUTABLE, :TEST_ARGS, :SYSTEM_PATHS, :HOMEBREW_PATHS, :NESTED_ERROR, :SysctlSetting,
|
||||
:SYSCTL_SETTINGS, :INSTALL_COMMANDS
|
||||
|
||||
class << self
|
||||
sig { returns(String) }
|
||||
def executable_name
|
||||
EXECUTABLE
|
||||
end
|
||||
|
||||
sig { params(candidate: ::Pathname).returns(T::Boolean) }
|
||||
def executable_usable?(candidate)
|
||||
!File.stat(candidate).setuid?
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def system_paths
|
||||
SYSTEM_PATHS
|
||||
end
|
||||
|
||||
sig { returns(::PATH) }
|
||||
def executable_candidate_paths
|
||||
PATH.new(HOMEBREW_PATHS, system_paths, ORIGINAL_PATHS, ENV.fetch("PATH"), HOMEBREW_ORIGINAL_BREW_FILE.dirname)
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(::Pathname)) }
|
||||
def executable
|
||||
executable_candidate_paths.each do |path|
|
||||
begin
|
||||
candidate = ::Pathname.new(File.expand_path(executable_name, path))
|
||||
rescue ArgumentError
|
||||
next
|
||||
end
|
||||
|
||||
next if !candidate.file? || !candidate.executable?
|
||||
next unless executable_usable?(candidate)
|
||||
|
||||
return candidate
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
sig { returns(::Pathname) }
|
||||
def executable!
|
||||
executable || raise("Bubblewrap is required to use the Linux sandbox.")
|
||||
end
|
||||
|
||||
sig { params(install_from_tests: T::Boolean).void }
|
||||
def ensure_installed!(install_from_tests: false)
|
||||
return unless Homebrew::EnvConfig.sandbox_linux?
|
||||
return if ENV["HOMEBREW_TESTS"] && !install_from_tests
|
||||
return if ENV["HOMEBREW_INSTALLING_BUBBLEWRAP"]
|
||||
return if executable
|
||||
|
||||
begin
|
||||
require "exceptions"
|
||||
require "formula"
|
||||
with_env(HOMEBREW_INSTALLING_BUBBLEWRAP: "1") do
|
||||
::Formula["bubblewrap"].ensure_installed!(reason: "Linux sandboxing")
|
||||
end
|
||||
reset_state!
|
||||
return if executable
|
||||
rescue ::FormulaUnavailableError
|
||||
nil
|
||||
end
|
||||
|
||||
return unless GitHub::Actions.env_set?
|
||||
return unless ENV.fetch("HOMEBREW_GITHUB_HOSTED_RUNNER", nil)
|
||||
return unless which("apt-get")
|
||||
|
||||
ohai "Installing Bubblewrap..."
|
||||
command = ["apt-get", "install", "--yes", "bubblewrap"]
|
||||
command.unshift("sudo") unless Process.euid.zero?
|
||||
system(*command)
|
||||
reset_state!
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def available?
|
||||
state == :available
|
||||
end
|
||||
|
||||
# Bubblewrap reports this specific namespace error when an outer
|
||||
# Bubblewrap sandbox prevents Homebrew from creating another rootless
|
||||
# sandbox. The shared `avoid_nested_sandboxing?` only calls this once the
|
||||
# `$HOMEBREW_AVOID_NESTED_SANDBOXING` opt-in is set.
|
||||
sig { returns(T::Boolean) }
|
||||
def nested_sandbox?
|
||||
return false unless Homebrew::EnvConfig.sandbox_linux?
|
||||
|
||||
bubblewrap = executable
|
||||
return false unless bubblewrap
|
||||
|
||||
Utils.popen_read(bubblewrap.to_s, *TEST_ARGS, err: :out).include?(NESTED_ERROR)
|
||||
end
|
||||
|
||||
sig { returns(Symbol) }
|
||||
def state
|
||||
return :config_disabled unless Homebrew::EnvConfig.sandbox_linux?
|
||||
|
||||
@state ||= T.let(compute_state, T.nilable(Symbol))
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def reset_state!
|
||||
@state = T.let(nil, T.nilable(Symbol))
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def configuration_commands
|
||||
SYSCTL_SETTINGS.map do |setting|
|
||||
command = "sudo sysctl -w #{setting.assignment}"
|
||||
command += " || true" if setting.optional
|
||||
command
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def configuration_command_messages
|
||||
commands = configuration_commands
|
||||
SYSCTL_SETTINGS.each_with_index.flat_map do |setting, index|
|
||||
[
|
||||
" #{commands.fetch(index)}",
|
||||
*setting.description.map { |line| " #{line}" },
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def configure!
|
||||
unless executable
|
||||
ensure_installed!(install_from_tests: true)
|
||||
unless executable
|
||||
reset_state!
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
ohai "Configuring Bubblewrap..."
|
||||
command = [HOMEBREW_BREW_FILE.to_s, "setup-sandbox"]
|
||||
command.unshift("sudo") unless Process.euid.zero?
|
||||
raise ErrorDuringExecution.new(command, status: $CHILD_STATUS || 1) unless system(*command)
|
||||
|
||||
reset_state!
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def failure_reason
|
||||
case state
|
||||
when :config_disabled, :available
|
||||
nil
|
||||
when :missing
|
||||
"Bubblewrap is required to use the Linux sandbox but was not found."
|
||||
when :setuid
|
||||
"A rootless Bubblewrap executable is required to use the Linux sandbox, " \
|
||||
"but all found `bwrap` executables are setuid."
|
||||
when :unavailable
|
||||
"Bubblewrap is installed but cannot create a rootless sandbox."
|
||||
else
|
||||
"The Linux sandbox is not available."
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def install_command
|
||||
INSTALL_COMMANDS.find { |package_manager, _| which(package_manager) }&.last
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { returns(Symbol) }
|
||||
def compute_state
|
||||
bubblewraps = executables
|
||||
return :missing if bubblewraps.empty?
|
||||
|
||||
bubblewraps = bubblewraps.select { |candidate| executable_usable?(candidate) }
|
||||
return :setuid if bubblewraps.empty?
|
||||
|
||||
return :available if bubblewraps.any? { |candidate| sandbox_available?(candidate) }
|
||||
|
||||
:unavailable
|
||||
end
|
||||
|
||||
sig { returns(T::Array[::Pathname]) }
|
||||
def executables
|
||||
executable_candidate_paths.filter_map do |path|
|
||||
begin
|
||||
candidate = ::Pathname.new(File.expand_path(executable_name, path))
|
||||
rescue ArgumentError
|
||||
next
|
||||
end
|
||||
|
||||
candidate if candidate.file? && candidate.executable?
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(bubblewrap: ::Pathname).returns(T::Boolean) }
|
||||
def sandbox_available?(bubblewrap)
|
||||
result = system_command(
|
||||
bubblewrap,
|
||||
args: TEST_ARGS,
|
||||
print_stderr: false,
|
||||
)
|
||||
return true if result.success?
|
||||
|
||||
opoo "bubblewrap test probe failed"
|
||||
$stderr.print result.merged_output
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(profile: SandboxProfile).void }
|
||||
def initialize(profile)
|
||||
super
|
||||
@masked_read_paths = T.let([], T::Array[::Pathname])
|
||||
end
|
||||
|
||||
sig { params(block: T.proc.void).void }
|
||||
def run(&block)
|
||||
old_report_on_exception = T.let(Thread.report_on_exception, T.nilable(T::Boolean))
|
||||
Thread.report_on_exception = false
|
||||
super
|
||||
ensure
|
||||
Thread.report_on_exception = old_report_on_exception unless old_report_on_exception.nil?
|
||||
@masked_read_paths.reverse_each { |path| FileUtils.rm_rf(path) }
|
||||
@masked_read_paths.clear
|
||||
end
|
||||
|
||||
sig { params(args: T::Array[T.any(String, ::Pathname)], tmpdir: String).returns(T::Array[T.any(String, ::Pathname)]) }
|
||||
def command(args, tmpdir)
|
||||
[self.class.executable!, *arguments(tmpdir), "--", *args]
|
||||
end
|
||||
|
||||
sig { params(tmpdir: String).returns(T::Array[String]) }
|
||||
def arguments(tmpdir)
|
||||
args = T.let([
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
"--unshare-uts",
|
||||
"--unshare-cgroup-try",
|
||||
"--die-with-parent",
|
||||
"--new-session",
|
||||
"--ro-bind", "/", "/",
|
||||
"--dev", "/dev",
|
||||
"--proc", "/proc"
|
||||
], T::Array[String])
|
||||
args << "--unshare-net" if deny_all_network?
|
||||
|
||||
writable_paths.each do |path, type|
|
||||
prepare_writable_path(path, type)
|
||||
args += ["--bind", path, path]
|
||||
end
|
||||
|
||||
denied_write_paths.each do |path|
|
||||
next unless File.exist?(path)
|
||||
|
||||
args += ["--ro-bind", path, path]
|
||||
end
|
||||
|
||||
denied_read_paths.each do |path|
|
||||
next unless File.exist?(path)
|
||||
|
||||
args += if File.directory?(path)
|
||||
["--bind", masked_read_path, path]
|
||||
else
|
||||
["--ro-bind", File::NULL, path]
|
||||
end
|
||||
end
|
||||
|
||||
args += ["--bind", tmpdir, tmpdir, "--chdir", tmpdir]
|
||||
|
||||
args
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def denied_write_paths
|
||||
profile_paths(allow: false, operation: "file-write")
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def denied_read_paths
|
||||
profile_paths(allow: false, operation: "file-read")
|
||||
end
|
||||
|
||||
sig { returns(String) }
|
||||
def masked_read_path
|
||||
path = ::Pathname.new(Dir.mktmpdir("homebrew-sandbox-deny-read", HOMEBREW_TEMP))
|
||||
@masked_read_paths << path
|
||||
path.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -94,8 +94,8 @@ class Sandbox
|
||||
|
||||
class << self
|
||||
# Landlock cannot restrict chmod, chown, extended attributes or timestamp
|
||||
# changes. Callers requiring Bubblewrap-equivalent write isolation must
|
||||
# compensate for these limitations:
|
||||
# changes. Callers requiring full write isolation must compensate for
|
||||
# these limitations:
|
||||
# https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#filesystem-flags
|
||||
sig { returns(T::Boolean) }
|
||||
def full_write_isolation? = false
|
||||
@@ -159,26 +159,6 @@ class Sandbox
|
||||
@abi_version = T.let(nil, T.nilable(Integer))
|
||||
end
|
||||
|
||||
sig { params(install_from_tests: T::Boolean).void }
|
||||
def ensure_installed!(install_from_tests: false); end
|
||||
|
||||
sig { void }
|
||||
def configure!
|
||||
ensure_available!
|
||||
end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def configuration_commands = []
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def configuration_command_messages = []
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def install_command = nil
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def nested_sandbox? = false
|
||||
|
||||
sig { params(attributes: T.nilable(String), size: Integer, flags: Integer).returns(Integer) }
|
||||
def landlock_create_ruleset(attributes, size, flags)
|
||||
@landlock_create_ruleset ||= T.let(
|
||||
@@ -264,13 +244,6 @@ class Sandbox
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def ensure_available!
|
||||
return if available?
|
||||
|
||||
raise failure_reason || "Landlock is not available."
|
||||
end
|
||||
|
||||
sig { returns(Symbol) }
|
||||
def compute_state
|
||||
return :config_disabled unless Homebrew::EnvConfig.sandbox_linux?
|
||||
@@ -357,7 +330,7 @@ class Sandbox
|
||||
# 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 Bubblewrap's private IPC namespace:
|
||||
# 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({
|
||||
|
||||
@@ -22,10 +22,7 @@ module OS
|
||||
sig { returns(T::Boolean) }
|
||||
def configure_sandbox!
|
||||
require "sandbox"
|
||||
::Sandbox.configure!
|
||||
::Sandbox.available?
|
||||
rescue ::ErrorDuringExecution
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -448,20 +448,7 @@ class FormulaInstaller
|
||||
if Homebrew::EnvConfig.developer?
|
||||
# `recursive_dependencies` trims cyclic dependencies, so we do one level and take the recursive deps of that.
|
||||
# Mapping direct dependencies to deeper dependencies in a hash is also useful for the cyclic output below.
|
||||
recursive_dep_map = formula.deps.to_h do |dep|
|
||||
# We cheat a bit with bubblewrap. We eagerly add it to build dependencies on tier-one systems.
|
||||
# But this cyclic dependency check is (intentionally) overly strict and forbids cyclic build dependencies,
|
||||
# to help prevent cases that would break, for example, mass bottling.
|
||||
recursive_deps = if dep.name == "bubblewrap" && dep.implicit?
|
||||
[]
|
||||
else
|
||||
dep.to_formula.recursive_dependencies do |_dependent, recursive_dep|
|
||||
Dependable::PRUNE if recursive_dep.name == "bubblewrap" && recursive_dep.implicit?
|
||||
end
|
||||
end
|
||||
|
||||
[dep, recursive_deps]
|
||||
end
|
||||
recursive_dep_map = formula.deps.to_h { |dep| [dep, dep.to_formula.recursive_dependencies] }
|
||||
|
||||
cyclic_dependencies = []
|
||||
recursive_dep_map.each do |dep, recursive_deps|
|
||||
@@ -1066,8 +1053,6 @@ on_request: installed_on_request?, options:)
|
||||
# let's reset Utils::Git.available? if we just installed git
|
||||
Utils::Git.clear_available_cache if formula.name == "git"
|
||||
|
||||
Sandbox.reset_state! if formula.name == "bubblewrap"
|
||||
|
||||
# use installed ca-certificates when it's needed and available
|
||||
if formula.name == "ca-certificates" &&
|
||||
!DevelopmentTools.ca_file_handles_most_https_certificates?
|
||||
|
||||
@@ -199,7 +199,7 @@ class GitHubRunnerMatrix
|
||||
unless self_hosted
|
||||
container = {
|
||||
image: "ghcr.io/homebrew/brew:main",
|
||||
options: "--init --user linuxbrew --env HOMEBREW_SANDBOX_LINUX_LANDLOCK=1",
|
||||
options: "--init --user linuxbrew",
|
||||
}
|
||||
workdir = "/github/home"
|
||||
end
|
||||
|
||||
@@ -470,7 +470,7 @@ is stripped during metadata serialisation.
|
||||
- [x] PR 4.1, formula install-step sandboxing.
|
||||
Commit: `Sandbox formula install steps`.
|
||||
Scope: run structured formula steps inside the existing post-install child
|
||||
process so macOS Seatbelt and Linux Bubblewrap apply the same filesystem and
|
||||
process so macOS Seatbelt and Linux Landlock apply the same filesystem and
|
||||
network policy as legacy `post_install` hooks. This must land before any tap
|
||||
migrations use filesystem-mutating steps.
|
||||
- PR 5, default config and template writes (historical split workflow).
|
||||
|
||||
@@ -120,9 +120,6 @@ class Sandbox
|
||||
true
|
||||
end
|
||||
|
||||
sig { params(install_from_tests: T::Boolean).void }
|
||||
def self.ensure_sandbox_installed!(install_from_tests: false); end
|
||||
|
||||
sig { void }
|
||||
def self.ensure_sandbox_available!
|
||||
return if available?
|
||||
@@ -145,24 +142,8 @@ class Sandbox
|
||||
sig { void }
|
||||
def self.reset_state!; end
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def self.configuration_commands = []
|
||||
|
||||
sig { returns(T::Array[String]) }
|
||||
def self.configuration_command_messages = []
|
||||
|
||||
sig { returns(T.nilable(String)) }
|
||||
def self.sandbox_install_command = nil
|
||||
|
||||
sig { void }
|
||||
def self.configure!
|
||||
ensure_sandbox_installed!
|
||||
reset_state!
|
||||
end
|
||||
|
||||
sig { params(command: T.any(String, Pathname), writable_path: T.any(String, Pathname), deny_network: T::Boolean).void }
|
||||
def self.run_command(*command, writable_path:, deny_network: false)
|
||||
ensure_sandbox_installed!
|
||||
ensure_sandbox_available!
|
||||
|
||||
writable_path = Pathname(writable_path).expand_path
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for dynamic methods in `Homebrew::Cmd::SetupSandbox`.
|
||||
# Please instead update this file by running `bin/tapioca dsl Homebrew::Cmd::SetupSandbox`.
|
||||
|
||||
|
||||
class Homebrew::Cmd::SetupSandbox
|
||||
sig { returns(Homebrew::Cmd::SetupSandbox::Args) }
|
||||
def args; end
|
||||
end
|
||||
|
||||
class Homebrew::Cmd::SetupSandbox::Args < Homebrew::CLI::Args; end
|
||||
@@ -22,21 +22,6 @@ RSpec.describe "Bash" do
|
||||
subject(:brew) { HOMEBREW_LIBRARY_PATH.parent.parent/"bin/brew" }
|
||||
|
||||
it { is_expected.to have_valid_bash_syntax }
|
||||
|
||||
it "selects Landlock on self-hosted Linux GitHub Actions runners", :needs_linux do
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
{
|
||||
"CI" => "1",
|
||||
"GITHUB_ACTIONS" => "true",
|
||||
"GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED" => "1",
|
||||
"HOMEBREW_DEV_CMD_RUN" => "1",
|
||||
"HOMEBREW_SANDBOX_LINUX_LANDLOCK" => nil,
|
||||
},
|
||||
brew.to_s, "ruby", "--", "-e", "print OS::Linux::Sandbox.sandbox_implementation"
|
||||
)
|
||||
|
||||
expect([stdout, stderr, status.success?]).to eq(["Sandbox::Landlock", "", true])
|
||||
end
|
||||
end
|
||||
|
||||
describe "setup-locale" do
|
||||
|
||||
@@ -428,7 +428,7 @@ RSpec.describe Homebrew::Bundle::Installer do
|
||||
allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with("alpha").and_return(Set.new)
|
||||
allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with("beta").and_return(Set.new)
|
||||
allow(DependencyCollector).to receive(:new).and_return(
|
||||
instance_double(DependencyCollector, implicit_dependency_names: Set["bubblewrap"]),
|
||||
instance_double(DependencyCollector, implicit_dependency_names: Set["glibc"]),
|
||||
)
|
||||
|
||||
entries = [alpha_entry, beta_entry]
|
||||
@@ -447,7 +447,7 @@ RSpec.describe Homebrew::Bundle::Installer do
|
||||
allow(Homebrew::Bundle::Brew).to receive(:formulae_by_full_name).with(any_args).and_return({ dependencies: [] })
|
||||
allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with(any_args).and_return(Set.new)
|
||||
allow(DependencyCollector).to receive(:new).and_return(
|
||||
instance_double(DependencyCollector, implicit_dependency_names: Set["bubblewrap"]),
|
||||
instance_double(DependencyCollector, implicit_dependency_names: Set["glibc"]),
|
||||
)
|
||||
|
||||
entries = [alpha_entry, beta_entry, gamma_entry]
|
||||
@@ -469,7 +469,7 @@ RSpec.describe Homebrew::Bundle::Installer do
|
||||
allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with("alpha").and_return(Set.new)
|
||||
allow(Homebrew::Bundle::Cask).to receive(:formula_dependencies).with(["google-chrome"]).and_return([])
|
||||
allow(DependencyCollector).to receive(:new).and_return(
|
||||
instance_double(DependencyCollector, implicit_dependency_names: Set["bubblewrap"]),
|
||||
instance_double(DependencyCollector, implicit_dependency_names: Set["glibc"]),
|
||||
)
|
||||
|
||||
entries = [alpha_entry, installable_cask_entry]
|
||||
|
||||
@@ -32,7 +32,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do
|
||||
it "generates completion scripts for default shells" do
|
||||
artifact = cask.artifacts.grep(described_class).first
|
||||
|
||||
allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true)
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
allow(Sandbox).to receive(:new) do
|
||||
instance_double(Sandbox).tap do |sandbox|
|
||||
allow(sandbox).to receive(:allow_read)
|
||||
@@ -61,7 +61,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do
|
||||
calls = []
|
||||
homes = []
|
||||
|
||||
allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true)
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
allow(Sandbox).to receive(:new) do
|
||||
instance_double(Sandbox).tap do |sandbox|
|
||||
expect(sandbox).to receive(:allow_read).with(path: staged_path, type: :subpath)
|
||||
@@ -89,7 +89,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do
|
||||
it "warns and continues generating other shells" do
|
||||
artifact = cask.artifacts.grep(described_class).first
|
||||
|
||||
allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true)
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
allow(Sandbox).to receive(:new) do
|
||||
instance_double(Sandbox).tap do |sandbox|
|
||||
allow(sandbox).to receive(:allow_read)
|
||||
@@ -146,7 +146,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do
|
||||
artifact = cask.artifacts.grep(described_class).first
|
||||
captured_args = T.let([], T::Array[String])
|
||||
|
||||
allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true)
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
allow(Sandbox).to receive(:new) do
|
||||
instance_double(Sandbox).tap do |sandbox|
|
||||
allow(sandbox).to receive(:allow_read)
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "fileutils"
|
||||
require "open3"
|
||||
|
||||
require "cmd/shared_examples/args_parse"
|
||||
require "cmd/setup-sandbox"
|
||||
|
||||
RSpec.describe Homebrew::Cmd::SetupSandbox do
|
||||
let(:setup_sandbox_script) { HOMEBREW_LIBRARY_PATH/"cmd/setup-sandbox.sh" }
|
||||
let(:proc_sys_root) { mktmpdir }
|
||||
|
||||
it_behaves_like "parseable arguments"
|
||||
|
||||
def run_setup_sandbox_shell(script, env = {})
|
||||
Bundler.with_unbundled_env do
|
||||
Open3.capture3(
|
||||
{ "GITHUB_ACTIONS" => nil, "HOMEBREW_LINUX" => "1", "HOMEBREW_PROC_SYS" => proc_sys_root.to_s }
|
||||
.merge(env),
|
||||
"/bin/bash", "-c", script
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def touch_proc_sys(path)
|
||||
file = proc_sys_root/path
|
||||
FileUtils.mkdir_p(file.dirname)
|
||||
FileUtils.touch(file)
|
||||
file
|
||||
end
|
||||
|
||||
it "does nothing on non-Linux systems" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "HOMEBREW_LINUX" => nil)
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() { printf 'sysctl %s\\n' "$*"; }
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to be_empty
|
||||
end
|
||||
|
||||
it "applies the sandbox sysctl settings when they are unset" do
|
||||
touch_proc_sys "kernel/unprivileged_userns_clone"
|
||||
touch_proc_sys "user/max_user_namespaces"
|
||||
|
||||
stdout, _stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() { [[ "$1" == "-n" ]] && { echo 0; return; }; printf 'sysctl %s\\n' "$*"; }
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to eq(<<~EOS)
|
||||
sysctl -w kernel.unprivileged_userns_clone=1
|
||||
sysctl -w user.max_user_namespaces=28633
|
||||
EOS
|
||||
end
|
||||
|
||||
it "leaves already-configured sysctls unchanged" do
|
||||
touch_proc_sys "kernel/unprivileged_userns_clone"
|
||||
touch_proc_sys "user/max_user_namespaces"
|
||||
touch_proc_sys "kernel/apparmor_restrict_unprivileged_userns"
|
||||
|
||||
stdout, _stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() {
|
||||
if [[ "$1" == "-n" ]]
|
||||
then
|
||||
case "$2" in
|
||||
kernel.unprivileged_userns_clone) echo 1;;
|
||||
user.max_user_namespaces) echo 28633;;
|
||||
kernel.apparmor_restrict_unprivileged_userns) echo 0;;
|
||||
esac
|
||||
return
|
||||
fi
|
||||
printf 'sysctl %s\\n' "$*"
|
||||
}
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to be_empty
|
||||
end
|
||||
|
||||
it "skips missing sysctls and read-only sysctl writes" do
|
||||
touch_proc_sys("user/max_user_namespaces").chmod(0444)
|
||||
|
||||
stdout, stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl_log="#{proc_sys_root}/sysctl.log"
|
||||
sysctl() {
|
||||
printf '%s\\n' "$*" >> "$sysctl_log"
|
||||
if [[ "$1" == "-n" && "$2" == "user.max_user_namespaces" ]]
|
||||
then
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
printf 'unexpected sysctl %s\\n' "$*" >&2
|
||||
return 1
|
||||
}
|
||||
homebrew-setup-sandbox
|
||||
cat "$sysctl_log"
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to eq("-n user.max_user_namespaces\n")
|
||||
expect(stderr).to be_empty
|
||||
end
|
||||
|
||||
it "does not hide sysctl write errors" do
|
||||
touch_proc_sys "user/max_user_namespaces"
|
||||
|
||||
stdout, stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() {
|
||||
if [[ "$1" == "-n" ]]
|
||||
then
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
echo 'sysctl: setting key "user.max_user_namespaces", ignoring: Read-only file system' >&2
|
||||
return 1
|
||||
}
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to be_empty
|
||||
expect(stderr).to eq("sysctl: setting key \"user.max_user_namespaces\", ignoring: Read-only file system\n")
|
||||
end
|
||||
|
||||
it "installs Bubblewrap on GitHub Actions when it is missing" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "GITHUB_ACTIONS" => "true")
|
||||
source "#{setup_sandbox_script}"
|
||||
command() { case "$2" in bwrap) return 1;; apt-get) return 0;; *) return 1;; esac; }
|
||||
apt-get() { printf 'apt-get %s\\n' "$*"; }
|
||||
sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; }
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to eq("apt-get install --yes bubblewrap\n")
|
||||
end
|
||||
|
||||
it "does not install Bubblewrap when it is already present" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "GITHUB_ACTIONS" => "true")
|
||||
source "#{setup_sandbox_script}"
|
||||
command() { return 0; }
|
||||
apt-get() { printf 'apt-get %s\\n' "$*"; }
|
||||
sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; }
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).not_to include("apt-get")
|
||||
end
|
||||
|
||||
it "installs Bubblewrap when only the cgroup marks a GitHub Actions runner" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
grep() { return 0; }
|
||||
command() { case "$2" in bwrap) return 1;; apt-get) return 0;; *) return 1;; esac; }
|
||||
apt-get() { printf 'apt-get %s\\n' "$*"; }
|
||||
sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; }
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to eq("apt-get install --yes bubblewrap\n")
|
||||
end
|
||||
|
||||
it "does not install Bubblewrap outside GitHub Actions" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
grep() { return 1; }
|
||||
apt-get() { printf 'apt-get %s\\n' "$*"; }
|
||||
sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; }
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).not_to include("apt-get")
|
||||
end
|
||||
end
|
||||
@@ -15,61 +15,34 @@ RSpec.describe Homebrew::DevCmd::Tests do
|
||||
require "sandbox"
|
||||
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true)
|
||||
allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(false)
|
||||
allow(GitHub::Actions).to receive(:env_set?).and_return(false)
|
||||
end
|
||||
|
||||
it "does not require the Linux sandbox when Linux sandboxing is disabled" do
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(false)
|
||||
allow(Sandbox).to receive_messages(available?: false, failure_reason: "sandbox unavailable")
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_installed!)
|
||||
expect(Sandbox).not_to receive(:configure!)
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_available!)
|
||||
|
||||
expect { tests.check_test_environment! }.not_to raise_error
|
||||
end
|
||||
|
||||
it "does not fail on GitHub Actions when requested Landlock is unavailable" do
|
||||
allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(true)
|
||||
it "does not fail on GitHub Actions when the Linux sandbox is unavailable" do
|
||||
allow(Sandbox).to receive(:available?).and_return(false)
|
||||
allow(GitHub::Actions).to receive(:env_set?).and_return(true)
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_installed!)
|
||||
expect(Sandbox).not_to receive(:configure!)
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_available!)
|
||||
|
||||
expect { tests.check_test_environment! }.not_to raise_error
|
||||
end
|
||||
|
||||
it "fails outside GitHub Actions when requested Landlock is unavailable" do
|
||||
allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(true)
|
||||
it "fails outside GitHub Actions when the Linux sandbox is unavailable" do
|
||||
allow(Sandbox).to receive_messages(available?: false, failure_reason: "Landlock is not available.")
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_installed!)
|
||||
expect(Sandbox).not_to receive(:configure!)
|
||||
|
||||
expect { tests.check_test_environment! }
|
||||
.to raise_error(RuntimeError, "Landlock is not available.")
|
||||
end
|
||||
|
||||
it "configures requested Landlock when it is available" do
|
||||
allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(true)
|
||||
it "passes when the Linux sandbox is available" do
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_installed!)
|
||||
expect(Sandbox).to receive(:configure!)
|
||||
|
||||
expect { tests.check_test_environment! }.not_to raise_error
|
||||
end
|
||||
|
||||
it "installs and checks Bubblewrap outside GitHub Actions" do
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
expect(Sandbox).to receive(:ensure_sandbox_installed!).with(install_from_tests: true)
|
||||
expect(Sandbox).not_to receive(:configure!)
|
||||
|
||||
expect { tests.check_test_environment! }.not_to raise_error
|
||||
end
|
||||
|
||||
it "configures and checks Bubblewrap on GitHub Actions" do
|
||||
allow(GitHub::Actions).to receive(:env_set?).and_return(true)
|
||||
allow(Sandbox).to receive(:available?).and_return(true)
|
||||
expect(Sandbox).not_to receive(:ensure_sandbox_installed!)
|
||||
expect(Sandbox).to receive(:configure!)
|
||||
|
||||
expect { tests.check_test_environment! }.not_to raise_error
|
||||
end
|
||||
|
||||
@@ -900,78 +900,6 @@ RSpec.describe FormulaInstaller do
|
||||
end.to raise_error(CannotInstallFormulaError)
|
||||
end
|
||||
|
||||
it "does not raise on cyclic dependency through direct implicit Bubblewrap" do
|
||||
ENV["HOMEBREW_DEVELOPER"] = "1"
|
||||
|
||||
formula_name = "homebrew-test-formula"
|
||||
f = formula formula_name do
|
||||
T.bind(self, T.class_of(Formula))
|
||||
url "foo-1.0"
|
||||
end
|
||||
dep = Dependency.new("bubblewrap", [:implicit])
|
||||
|
||||
allow(f).to receive_messages(deps: [dep], recursive_dependencies: [])
|
||||
|
||||
fi = described_class.new(f)
|
||||
|
||||
expect do
|
||||
fi.check_install_sanity
|
||||
end.not_to raise_error
|
||||
end
|
||||
|
||||
it "does not raise on cyclic dependency through recursive implicit Bubblewrap" do
|
||||
ENV["HOMEBREW_DEVELOPER"] = "1"
|
||||
|
||||
formula_name = "homebrew-test-formula"
|
||||
f = formula formula_name do
|
||||
T.bind(self, T.class_of(Formula))
|
||||
url "foo-1.0"
|
||||
end
|
||||
dep = Dependency.new("cmake", [:build])
|
||||
implicit_bubblewrap = Dependency.new("bubblewrap", [:implicit])
|
||||
recursive_dep = Dependency.new(formula_name)
|
||||
dep_formula = instance_double(Formula)
|
||||
|
||||
allow(f).to receive_messages(deps: [dep], recursive_dependencies: [])
|
||||
allow(dep).to receive(:to_formula).and_return(dep_formula)
|
||||
allow(dep_formula).to receive(:recursive_dependencies) do |&block|
|
||||
(block&.call(dep_formula, implicit_bubblewrap) == Dependable::PRUNE) ? [] : [recursive_dep]
|
||||
end
|
||||
|
||||
fi = described_class.new(f)
|
||||
|
||||
expect do
|
||||
fi.check_install_sanity
|
||||
end.not_to raise_error
|
||||
end
|
||||
|
||||
it "raises on cyclic dependency through recursive explicit Bubblewrap" do
|
||||
ENV["HOMEBREW_DEVELOPER"] = "1"
|
||||
|
||||
formula_name = "homebrew-test-formula"
|
||||
f = formula formula_name do
|
||||
T.bind(self, T.class_of(Formula))
|
||||
url "foo-1.0"
|
||||
end
|
||||
dep = Dependency.new("cmake", [:build])
|
||||
explicit_bubblewrap = Dependency.new("bubblewrap")
|
||||
recursive_dep = Dependency.new(formula_name)
|
||||
dep_formula = instance_double(Formula)
|
||||
|
||||
allow(f).to receive_messages(deps: [dep], recursive_dependencies: [])
|
||||
allow(dep).to receive(:to_formula).and_return(dep_formula)
|
||||
allow(dep_formula).to receive(:recursive_dependencies) do |&block|
|
||||
block&.call(dep_formula, explicit_bubblewrap)
|
||||
[recursive_dep]
|
||||
end
|
||||
|
||||
fi = described_class.new(f)
|
||||
|
||||
expect do
|
||||
fi.check_install_sanity
|
||||
end.to raise_error(CannotInstallFormulaError)
|
||||
end
|
||||
|
||||
it "raises on pinned dependency" do
|
||||
dep_name = "homebrew-test-dependency"
|
||||
dep_path = CoreTap.instance.new_formula_path(dep_name)
|
||||
@@ -1552,7 +1480,7 @@ RSpec.describe FormulaInstaller do
|
||||
sandbox = instance_double(Sandbox)
|
||||
|
||||
allow(installer).to receive(:build_argv).and_return([])
|
||||
allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true, new: sandbox)
|
||||
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
|
||||
allow(sandbox).to receive_messages(record_log: nil, allow_read_if_exists: nil, allow_write_temp_and_cache: nil,
|
||||
allow_write_log: nil, allow_cvs: nil, allow_fossil: nil,
|
||||
allow_write_xcode: nil, allow_write_cellar: nil, deny_read_home: nil,
|
||||
|
||||
@@ -50,7 +50,7 @@ RSpec.describe GitHubRunnerMatrix, :no_api do
|
||||
).to be(true)
|
||||
end
|
||||
|
||||
it "uses Landlock in unprivileged Linux containers" do
|
||||
it "uses unprivileged Linux containers" do
|
||||
linux_containers = described_class.new([], ["deleted"], all_supported: false, dependent_matrix: false)
|
||||
.active_runner_specs_hash
|
||||
.filter_map { |runner| runner[:container] }
|
||||
@@ -58,7 +58,7 @@ RSpec.describe GitHubRunnerMatrix, :no_api do
|
||||
expect(linux_containers).to eq(Array.new(2) do
|
||||
{
|
||||
image: "ghcr.io/homebrew/brew:main",
|
||||
options: "--init --user linuxbrew --env HOMEBREW_SANDBOX_LINUX_LANDLOCK=1",
|
||||
options: "--init --user linuxbrew",
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "dependency_collector"
|
||||
require "sandbox"
|
||||
|
||||
RSpec.describe DependencyCollector do
|
||||
subject(:collector) { described_class.new }
|
||||
@@ -53,100 +52,13 @@ RSpec.describe DependencyCollector do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#bubblewrap_dep_if_needed" do
|
||||
let(:formulae) do
|
||||
Hash.new { |hash, name| hash[name] = instance_double(Formula, deps: []) }
|
||||
end
|
||||
|
||||
around do |example|
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil, HOMEBREW_TESTS: nil) { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true)
|
||||
allow(DevelopmentTools).to receive(:needs_build_formulae?).and_return(false)
|
||||
allow(Sandbox).to receive(:executable)
|
||||
allow(OS).to receive(:not_tier_one_configuration?).and_return(false)
|
||||
allow(Formula).to receive(:[]) { |name| formulae[name] }
|
||||
global_dep_tree.clear
|
||||
end
|
||||
|
||||
after do
|
||||
global_dep_tree.clear
|
||||
end
|
||||
|
||||
def global_dep_tree
|
||||
OS::Linux::DependencyCollector.module_eval { class_variable_get(:@@global_dep_tree) }
|
||||
end
|
||||
|
||||
it "returns a Bubblewrap implicit dependency when the Linux sandbox needs one" do
|
||||
expect(collector.bubblewrap_dep_if_needed(Set.new)).to eq(Dependency.new("bubblewrap", [:implicit]))
|
||||
end
|
||||
|
||||
it "returns nil when Bubblewrap is already available" do
|
||||
allow(Sandbox).to receive(:executable).and_return(Pathname("/usr/bin/bwrap"))
|
||||
|
||||
expect(collector.bubblewrap_dep_if_needed(Set.new)).to be_nil
|
||||
end
|
||||
|
||||
it "returns nil when using Landlock" do
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do
|
||||
expect(collector.bubblewrap_dep_if_needed(Set.new)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "returns nil for Bubblewrap and its dependencies" do
|
||||
collector.global_dep_tree["bubblewrap"] = Set["libcap"]
|
||||
|
||||
expect(collector.bubblewrap_dep_if_needed(Set["bubblewrap"])).to be_nil
|
||||
expect(collector.bubblewrap_dep_if_needed(Set["libcap"])).to be_nil
|
||||
end
|
||||
|
||||
it "returns nil when Bubblewrap is already in the dependency tree" do
|
||||
expect(collector.bubblewrap_dep_if_needed(Set["bubblewrap"])).to be_nil
|
||||
end
|
||||
|
||||
it "returns nil when a Bubblewrap runtime dependency is already in the dependency tree" do
|
||||
formulae["bubblewrap"] = instance_double(Formula, deps: [Dependency.new("libcap")])
|
||||
|
||||
expect(collector.bubblewrap_dep_if_needed(Set["libcap"])).to be_nil
|
||||
end
|
||||
|
||||
it "ignores Bubblewrap build dependencies when build formulae are not needed" do
|
||||
formulae["bubblewrap"] = instance_double(Formula, deps: [
|
||||
Dependency.new("libcap"),
|
||||
Dependency.new("pkgconf", [:build]),
|
||||
])
|
||||
|
||||
expect(collector.bubblewrap_dep_if_needed(Set["pkgconf"])).to eq(Dependency.new("bubblewrap", [:implicit]))
|
||||
end
|
||||
|
||||
it "includes Bubblewrap build dependencies when build formulae are needed" do
|
||||
allow(DevelopmentTools).to receive(:needs_build_formulae?).and_return(true)
|
||||
formulae["bubblewrap"] = instance_double(Formula, deps: [
|
||||
Dependency.new("pkgconf", [:build]),
|
||||
])
|
||||
formulae["glibc"]
|
||||
formulae[OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA]
|
||||
|
||||
expect(collector.bubblewrap_dep_if_needed(Set["pkgconf"])).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#implicit_dependency_names" do
|
||||
let(:formulae) do
|
||||
Hash.new { |hash, name| hash[name] = instance_double(Formula, deps: []) }
|
||||
end
|
||||
|
||||
around do |example|
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil, HOMEBREW_TESTS: nil) { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true)
|
||||
allow(DevelopmentTools).to receive_messages(needs_build_formulae?: false, needs_libc_formula?: false)
|
||||
allow(Sandbox).to receive(:executable).and_return(nil)
|
||||
allow(OS).to receive(:not_tier_one_configuration?).and_return(false)
|
||||
allow(Formula).to receive(:[]) { |name| formulae[name] }
|
||||
global_dep_tree.clear
|
||||
end
|
||||
@@ -159,13 +71,7 @@ RSpec.describe DependencyCollector do
|
||||
OS::Linux::DependencyCollector.module_eval { class_variable_get(:@@global_dep_tree) }
|
||||
end
|
||||
|
||||
it "includes bubblewrap when the sandbox needs it" do
|
||||
expect(collector.implicit_dependency_names).to eq(Set["bubblewrap"])
|
||||
end
|
||||
|
||||
it "is empty when bubblewrap is already available and build formulae aren't needed" do
|
||||
allow(Sandbox).to receive(:executable).and_return(Pathname("/usr/bin/bwrap"))
|
||||
|
||||
it "is empty when build formulae and a libc formula aren't needed" do
|
||||
expect(collector.implicit_dependency_names).to eq(Set.new)
|
||||
end
|
||||
|
||||
|
||||
@@ -7,10 +7,6 @@ require "sandbox"
|
||||
RSpec.describe Homebrew::Diagnostic::Checks do
|
||||
subject(:checks) { described_class.new }
|
||||
|
||||
around do |example|
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil) { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
allow(OS::Linux).to receive(:inside_docker?).and_return(false)
|
||||
end
|
||||
@@ -92,101 +88,53 @@ RSpec.describe Homebrew::Diagnostic::Checks do
|
||||
end
|
||||
end
|
||||
|
||||
specify "#check_linux_sandbox describes missing Bubblewrap" do
|
||||
allow(Sandbox).to receive_messages(
|
||||
state: :missing,
|
||||
failure_reason: "Bubblewrap is required to use the Linux sandbox but was not found.",
|
||||
sandbox_install_command: "sudo apt-get install bubblewrap",
|
||||
)
|
||||
|
||||
with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do
|
||||
message = checks.check_linux_sandbox&.to_s
|
||||
|
||||
expect(message)
|
||||
.to include(
|
||||
"Bubblewrap is required to use the Linux sandbox but was not found.",
|
||||
"Install Bubblewrap and ensure a rootless `bwrap` executable is available on `PATH`.",
|
||||
"On this system, install it with:",
|
||||
" sudo apt-get install bubblewrap",
|
||||
"export HOMEBREW_NO_SANDBOX_LINUX=1",
|
||||
)
|
||||
expect(message).not_to include("sysctl")
|
||||
expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1")
|
||||
end
|
||||
end
|
||||
|
||||
specify "#check_linux_sandbox describes setuid Bubblewrap" do
|
||||
allow(Sandbox).to receive_messages(
|
||||
state: :setuid,
|
||||
failure_reason: "All found `bwrap` executables are setuid.",
|
||||
)
|
||||
|
||||
with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do
|
||||
message = checks.check_linux_sandbox&.to_s
|
||||
|
||||
expect(message)
|
||||
.to include(
|
||||
"All found `bwrap` executables are setuid.",
|
||||
"Homebrew's Linux sandbox requires a rootless `bwrap` executable.",
|
||||
"Install a non-setuid Bubblewrap or put it earlier on `PATH`.",
|
||||
"export HOMEBREW_NO_SANDBOX_LINUX=1",
|
||||
)
|
||||
expect(message).not_to include("sysctl")
|
||||
expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1")
|
||||
end
|
||||
end
|
||||
|
||||
specify "#check_linux_sandbox describes Bubblewrap configuration" do
|
||||
allow(Sandbox).to receive_messages(
|
||||
state: :unavailable,
|
||||
failure_reason: "Bubblewrap is installed but cannot create a rootless sandbox.",
|
||||
)
|
||||
|
||||
with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do
|
||||
message = checks.check_linux_sandbox&.to_s
|
||||
|
||||
expect(message)
|
||||
.to include(
|
||||
"Bubblewrap is installed but cannot create a rootless sandbox.",
|
||||
"Homebrew's Linux sandbox requires rootless Bubblewrap and unprivileged",
|
||||
"sudo sysctl -w kernel.unprivileged_userns_clone=1",
|
||||
"Allows unprivileged processes to create user namespaces.",
|
||||
"sudo sysctl -w user.max_user_namespaces=28633",
|
||||
"Allows each user to allocate enough user namespaces.",
|
||||
"sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true",
|
||||
"Allows unprivileged user namespaces on AppArmor-enabled systems",
|
||||
"export HOMEBREW_NO_SANDBOX_LINUX=1",
|
||||
)
|
||||
expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1")
|
||||
end
|
||||
end
|
||||
|
||||
specify "#check_linux_sandbox describes unavailable Landlock" do
|
||||
allow(OS::Linux).to receive(:inside_docker?).and_return(true)
|
||||
specify "#check_linux_sandbox describes unsupported Landlock" do
|
||||
allow(Sandbox).to receive_messages(
|
||||
state: :unsupported,
|
||||
failure_reason: "Landlock is not supported by this Linux kernel.",
|
||||
)
|
||||
|
||||
with_env(GITHUB_ACTIONS: "true", HOMEBREW_NO_SANDBOX_LINUX: nil, HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do
|
||||
with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do
|
||||
message = checks.check_linux_sandbox&.to_s
|
||||
|
||||
expect(message).to include("Landlock is not supported by this Linux kernel.")
|
||||
expect(message).not_to include("Bubblewrap", "--privileged")
|
||||
expect(message)
|
||||
.to include(
|
||||
"Landlock is not supported by this Linux kernel.",
|
||||
"Homebrew's Linux sandbox requires a kernel with Landlock enabled.",
|
||||
"export HOMEBREW_NO_SANDBOX_LINUX=1",
|
||||
)
|
||||
expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1")
|
||||
end
|
||||
end
|
||||
|
||||
specify "#check_linux_sandbox suggests privileged GitHub Actions containers" do
|
||||
specify "#check_linux_sandbox describes missing Fiddle" do
|
||||
allow(Sandbox).to receive_messages(
|
||||
state: :missing_fiddle,
|
||||
failure_reason: "Landlock requires Ruby's bundled Fiddle library.",
|
||||
)
|
||||
|
||||
with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do
|
||||
message = checks.check_linux_sandbox&.to_s
|
||||
|
||||
expect(message)
|
||||
.to include(
|
||||
"Landlock requires Ruby's bundled Fiddle library.",
|
||||
"Run Homebrew with its vendored Ruby, which includes Fiddle.",
|
||||
"export HOMEBREW_NO_SANDBOX_LINUX=1",
|
||||
)
|
||||
expect(message).not_to include("kernel with Landlock")
|
||||
end
|
||||
end
|
||||
|
||||
specify "#check_linux_sandbox describes unavailable Landlock inside Docker on GitHub Actions" do
|
||||
allow(OS::Linux).to receive(:inside_docker?).and_return(true)
|
||||
allow(Sandbox).to receive_messages(
|
||||
state: :unavailable,
|
||||
failure_reason: "Bubblewrap is installed but cannot create a rootless sandbox.",
|
||||
state: :disabled,
|
||||
failure_reason: "Landlock is disabled by this Linux kernel.",
|
||||
)
|
||||
|
||||
with_env(GITHUB_ACTIONS: "true", HOMEBREW_NO_SANDBOX_LINUX: nil) do
|
||||
expect(checks.check_linux_sandbox&.to_s).to include(
|
||||
"If this is a GitHub Actions container, add `options: --privileged` to the job's `container` configuration.",
|
||||
)
|
||||
expect(checks.check_linux_sandbox&.to_s).to include("Landlock is disabled by this Linux kernel.")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -77,13 +77,6 @@ RSpec.describe Sandbox::Landlock do
|
||||
expect(described_class.abi_version).to eq(1)
|
||||
expect(described_class.failure_reason).to eq("Landlock ABI 2 or later is required; found ABI 1.")
|
||||
end
|
||||
|
||||
it "only raises when explicitly configuring unavailable Landlock" do
|
||||
allow(described_class).to receive_messages(available?: false, failure_reason: "Landlock is not available.")
|
||||
|
||||
expect { described_class.ensure_installed! }.not_to raise_error
|
||||
expect { described_class.configure! }.to raise_error(RuntimeError, "Landlock is not available.")
|
||||
end
|
||||
end
|
||||
|
||||
describe "::kernel_abi_version" do
|
||||
|
||||
@@ -7,455 +7,7 @@ require "extend/os/linux/sandbox" if OS.linux?
|
||||
RSpec.describe Sandbox, :needs_linux do
|
||||
subject(:sandbox) { described_class.new }
|
||||
|
||||
describe "::sandbox_implementation" do
|
||||
it "uses Bubblewrap by default" do
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil) do
|
||||
expect(OS::Linux::Sandbox.sandbox_implementation).to eq(Sandbox::Bubblewrap)
|
||||
end
|
||||
end
|
||||
|
||||
it "uses Landlock when requested" do
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do
|
||||
expect(OS::Linux::Sandbox.sandbox_implementation).to eq(Sandbox::Landlock)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "::bubblewrap_executable" do
|
||||
let(:sandbox_class) do
|
||||
Class.new(Sandbox::Bubblewrap) do
|
||||
class << self
|
||||
attr_accessor :test_executable_candidate_paths
|
||||
|
||||
def executable_candidate_paths = test_executable_candidate_paths
|
||||
end
|
||||
end
|
||||
end
|
||||
let(:setuid_dir) { mktmpdir }
|
||||
let(:usable_dir) { mktmpdir }
|
||||
let(:setuid_bubblewrap) { setuid_dir/"bwrap" }
|
||||
let(:usable_bubblewrap) { usable_dir/"bwrap" }
|
||||
|
||||
before do
|
||||
FileUtils.touch setuid_bubblewrap
|
||||
FileUtils.chmod "+x", setuid_bubblewrap
|
||||
FileUtils.touch usable_bubblewrap
|
||||
FileUtils.chmod "+x", usable_bubblewrap
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(setuid_dir, usable_dir)
|
||||
allow(File).to receive(:stat).and_call_original
|
||||
allow(File).to receive(:stat).with(setuid_bubblewrap).and_return(instance_double(File::Stat, setuid?: true))
|
||||
end
|
||||
|
||||
it "searches Homebrew Bubblewrap before system Bubblewrap and skips setuid candidates" do
|
||||
expect(Sandbox::Bubblewrap.executable_candidate_paths.to_a).to start_with("#{HOMEBREW_PREFIX}/bin", "/usr/bin",
|
||||
"/bin")
|
||||
expect(sandbox_class.executable).to eq(usable_bubblewrap)
|
||||
end
|
||||
|
||||
it "raises when no suitable bubblewrap candidate exists" do
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(mktmpdir)
|
||||
|
||||
expect { sandbox_class.executable! }
|
||||
.to raise_error(RuntimeError, "Bubblewrap is required to use the Linux sandbox.")
|
||||
end
|
||||
end
|
||||
|
||||
describe "::available?" do
|
||||
let(:sandbox_class) do
|
||||
Class.new(Sandbox::Bubblewrap) do
|
||||
class << self
|
||||
attr_accessor :test_executable_candidate_paths
|
||||
|
||||
def executable_candidate_paths = test_executable_candidate_paths
|
||||
end
|
||||
end
|
||||
end
|
||||
let(:bubblewrap_dir) { mktmpdir }
|
||||
let(:bubblewrap) { bubblewrap_dir/"bwrap" }
|
||||
let(:fallback_bubblewrap_dir) { mktmpdir }
|
||||
let(:fallback_bubblewrap) { fallback_bubblewrap_dir/"bwrap" }
|
||||
let(:successful_result) { instance_double(SystemCommand::Result, success?: true) }
|
||||
let(:failed_result) { instance_double(SystemCommand::Result, success?: false, merged_output: "") }
|
||||
let(:bubblewrap_probe_args) do
|
||||
[
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
"--unshare-uts",
|
||||
"--unshare-cgroup-try",
|
||||
"--ro-bind", "/", "/",
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"true"
|
||||
]
|
||||
end
|
||||
let(:bubblewrap_test_args) do
|
||||
[
|
||||
bubblewrap.to_s,
|
||||
*bubblewrap_probe_args,
|
||||
{ err: :out },
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true)
|
||||
FileUtils.touch bubblewrap
|
||||
FileUtils.chmod "+x", bubblewrap
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir)
|
||||
end
|
||||
|
||||
it "returns false when Linux sandboxing is disabled" do
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(false)
|
||||
|
||||
expect(sandbox_class.available?).to be(false)
|
||||
expect(sandbox_class.state).to eq(:config_disabled)
|
||||
end
|
||||
|
||||
it "returns false when bubblewrap is unavailable" do
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(mktmpdir)
|
||||
|
||||
expect(sandbox_class.available?).to be(false)
|
||||
expect(sandbox_class.state).to eq(:missing)
|
||||
end
|
||||
|
||||
it "probes unprivileged namespace support once" do
|
||||
expect(sandbox_class).to receive(:system_command).once.with(
|
||||
bubblewrap,
|
||||
args: bubblewrap_probe_args,
|
||||
print_stderr: false,
|
||||
).and_return(successful_result)
|
||||
|
||||
expect(sandbox_class.available?).to be(true)
|
||||
expect(sandbox_class.state).to eq(:available)
|
||||
expect(sandbox_class.failure_reason).to be_nil
|
||||
end
|
||||
|
||||
it "probes later usable Bubblewrap candidates if earlier candidates fail" do
|
||||
FileUtils.touch fallback_bubblewrap
|
||||
FileUtils.chmod "+x", fallback_bubblewrap
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir, fallback_bubblewrap_dir)
|
||||
|
||||
expect(sandbox_class).to receive(:system_command).with(
|
||||
bubblewrap,
|
||||
args: bubblewrap_probe_args,
|
||||
print_stderr: false,
|
||||
).and_return(failed_result)
|
||||
expect(sandbox_class).to receive(:system_command).with(
|
||||
fallback_bubblewrap,
|
||||
args: bubblewrap_probe_args,
|
||||
print_stderr: false,
|
||||
).and_return(successful_result)
|
||||
|
||||
expect(sandbox_class.available?).to be(true)
|
||||
end
|
||||
|
||||
it "reports setuid bubblewrap candidates" do
|
||||
allow(File).to receive(:stat).and_call_original
|
||||
allow(File).to receive(:stat).with(bubblewrap).and_return(instance_double(File::Stat, setuid?: true))
|
||||
|
||||
expect(sandbox_class.available?).to be(false)
|
||||
expect(sandbox_class.state).to eq(:setuid)
|
||||
expect(sandbox_class.failure_reason).to include("setuid")
|
||||
end
|
||||
|
||||
it "reports bubblewrap sandbox probe failures" do
|
||||
allow(sandbox_class).to receive(:system_command).and_return(failed_result)
|
||||
|
||||
expect(sandbox_class.available?).to be(false)
|
||||
expect(sandbox_class.state).to eq(:unavailable)
|
||||
expect(sandbox_class.failure_reason).to include("cannot create a rootless sandbox")
|
||||
end
|
||||
|
||||
it "prints bubblewrap sandbox probe failure output" do
|
||||
expect(sandbox_class).to receive(:system_command)
|
||||
.and_return(instance_double(SystemCommand::Result, success?: false,
|
||||
merged_output: "bwrap stdout\nbwrap stderr\n"))
|
||||
expect(sandbox_class).to receive(:opoo).with("bubblewrap test probe failed")
|
||||
|
||||
expect { sandbox_class.available? }
|
||||
.to output("bwrap stdout\nbwrap stderr\n").to_stderr
|
||||
end
|
||||
|
||||
it "does not treat generic bubblewrap sandbox probe failures as nested" do
|
||||
FileUtils.touch fallback_bubblewrap
|
||||
FileUtils.chmod "+x", fallback_bubblewrap
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir, fallback_bubblewrap_dir)
|
||||
|
||||
expect(Utils).to receive(:popen_read)
|
||||
.with(*bubblewrap_test_args)
|
||||
.and_return("bwrap: No permissions to create a new namespace\n")
|
||||
|
||||
expect(sandbox_class.nested_sandbox?).to be(false)
|
||||
end
|
||||
|
||||
it "treats a bubblewrap namespace nesting failure as nested" do
|
||||
expect(Utils).to receive(:popen_read)
|
||||
.with(*bubblewrap_test_args)
|
||||
.and_return("bwrap: Creating new namespace failed: " \
|
||||
"nesting depth or /proc/sys/user/max_*_namespaces exceeded (ENOSPC)\n")
|
||||
|
||||
expect(sandbox_class.nested_sandbox?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "::configuration_commands" do
|
||||
let(:sandbox_class) { Class.new(Sandbox::Bubblewrap) }
|
||||
|
||||
around do |example|
|
||||
with_env(GITHUB_ACTIONS: nil, HOMEBREW_GITHUB_HOSTED_RUNNER: nil) { example.run }
|
||||
end
|
||||
|
||||
it "lists Linux sandbox sysctl commands" do
|
||||
expect(sandbox_class.configuration_commands).to eq([
|
||||
"sudo sysctl -w kernel.unprivileged_userns_clone=1",
|
||||
"sudo sysctl -w user.max_user_namespaces=28633",
|
||||
"sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true",
|
||||
])
|
||||
end
|
||||
|
||||
it "uses system Bubblewrap when configuring Linux sandbox sysctls" do
|
||||
allow(sandbox_class).to receive(:executable).and_return(Pathname("/usr/bin/bwrap"))
|
||||
allow(Process).to receive(:euid).and_return(1000)
|
||||
expect(sandbox_class).not_to receive(:ensure_installed!)
|
||||
expect(sandbox_class).to receive(:ohai).with("Configuring Bubblewrap...").ordered
|
||||
expect(sandbox_class).to receive(:system)
|
||||
.with("sudo", HOMEBREW_BREW_FILE.to_s, "setup-sandbox").and_return(true).ordered
|
||||
|
||||
sandbox_class.configure!
|
||||
end
|
||||
|
||||
it "does not configure Linux sandbox sysctls when Bubblewrap remains unavailable" do
|
||||
expect(sandbox_class).to receive(:executable).twice.and_return(nil)
|
||||
expect(sandbox_class).to receive(:ensure_installed!)
|
||||
.with(install_from_tests: true)
|
||||
expect(sandbox_class).not_to receive(:system)
|
||||
|
||||
sandbox_class.configure!
|
||||
end
|
||||
|
||||
it "installs Bubblewrap and configures Linux sandbox sysctls as root" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.twice
|
||||
.and_return(nil, Pathname(HOMEBREW_PREFIX/"bin/bwrap"))
|
||||
allow(Process).to receive(:euid).and_return(0)
|
||||
expect(sandbox_class).to receive(:ensure_installed!)
|
||||
.with(install_from_tests: true)
|
||||
expect(sandbox_class).to receive(:ohai).with("Configuring Bubblewrap...").ordered
|
||||
expect(sandbox_class).to receive(:system)
|
||||
.with(HOMEBREW_BREW_FILE.to_s, "setup-sandbox").and_return(true).ordered
|
||||
|
||||
sandbox_class.configure!
|
||||
end
|
||||
|
||||
it "raises when configuring Linux sandbox sysctls fails" do
|
||||
allow(sandbox_class).to receive(:executable).and_return(Pathname("/usr/bin/bwrap"))
|
||||
allow(Process).to receive(:euid).and_return(0)
|
||||
allow(sandbox_class).to receive(:ohai)
|
||||
expect(sandbox_class).to receive(:system)
|
||||
.with(HOMEBREW_BREW_FILE.to_s, "setup-sandbox").and_return(false)
|
||||
|
||||
expect { sandbox_class.configure! }.to raise_error(ErrorDuringExecution)
|
||||
end
|
||||
end
|
||||
|
||||
describe "::sandbox_install_command" do
|
||||
let(:sandbox_class) { Class.new(Sandbox::Bubblewrap) }
|
||||
|
||||
it "returns the distro-specific install command for the detected package manager" do
|
||||
allow(sandbox_class).to receive(:which).with("apt-get").and_return(nil)
|
||||
allow(sandbox_class).to receive(:which).with("dnf").and_return(Pathname("/usr/bin/dnf"))
|
||||
expect(sandbox_class.install_command).to eq("sudo dnf install bubblewrap")
|
||||
end
|
||||
|
||||
it "returns nil when no known package manager is found" do
|
||||
allow(sandbox_class).to receive(:which).and_return(nil)
|
||||
expect(sandbox_class.install_command).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "::ensure_sandbox_installed!" do
|
||||
let(:sandbox_class) { Class.new(Sandbox::Bubblewrap) }
|
||||
|
||||
around do |example|
|
||||
with_env(GITHUB_ACTIONS: nil, HOMEBREW_GITHUB_HOSTED_RUNNER: nil,
|
||||
HOMEBREW_INSTALLING_BUBBLEWRAP: nil, HOMEBREW_TESTS: nil) { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true)
|
||||
end
|
||||
|
||||
it "does nothing when Homebrew Bubblewrap is already available" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.once
|
||||
.and_return(Pathname(HOMEBREW_PREFIX/"bin/bwrap"))
|
||||
expect(Formula).not_to receive(:[])
|
||||
expect(sandbox_class).not_to receive(:which)
|
||||
expect(sandbox_class).not_to receive(:system)
|
||||
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
|
||||
it "does nothing when system Bubblewrap is already available" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.once
|
||||
.and_return(Pathname("/usr/bin/bwrap"))
|
||||
expect(Formula).not_to receive(:[])
|
||||
expect(sandbox_class).not_to receive(:which)
|
||||
expect(sandbox_class).not_to receive(:system)
|
||||
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
|
||||
it "installs Bubblewrap with Homebrew before trying apt-get on GitHub Actions" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.twice
|
||||
.and_return(nil, Pathname(HOMEBREW_PREFIX/"bin/bwrap"))
|
||||
expect(Formula).to receive(:[]).with("bubblewrap")
|
||||
.and_return(instance_double(Formula, ensure_installed!: nil))
|
||||
expect(sandbox_class).not_to receive(:which)
|
||||
expect(sandbox_class).not_to receive(:system)
|
||||
|
||||
with_env(GITHUB_ACTIONS: "true", HOMEBREW_GITHUB_HOSTED_RUNNER: "1") do
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
end
|
||||
|
||||
it "falls back to sudo apt-get on GitHub Actions Ubuntu when Homebrew Bubblewrap is unavailable" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.twice
|
||||
.and_return(nil)
|
||||
expect(Formula).to receive(:[]).with("bubblewrap")
|
||||
.and_return(instance_double(Formula, ensure_installed!: nil))
|
||||
expect(sandbox_class).to receive(:which).with("apt-get").and_return(Pathname("/usr/bin/apt-get"))
|
||||
expect(Process).to receive(:euid).and_return(1000)
|
||||
expect(sandbox_class).to receive(:ohai).with("Installing Bubblewrap...")
|
||||
expect(sandbox_class).to receive(:system)
|
||||
.with("sudo", "apt-get", "install", "--yes", "bubblewrap")
|
||||
.and_return(true)
|
||||
|
||||
with_env(GITHUB_ACTIONS: "true", HOMEBREW_GITHUB_HOSTED_RUNNER: "1") do
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
end
|
||||
|
||||
it "falls back to apt-get as root on GitHub Actions Ubuntu when Homebrew Bubblewrap is unavailable" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.twice
|
||||
.and_return(nil)
|
||||
expect(Formula).to receive(:[]).with("bubblewrap")
|
||||
.and_return(instance_double(Formula, ensure_installed!: nil))
|
||||
expect(sandbox_class).to receive(:which).with("apt-get").and_return(Pathname("/usr/bin/apt-get"))
|
||||
expect(Process).to receive(:euid).and_return(0)
|
||||
expect(sandbox_class).to receive(:ohai).with("Installing Bubblewrap...")
|
||||
expect(sandbox_class).to receive(:system)
|
||||
.with("apt-get", "install", "--yes", "bubblewrap")
|
||||
.and_return(true)
|
||||
|
||||
with_env(GITHUB_ACTIONS: "true", HOMEBREW_GITHUB_HOSTED_RUNNER: "1") do
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
end
|
||||
|
||||
it "does not fall back to apt-get outside GitHub Actions Ubuntu" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.twice
|
||||
.and_return(nil, nil)
|
||||
expect(Formula).to receive(:[]).with("bubblewrap")
|
||||
.and_return(instance_double(Formula, ensure_installed!: nil))
|
||||
expect(sandbox_class).not_to receive(:which)
|
||||
expect(sandbox_class).not_to receive(:system)
|
||||
|
||||
with_env(GITHUB_ACTIONS: "true") do
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
end
|
||||
|
||||
it "does not fall back to apt-get outside GitHub Actions" do
|
||||
expect(sandbox_class).to receive(:executable)
|
||||
.twice
|
||||
.and_return(nil, nil)
|
||||
expect(Formula).to receive(:[]).with("bubblewrap")
|
||||
.and_return(instance_double(Formula, ensure_installed!: nil))
|
||||
expect(sandbox_class).not_to receive(:which)
|
||||
expect(sandbox_class).not_to receive(:system)
|
||||
|
||||
sandbox_class.ensure_installed!
|
||||
end
|
||||
end
|
||||
|
||||
describe "#bubblewrap_args" do
|
||||
let(:dir) { mktmpdir }
|
||||
let(:denied_dir) { mktmpdir }
|
||||
let(:tmpdir) { mktmpdir }
|
||||
let(:args) { sandbox.bubblewrap_args(tmpdir.to_s) }
|
||||
|
||||
it "maps allowed and denied writes to bind mounts" do
|
||||
sandbox.allow_write_path dir
|
||||
sandbox.deny_write_path denied_dir
|
||||
sandbox.deny_all_network
|
||||
|
||||
expect(args).to include("--unshare-user", "--unshare-ipc", "--unshare-pid", "--unshare-net", "--new-session")
|
||||
expect(args.each_cons(3)).to include(["--bind", dir.to_s, dir.to_s])
|
||||
expect(args.each_cons(3)).to include(["--ro-bind", denied_dir.to_s, denied_dir.to_s])
|
||||
end
|
||||
|
||||
it "runs from the sandbox tmpdir" do
|
||||
expect(args.each_cons(3)).to include(["--bind", tmpdir.to_s, tmpdir.to_s])
|
||||
expect(args.each_cons(2)).to include(["--chdir", tmpdir.to_s])
|
||||
end
|
||||
|
||||
it "exposes the host filesystem read-only" do
|
||||
expect(args.each_cons(3)).to include(["--ro-bind", "/", "/"])
|
||||
expect(args.index("--ro-bind")).to be < args.index("--dev")
|
||||
end
|
||||
|
||||
it "masks denied read directories" do
|
||||
sandbox.deny_read_path dir
|
||||
|
||||
bind = args.each_cons(3).find { |arg| arg.fetch(0) == "--bind" && arg.fetch(2) == dir.to_s }
|
||||
expect(bind).not_to be_nil
|
||||
expect(Pathname(bind.fetch(1)).children).to be_empty
|
||||
end
|
||||
|
||||
it "overlays Linux runtime filesystems" do
|
||||
expect(args.each_cons(2)).to include(["--dev", "/dev"], ["--proc", "/proc"])
|
||||
end
|
||||
|
||||
it "does not need explicit mounts for allowed reads" do
|
||||
file = mktmpdir/"foo.rb"
|
||||
FileUtils.touch file
|
||||
sandbox.allow_read path: file
|
||||
|
||||
expect(args.each_cons(3)).to include(["--ro-bind", "/", "/"])
|
||||
expect(args.each_cons(3)).not_to include(["--ro-bind", file.to_s, file.to_s])
|
||||
end
|
||||
|
||||
it "uses Linux temp paths instead of macOS temp paths" do
|
||||
sandbox.allow_write_temp_and_cache
|
||||
|
||||
expect(args).to include("/tmp", "/var/tmp", HOMEBREW_TEMP.to_s, HOMEBREW_CACHE.to_s)
|
||||
expect(args).not_to include("/private/tmp", "/private/var/tmp")
|
||||
end
|
||||
|
||||
it "does not add Xcode write paths" do
|
||||
sandbox.allow_write_xcode
|
||||
|
||||
expect(sandbox.writable_paths).to be_empty
|
||||
end
|
||||
|
||||
it "rejects regex path filters" do
|
||||
sandbox.allow_write path: "^/tmp/homebrew-[^/]+$", type: :regex
|
||||
|
||||
expect { args }.to raise_error(ArgumentError, /Linux sandbox does not support regex path filters/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run with Bubblewrap" do
|
||||
around do |example|
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil) { example.run }
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
before do
|
||||
skip "Sandbox not available." unless described_class.available?
|
||||
end
|
||||
@@ -509,15 +61,10 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
expect { sandbox.run "/bin/sh", "-c", 'exec "$1"', "brew-test", executable }
|
||||
.to raise_error(ErrorDuringExecution)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run with Landlock" do
|
||||
it "allows standard devices and shared memory" do
|
||||
skip "Landlock not available." unless Sandbox::Landlock.available?
|
||||
|
||||
with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do
|
||||
landlock_sandbox = described_class.new
|
||||
landlock_sandbox.run RUBY_PATH, "-rio/console", "-e", <<~'RUBY'
|
||||
expect do
|
||||
sandbox.run RUBY_PATH, "-rio/console", "-e", <<~'RUBY'
|
||||
begin
|
||||
File.open("/dev/tty", "r+") { |tty| tty.winsize }
|
||||
rescue Errno::ENXIO, Errno::ENOENT, Errno::EACCES, Errno::EPERM
|
||||
@@ -539,7 +86,7 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
File.unlink(path)
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,9 +12,8 @@ RSpec.describe Sandbox do
|
||||
|
||||
before do
|
||||
allow(described_class).to receive_messages(
|
||||
ensure_sandbox_installed!: nil,
|
||||
available?: true,
|
||||
new: command_sandbox,
|
||||
available?: true,
|
||||
new: command_sandbox,
|
||||
)
|
||||
allow(command_sandbox).to receive_messages(
|
||||
allow_write_temp_and_cache: nil,
|
||||
|
||||
@@ -2810,22 +2810,6 @@ _brew_setup_ruby() {
|
||||
__brew_complete_commands
|
||||
}
|
||||
|
||||
_brew_setup_sandbox() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
case "${cur}" in
|
||||
-*)
|
||||
__brewcomp "
|
||||
--debug
|
||||
--help
|
||||
--quiet
|
||||
--verbose
|
||||
"
|
||||
return
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_brew_sh() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
case "${cur}" in
|
||||
@@ -3840,7 +3824,6 @@ _brew() {
|
||||
search) _brew_search ;;
|
||||
services) _brew_services ;;
|
||||
setup-ruby) _brew_setup_ruby ;;
|
||||
setup-sandbox) _brew_setup_sandbox ;;
|
||||
sh) _brew_sh ;;
|
||||
shellenv) _brew_shellenv ;;
|
||||
source) _brew_source ;;
|
||||
|
||||
@@ -1804,13 +1804,6 @@ __fish_brew_complete_arg 'setup-ruby' -l verbose -d 'Make some output more verbo
|
||||
__fish_brew_complete_arg 'setup-ruby' -a '(__fish_brew_suggest_commands)'
|
||||
|
||||
|
||||
__fish_brew_complete_cmd 'setup-sandbox' 'Run any necessary commands to setup the Homebrew sandbox'
|
||||
__fish_brew_complete_arg 'setup-sandbox' -l debug -d 'Display any debugging information'
|
||||
__fish_brew_complete_arg 'setup-sandbox' -l help -d 'Show this message'
|
||||
__fish_brew_complete_arg 'setup-sandbox' -l quiet -d 'Make some output more quiet'
|
||||
__fish_brew_complete_arg 'setup-sandbox' -l verbose -d 'Make some output more verbose'
|
||||
|
||||
|
||||
__fish_brew_complete_cmd 'sh' 'Enter an interactive shell for Homebrew\'s build environment'
|
||||
__fish_brew_complete_arg 'sh' -l cmd -d 'Execute commands in a non-interactive shell'
|
||||
__fish_brew_complete_arg 'sh' -l debug -d 'Display any debugging information'
|
||||
|
||||
@@ -76,7 +76,6 @@ sandbox-exec
|
||||
search
|
||||
services
|
||||
setup-ruby
|
||||
setup-sandbox
|
||||
sh
|
||||
shellenv
|
||||
source
|
||||
|
||||
@@ -234,7 +234,6 @@ __brew_internal_commands() {
|
||||
'search:Perform a substring search of cask tokens and formula names for text'
|
||||
'services:Manage background services with macOS'\'' `launchctl`(1) daemon manager or Linux'\''s `systemctl`(1) service manager'
|
||||
'setup-ruby:Installs and configures Homebrew'\''s Ruby'
|
||||
'setup-sandbox:Run any necessary commands to setup the Homebrew sandbox'
|
||||
'sh:Enter an interactive shell for Homebrew'\''s build environment'
|
||||
'shellenv:Valid shells: bash|csh|fish|pwsh|sh|tcsh|zsh Print export statements'
|
||||
'source:Open a formula'\''s source repository in a browser, or open Homebrew'\''s own repository if no argument is provided'
|
||||
@@ -2325,15 +2324,6 @@ _brew_setup_ruby() {
|
||||
'*:command:__brew_commands'
|
||||
}
|
||||
|
||||
# brew setup-sandbox
|
||||
_brew_setup_sandbox() {
|
||||
_arguments \
|
||||
'--debug[Display any debugging information]' \
|
||||
'--help[Show this message]' \
|
||||
'--quiet[Make some output more quiet]' \
|
||||
'--verbose[Make some output more verbose]'
|
||||
}
|
||||
|
||||
# brew sh
|
||||
_brew_sh() {
|
||||
_arguments \
|
||||
|
||||
@@ -2116,11 +2116,6 @@ line; lines starting with `#` are comments. Changes take effect on the next
|
||||
Installs and configures Homebrew's Ruby. If `command` is passed, it will only
|
||||
run Bundler if necessary for that command.
|
||||
|
||||
### `setup-sandbox`
|
||||
|
||||
Run any necessary commands to setup the Homebrew sandbox. Must be run with
|
||||
`sudo`. Currently a no-op on non-Linux.
|
||||
|
||||
### `shellenv` \[*`shell`* ...\]
|
||||
|
||||
Valid shells: bash\|csh\|fish\|pwsh\|sh\|tcsh\|zsh
|
||||
|
||||
@@ -1346,8 +1346,6 @@ Output as JSON\.
|
||||
Remove all unused services\.
|
||||
.SS "\fBsetup\-ruby\fP \fR[\fIcommand\fP \.\.\.]"
|
||||
Installs and configures Homebrew\[u2019]s Ruby\. If \fBcommand\fP is passed, it will only run Bundler if necessary for that command\.
|
||||
.SS "\fBsetup\-sandbox\fP"
|
||||
Run any necessary commands to setup the Homebrew sandbox\. Must be run with \fBsudo\fP\&\. Currently a no\-op on non\-Linux\.
|
||||
.SS "\fBshellenv\fP \fR[\fIshell\fP \.\.\.]"
|
||||
Valid shells: bash|csh|fish|pwsh|sh|tcsh|zsh
|
||||
.P
|
||||
|
||||
Reference in New Issue
Block a user