diff --git a/Library/Homebrew/ast_constants.rb b/Library/Homebrew/ast_constants.rb index 38df04869e65d..1b28d06bf9e9f 100644 --- a/Library/Homebrew/ast_constants.rb +++ b/Library/Homebrew/ast_constants.rb @@ -46,6 +46,7 @@ [{ name: :needs, type: :method_call }], [{ name: :allow_network_access!, type: :method_call }], [{ name: :deny_network_access!, type: :method_call }], + [{ name: :allow_in_sandbox!, type: :method_call }], [{ name: :install, type: :method_definition }], [{ name: :post_install, type: :method_definition }], [{ name: :caveats, type: :method_definition }], diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index f837c599d3267..efc2c7256fff3 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -53,12 +53,12 @@ if [[ -n "${HOMEBREW_MACOS}" ]] then HOMEBREW_DEFAULT_CACHE="${HOME}/Library/Caches/Homebrew" HOMEBREW_DEFAULT_LOGS="${HOME}/Library/Logs/Homebrew" - HOMEBREW_DEFAULT_TEMP="/private/tmp" + HOMEBREW_DEFAULT_TEMP="/private/tmp/homebrew" else CACHE_HOME="${HOMEBREW_XDG_CACHE_HOME:-${HOME}/.cache}" HOMEBREW_DEFAULT_CACHE="${CACHE_HOME}/Homebrew" HOMEBREW_DEFAULT_LOGS="${CACHE_HOME}/Homebrew/Logs" - HOMEBREW_DEFAULT_TEMP="/tmp" + HOMEBREW_DEFAULT_TEMP="/tmp/homebrew" fi realpath() { diff --git a/Library/Homebrew/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb index 38684a29bedb4..4cf2aaa4bc917 100644 --- a/Library/Homebrew/dev-cmd/test.rb +++ b/Library/Homebrew/dev-cmd/test.rb @@ -93,7 +93,9 @@ def run sandbox.allow_write_path(HOMEBREW_PREFIX/"var/homebrew/locks") sandbox.allow_write_path(HOMEBREW_PREFIX/"var/log") sandbox.allow_write_path(HOMEBREW_PREFIX/"var/run") - sandbox.deny_all_network_except_pipe(error_pipe) unless f.class.network_access_allowed?(:test) + sandbox.deny_all_network_except_pipe(error_pipe) unless f.allowed_in_sandbox?(:network, phase: :test) + sandbox.allow_write_global_temp if f.allowed_in_sandbox?(:write_to_temp, phase: :test) + sandbox.deny_signal unless f.allowed_in_sandbox?(:signal, phase: :test) sandbox.exec(*exec_args) else exec(*exec_args) diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index 35b56c45eec87..4a2c93799addd 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -432,7 +432,7 @@ module EnvConfig "different volumes, as macOS has trouble moving symlinks across volumes when the target " \ "does not yet exist. This issue typically occurs when using FileVault or custom SSD " \ "configurations.", - default_text: "macOS: `/private/tmp`, Linux: `/tmp`.", + default_text: "macOS: `/private/tmp/homebrew`, Linux: `/tmp/homebrew`.", default: HOMEBREW_DEFAULT_TEMP, }, HOMEBREW_UPDATE_TO_TAG: { diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 11ee34f31466e..efc92e31029a0 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -35,6 +35,7 @@ require "extend/on_system" require "api" require "extend/api_hashable" +require "sandbox" # A formula provides instructions and metadata for Homebrew to install a piece # of software. Every Homebrew formula is a {Formula}. @@ -77,11 +78,6 @@ class Formula extend Attrable extend APIHashable - SUPPORTED_NETWORK_ACCESS_PHASES = [:build, :test, :postinstall].freeze - private_constant :SUPPORTED_NETWORK_ACCESS_PHASES - DEFAULT_NETWORK_ACCESS_ALLOWED = true - private_constant :DEFAULT_NETWORK_ACCESS_ALLOWED - # The name of this {Formula}. # e.g. `this-formula` # @@ -1550,6 +1546,26 @@ def link_overwrite?(path) # @see .disable! delegate disable_reason: :"self.class" + # Whether or not the given sandbox rule should be skipped during the given phase in this {Formula}. + # @!method allowed_in_sandbox? + # @param type [Symbol] the type of sandbox rule + # @param phase [Symbol] the phase to check + # @return [Boolean] + # @see .allow_in_sandbox + sig { params(type: Symbol, phase: Symbol).returns(T::Boolean) } + def allowed_in_sandbox?(type, phase:) + raise ArgumentError, "Unknown phase: #{phase}" unless Sandbox::SANDBOX_DSL_PHASES.include?(phase) + + return false unless self.class.allowed_in_sandbox + return false unless self.class.allowed_in_sandbox.key?(phase) + + allowed = self.class.allowed_in_sandbox[phase].include?(type) + return allowed if type != :network + + env_var = Homebrew::EnvConfig.send(:"formula_#{phase}_network") + env_var.nil? ? allowed : env_var == "allow" + end + sig { returns(T::Boolean) } def skip_cxxstdlib_check? false @@ -3240,9 +3256,7 @@ def inherited(child) @skip_clean_paths = Set.new @link_overwrite_paths = Set.new @loaded_from_api = false - @network_access_allowed = SUPPORTED_NETWORK_ACCESS_PHASES.to_h do |phase| - [phase, DEFAULT_NETWORK_ACCESS_ALLOWED] - end + @allowed_in_sandbox = {} end end @@ -3276,6 +3290,9 @@ def freeze # The reason for why this software is not linked (by default) to {::HOMEBREW_PREFIX}. attr_reader :keg_only_reason + # The types of sandbox restrictions that should be lifted from the formula. + attr_reader :allowed_in_sandbox + # A one-line description of the software. Used by users to get an overview # of the software and Homebrew maintainers. # Shows when running `brew info`. @@ -3370,16 +3387,8 @@ def license(args = nil) # @!attribute [w] allow_network_access! sig { params(phases: T.any(Symbol, T::Array[Symbol])).void } def allow_network_access!(phases = []) - phases_array = Array(phases) - if phases_array.empty? - @network_access_allowed.each_key { |phase| @network_access_allowed[phase] = true } - else - phases_array.each do |phase| - raise ArgumentError, "Unknown phase: #{phase}" unless SUPPORTED_NETWORK_ACCESS_PHASES.include?(phase) - - @network_access_allowed[phase] = true - end - end + # TODO: uncomment for Homebrew 3.4.0 + # odeprecated "`allow_network_access!`", "`allow_in_sandbox! :network`" end # The phases for which network access is denied. By default, network @@ -3402,27 +3411,11 @@ def allow_network_access!(phases = []) # ``` # # @!attribute [w] deny_network_access! - sig { params(phases: T.any(Symbol, T::Array[Symbol])).void } - def deny_network_access!(phases = []) - phases_array = Array(phases) - if phases_array.empty? - @network_access_allowed.each_key { |phase| @network_access_allowed[phase] = false } - else - phases_array.each do |phase| - raise ArgumentError, "Unknown phase: #{phase}" unless SUPPORTED_NETWORK_ACCESS_PHASES.include?(phase) - - @network_access_allowed[phase] = false - end - end - end - - # Whether the specified phase should be forced offline. - sig { params(phase: Symbol).returns(T::Boolean) } - def network_access_allowed?(phase) - raise ArgumentError, "Unknown phase: #{phase}" unless SUPPORTED_NETWORK_ACCESS_PHASES.include?(phase) - - env_var = Homebrew::EnvConfig.send(:"formula_#{phase}_network") - env_var.nil? ? @network_access_allowed[phase] : env_var == "allow" + sig { params(phases: T.nilable(T.any(Symbol, T::Array[Symbol]))).void } + def deny_network_access!(phases = nil) + # TODO: uncomment for Homebrew 3.4.0 + # odeprecated "`deny_network_access!`" + allow_in_sandbox! :network, phase: phases end # The homepage for the software. Used by users to get more information @@ -4295,6 +4288,62 @@ def link_overwrite(*paths) paths.flatten! link_overwrite_paths.merge(paths) end + + # Skip certain sandbox restrictions when installing and testing this formula. + # This can be useful if the upstream build system needs to write to + # locations that are protected by sandbox restrictions. Passing a + # phase is optional, and if not provided, the rule will be applied to + # all phases. The possible phases are `:build`, `:postinstall`, and `:test`. + # + # ### Example + # + # If the formula needs to write to `/private/tmp` in all phases: + # + # ```ruby + # allow_in_sandbox! :write_to_temp + # ``` + # + # If the formula needs to send signals in the `:test` phase: + # ```ruby + # allow_in_sandbox! :signal, phase: :test + # ``` + # + # If the formula needs to write to `/private/tmp` and send signals + # in the `:test` and `:install` phase: + # ```ruby + # allow_in_sandbox! :write_to_temp, :signal, phase: [:test, :install] + # ``` + sig { params(types: Symbol, phase: T.nilable(T.any(Symbol, T::Array[Symbol]))).void } + def allow_in_sandbox!(*types, phase: nil) + invalid_types = types.select { |type| Sandbox::SANDBOX_DSL_RULES.exclude?(type) } + if invalid_types.any? + noun = if invalid_types.count > 1 + "types" + else + "type" + end + raise ArgumentError, "Unsupported allow in sandbox item #{noun}: #{invalid_types.join(", ")}" + end + + phase ||= Sandbox::SANDBOX_DSL_PHASES + phases = Array(phase) + invalid_phases = phases.select { |p| Sandbox::SANDBOX_DSL_PHASES.exclude?(p) } + if invalid_phases.any? + noun = if invalid_phases.count > 1 + "phases" + else + "phase" + end + raise ArgumentError, "Unsupported sandbox phase #{noun}: #{invalid_phases.join(", ")}" + end + + @allowed_in_sandbox ||= {} + phases.each do |p| + @allowed_in_sandbox[p] ||= [] + @allowed_in_sandbox[p].concat(types) + @allowed_in_sandbox[p] = @allowed_in_sandbox[p].uniq + end + end end end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 8e6b56049516b..7b9355f328ba4 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -943,7 +943,9 @@ def build sandbox.allow_fossil sandbox.allow_write_xcode sandbox.allow_write_cellar(formula) - sandbox.deny_all_network_except_pipe(error_pipe) unless formula.network_access_allowed?(:build) + sandbox.deny_all_network_except_pipe(error_pipe) unless formula.allowed_in_sandbox?(:network, phase: :build) + sandbox.allow_write_global_temp if formula.allowed_in_sandbox?(:write_to_temp, phase: :build) + sandbox.deny_signal unless formula.allowed_in_sandbox?(:signal, phase: :build) sandbox.exec(*args) else exec(*args) @@ -1158,10 +1160,14 @@ def post_install sandbox.allow_write_xcode sandbox.deny_write_homebrew_repository sandbox.allow_write_cellar(formula) - sandbox.deny_all_network_except_pipe(error_pipe) unless formula.network_access_allowed?(:postinstall) Keg::KEG_LINK_DIRECTORIES.each do |dir| sandbox.allow_write_path "#{HOMEBREW_PREFIX}/#{dir}" end + unless formula.allowed_in_sandbox?(:network, phase: :postinstall) + sandbox.deny_all_network_except_pipe(error_pipe) + end + sandbox.allow_write_global_temp if formula.allowed_in_sandbox?(:write_to_temp, phase: :postinstall) + sandbox.deny_signal unless formula.allowed_in_sandbox?(:signal, phase: :postinstall) sandbox.exec(*args) else exec(*args) diff --git a/Library/Homebrew/sandbox.rb b/Library/Homebrew/sandbox.rb index 5fe1f43766510..0154646d5c168 100644 --- a/Library/Homebrew/sandbox.rb +++ b/Library/Homebrew/sandbox.rb @@ -11,6 +11,9 @@ class Sandbox SANDBOX_EXEC = "/usr/bin/sandbox-exec" private_constant :SANDBOX_EXEC + SANDBOX_DSL_RULES = [:write_to_temp, :signal, :network].freeze + SANDBOX_DSL_PHASES = [:build, :postinstall, :test].freeze + sig { returns(T::Boolean) } def self.available? false @@ -57,13 +60,22 @@ def deny_write_path(path) sig { void } def allow_write_temp_and_cache - allow_write_path "/private/tmp" - allow_write_path "/private/var/tmp" allow_write path: "^/private/var/folders/[^/]+/[^/]+/[C,T]/", type: :regex allow_write_path HOMEBREW_TEMP allow_write_path HOMEBREW_CACHE end + sig { void } + def allow_write_global_temp + allow_write_path "/private/tmp" + allow_write_path "/private/var/tmp" + end + + sig { void } + def deny_signal + add_rule allow: false, operation: "signal", filter: "target others" + end + sig { void } def allow_cvs allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.cvspass"