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/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb index bcab2320235c1..2c17d2360d714 100644 --- a/Library/Homebrew/dev-cmd/test.rb +++ b/Library/Homebrew/dev-cmd/test.rb @@ -86,7 +86,7 @@ def run sandbox = Sandbox.new f.logs.mkpath sandbox.record_log(f.logs/"test.sandbox.log") - sandbox.allow_write_temp_and_cache(f) + sandbox.allow_write_temp_and_cache sandbox.allow_write_log(f) sandbox.allow_write_xcode sandbox.allow_write_path(HOMEBREW_PREFIX/"var/cache") @@ -94,6 +94,8 @@ def run 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.allow_write_global_temp if f.allowed_in_sandbox?(:write_to_temp, phase: :test) + sandbox.deny_signal if f.allowed_in_sandbox?(:signal, phase: :test) sandbox.exec(*exec_args) else exec(*exec_args) diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 6298a2da05b3f..e9c107ec5b3a7 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1551,13 +1551,17 @@ def link_overwrite?(path) # @see .disable! delegate disable_reason: :"self.class" - # Sandbox rules that should be skipped when installing or testing this {Formula}. - # Returns `nil` if there are no sandbox rules to skip. - # @!method reduced_sandbox - # @return [Array] - # @see .reduce_sandbox - def reduced_sandbox - self.class.reduced_sandbox || [] + # 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 + def allowed_in_sandbox?(type, phase:) + return false unless self.class.allowed_in_sandbox + return false unless self.class.allowed_in_sandbox.key?(phase) + + self.class.allowed_in_sandbox[phase].include?(type) end sig { returns(T::Boolean) } @@ -3287,7 +3291,7 @@ def freeze attr_reader :keg_only_reason # The types of sandbox restrictions that should be lifted from the formula. - attr_reader :reduced_sandbox + 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. @@ -4309,29 +4313,59 @@ def link_overwrite(*paths) link_overwrite_paths.merge(paths) end - # Skip certain sandbox restrictions when installing this formula. + # 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. + # 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 upstream needs to write to `/private/tmp`: + # If the formula needs to write to `/private/tmp` in all phases: # # ```ruby - # reduce_sandbox :allow_write_to_temp + # allow_in_sandbox! :write_to_temp # ``` - def reduce_sandbox!(*types) - invalid_types = types.select { |type| Sandbox::SANDBOX_REDUCTIONS.exclude?(type) } + # + # 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] + # ``` + 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 sandbox reduction #{noun}: #{invalid_types.join(", ")}" + 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 - @reduced_sandbox = types + @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 29b9d4da152de..5d81cfc56e54d 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -937,14 +937,15 @@ def build formula.logs.mkpath sandbox.record_log(formula.logs/"build.sandbox.log") sandbox.allow_write_path(Dir.home) if interactive? - sandbox.allow_write_temp_and_cache(formula) + sandbox.allow_write_temp_and_cache sandbox.allow_write_log(formula) sandbox.allow_cvs sandbox.allow_fossil sandbox.allow_write_xcode sandbox.allow_write_cellar(formula) - sandbox.deny_signal(formula) sandbox.deny_all_network_except_pipe(error_pipe) unless formula.network_access_allowed?(:build) + sandbox.allow_write_global_temp if formula.allowed_in_sandbox?(:write_to_temp, phase: :build) + sandbox.deny_signal if formula.allowed_in_sandbox?(:signal, phase: :build) sandbox.exec(*args) else exec(*args) @@ -1154,7 +1155,7 @@ def post_install sandbox = Sandbox.new formula.logs.mkpath sandbox.record_log(formula.logs/"postinstall.sandbox.log") - sandbox.allow_write_temp_and_cache(formula) + sandbox.allow_write_temp_and_cache sandbox.allow_write_log(formula) sandbox.allow_write_xcode sandbox.deny_write_homebrew_repository @@ -1163,6 +1164,8 @@ def post_install Keg::KEG_LINK_DIRECTORIES.each do |dir| sandbox.allow_write_path "#{HOMEBREW_PREFIX}/#{dir}" end + sandbox.allow_write_global_temp if formula.allowed_in_sandbox?(:write_to_temp, phase: :postinstall) + sandbox.deny_signal if 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 ba923b9cc9a66..b65c2ebbaec27 100644 --- a/Library/Homebrew/sandbox.rb +++ b/Library/Homebrew/sandbox.rb @@ -11,7 +11,8 @@ class Sandbox SANDBOX_EXEC = "/usr/bin/sandbox-exec" private_constant :SANDBOX_EXEC - SANDBOX_REDUCTIONS = [:allow_write_to_temp, :allow_signal].freeze + SANDBOX_DSL_RULES = [:write_to_temp, :signal].freeze + SANDBOX_DSL_PHASES = [:build, :postinstall, :test].freeze sig { returns(T::Boolean) } def self.available? @@ -57,23 +58,22 @@ def deny_write_path(path) deny_write path:, type: :subpath end - sig { params(formula: T.nilable(Formula)).void } - def allow_write_temp_and_cache(formula = nil) - if formula&.reduced_sandbox&.include?(:allow_write_to_temp) - allow_write_path "/private/tmp" - allow_write_path "/private/var/tmp" - end + sig { void } + def allow_write_temp_and_cache allow_write path: "^/private/var/folders/[^/]+/[^/]+/[C,T]/", type: :regex allow_write_path HOMEBREW_TEMP allow_write_path HOMEBREW_CACHE end - sig { params(formula: T.nilable(Formula)).void } - def deny_signal(formula = nil) - puts "deny_signal: #{formula&.reduced_sandbox&.include?(:allow_signal)}" - unless formula&.reduced_sandbox&.include?(:allow_signal) - add_rule allow: false, operation: "signal", filter: "target others" - 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 }