diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7d80e98816..a76beb9fb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 42eb0a8518..a0634c5699 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index 4097b38d62..80101b8589 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -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 ##### diff --git a/Library/Homebrew/cask/artifact/abstract_artifact.rb b/Library/Homebrew/cask/artifact/abstract_artifact.rb index b5176887de..52363afb37 100644 --- a/Library/Homebrew/cask/artifact/abstract_artifact.rb +++ b/Library/Homebrew/cask/artifact/abstract_artifact.rb @@ -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| diff --git a/Library/Homebrew/cmd/setup-sandbox.rb b/Library/Homebrew/cmd/setup-sandbox.rb deleted file mode 100644 index ace0d09277..0000000000 --- a/Library/Homebrew/cmd/setup-sandbox.rb +++ /dev/null @@ -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 diff --git a/Library/Homebrew/cmd/setup-sandbox.sh b/Library/Homebrew/cmd/setup-sandbox.sh deleted file mode 100644 index 85601fe8ec..0000000000 --- a/Library/Homebrew/cmd/setup-sandbox.sh +++ /dev/null @@ -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 -} diff --git a/Library/Homebrew/dependency_collector.rb b/Library/Homebrew/dependency_collector.rb index a6dd80ef72..dfa27217e7 100644 --- a/Library/Homebrew/dependency_collector.rb +++ b/Library/Homebrew/dependency_collector.rb @@ -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) diff --git a/Library/Homebrew/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb index 019ea00e86..906864d1d6 100644 --- a/Library/Homebrew/dev-cmd/test.rb +++ b/Library/Homebrew/dev-cmd/test.rb @@ -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 diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index 7404d357d7..6318167726 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -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, diff --git a/Library/Homebrew/extend/os/linux/dependency_collector.rb b/Library/Homebrew/extend/os/linux/dependency_collector.rb index ba8494d2b3..7a69985472 100644 --- a/Library/Homebrew/extend/os/linux/dependency_collector.rb +++ b/Library/Homebrew/extend/os/linux/dependency_collector.rb @@ -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 [] diff --git a/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb b/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb index f16216bf83..d76e3f233a 100644 --- a/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb +++ b/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb @@ -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 diff --git a/Library/Homebrew/extend/os/linux/diagnostic.rb b/Library/Homebrew/extend/os/linux/diagnostic.rb index 14fb04c23b..c5e401d5c4 100644 --- a/Library/Homebrew/extend/os/linux/diagnostic.rb +++ b/Library/Homebrew/extend/os/linux/diagnostic.rb @@ -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 diff --git a/Library/Homebrew/extend/os/linux/formula.rb b/Library/Homebrew/extend/os/linux/formula.rb index 63452d2b42..6fb50dc865 100644 --- a/Library/Homebrew/extend/os/linux/formula.rb +++ b/Library/Homebrew/extend/os/linux/formula.rb @@ -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 diff --git a/Library/Homebrew/extend/os/linux/sandbox.rb b/Library/Homebrew/extend/os/linux/sandbox.rb index a72502550c..81bd0b1574 100644 --- a/Library/Homebrew/extend/os/linux/sandbox.rb +++ b/Library/Homebrew/extend/os/linux/sandbox.rb @@ -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 diff --git a/Library/Homebrew/extend/os/linux/sandbox/bubblewrap.rb b/Library/Homebrew/extend/os/linux/sandbox/bubblewrap.rb deleted file mode 100644 index 0d919bcf97..0000000000 --- a/Library/Homebrew/extend/os/linux/sandbox/bubblewrap.rb +++ /dev/null @@ -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 diff --git a/Library/Homebrew/extend/os/linux/sandbox/landlock.rb b/Library/Homebrew/extend/os/linux/sandbox/landlock.rb index f6bf46a4f1..c52c8a37a4 100644 --- a/Library/Homebrew/extend/os/linux/sandbox/landlock.rb +++ b/Library/Homebrew/extend/os/linux/sandbox/landlock.rb @@ -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({ diff --git a/Library/Homebrew/extend/os/linux/test_bot.rb b/Library/Homebrew/extend/os/linux/test_bot.rb index 51a42f0916..f1e24d7cf7 100644 --- a/Library/Homebrew/extend/os/linux/test_bot.rb +++ b/Library/Homebrew/extend/os/linux/test_bot.rb @@ -22,10 +22,7 @@ module OS sig { returns(T::Boolean) } def configure_sandbox! require "sandbox" - ::Sandbox.configure! ::Sandbox.available? - rescue ::ErrorDuringExecution - false end end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 7ce0d267ac..22c1a5af36 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -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? diff --git a/Library/Homebrew/github_runner_matrix.rb b/Library/Homebrew/github_runner_matrix.rb index c486c0a162..f0b5d8ea93 100644 --- a/Library/Homebrew/github_runner_matrix.rb +++ b/Library/Homebrew/github_runner_matrix.rb @@ -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 diff --git a/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md b/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md index bbf5b8b906..f04b58d56b 100644 --- a/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md +++ b/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md @@ -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). diff --git a/Library/Homebrew/sandbox.rb b/Library/Homebrew/sandbox.rb index ada62f515d..a967a5025b 100644 --- a/Library/Homebrew/sandbox.rb +++ b/Library/Homebrew/sandbox.rb @@ -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 diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/setup_sandbox.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/setup_sandbox.rbi deleted file mode 100644 index 5aba326c27..0000000000 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/setup_sandbox.rbi +++ /dev/null @@ -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 diff --git a/Library/Homebrew/test/bash_spec.rb b/Library/Homebrew/test/bash_spec.rb index 49e12ffebd..9bcc128eb7 100644 --- a/Library/Homebrew/test/bash_spec.rb +++ b/Library/Homebrew/test/bash_spec.rb @@ -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 diff --git a/Library/Homebrew/test/bundle/installer_spec.rb b/Library/Homebrew/test/bundle/installer_spec.rb index d3391d01ee..84a66c3ced 100644 --- a/Library/Homebrew/test/bundle/installer_spec.rb +++ b/Library/Homebrew/test/bundle/installer_spec.rb @@ -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] diff --git a/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb b/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb index 4106dc0c4a..a4464a6148 100644 --- a/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb +++ b/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb @@ -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) diff --git a/Library/Homebrew/test/cmd/setup-sandbox_spec.rb b/Library/Homebrew/test/cmd/setup-sandbox_spec.rb deleted file mode 100644 index 47bee45963..0000000000 --- a/Library/Homebrew/test/cmd/setup-sandbox_spec.rb +++ /dev/null @@ -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 diff --git a/Library/Homebrew/test/dev-cmd/tests_spec.rb b/Library/Homebrew/test/dev-cmd/tests_spec.rb index 3da3265b6e..6d106c84ec 100644 --- a/Library/Homebrew/test/dev-cmd/tests_spec.rb +++ b/Library/Homebrew/test/dev-cmd/tests_spec.rb @@ -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 diff --git a/Library/Homebrew/test/formula_installer_spec.rb b/Library/Homebrew/test/formula_installer_spec.rb index 3c16d624dd..4cb761fbc1 100644 --- a/Library/Homebrew/test/formula_installer_spec.rb +++ b/Library/Homebrew/test/formula_installer_spec.rb @@ -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, diff --git a/Library/Homebrew/test/github_runner_matrix_spec.rb b/Library/Homebrew/test/github_runner_matrix_spec.rb index 54efc6cbe3..fe8365e1e7 100644 --- a/Library/Homebrew/test/github_runner_matrix_spec.rb +++ b/Library/Homebrew/test/github_runner_matrix_spec.rb @@ -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 diff --git a/Library/Homebrew/test/os/linux/dependency_collector_spec.rb b/Library/Homebrew/test/os/linux/dependency_collector_spec.rb index 15457af1f6..c29d9ee5aa 100644 --- a/Library/Homebrew/test/os/linux/dependency_collector_spec.rb +++ b/Library/Homebrew/test/os/linux/dependency_collector_spec.rb @@ -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 diff --git a/Library/Homebrew/test/os/linux/diagnostic_spec.rb b/Library/Homebrew/test/os/linux/diagnostic_spec.rb index ce50003887..9f2b54440e 100644 --- a/Library/Homebrew/test/os/linux/diagnostic_spec.rb +++ b/Library/Homebrew/test/os/linux/diagnostic_spec.rb @@ -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 diff --git a/Library/Homebrew/test/sandbox_landlock_spec.rb b/Library/Homebrew/test/sandbox_landlock_spec.rb index 304b2952c3..c6d05ff219 100644 --- a/Library/Homebrew/test/sandbox_landlock_spec.rb +++ b/Library/Homebrew/test/sandbox_landlock_spec.rb @@ -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 diff --git a/Library/Homebrew/test/sandbox_linux_spec.rb b/Library/Homebrew/test/sandbox_linux_spec.rb index 91bba304df..4feaa9bd1b 100644 --- a/Library/Homebrew/test/sandbox_linux_spec.rb +++ b/Library/Homebrew/test/sandbox_linux_spec.rb @@ -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 diff --git a/Library/Homebrew/test/sandbox_shared_spec.rb b/Library/Homebrew/test/sandbox_shared_spec.rb index 957b9775be..689d9d4189 100644 --- a/Library/Homebrew/test/sandbox_shared_spec.rb +++ b/Library/Homebrew/test/sandbox_shared_spec.rb @@ -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, diff --git a/completions/bash/brew b/completions/bash/brew index b9c405060a..0ec07547b8 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -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 ;; diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index 11e7f88bbb..734bece567 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -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' diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index d2df9507f2..7a37ff8baf 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -76,7 +76,6 @@ sandbox-exec search services setup-ruby -setup-sandbox sh shellenv source diff --git a/completions/zsh/_brew b/completions/zsh/_brew index 025f827f6c..21a151d828 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -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 \ diff --git a/docs/Manpage.md b/docs/Manpage.md index 662614aa60..bdb2140b73 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -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 diff --git a/manpages/brew.1 b/manpages/brew.1 index e0809fb8f3..01815e2954 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -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