Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allow_in_sandbox! DSL #17734

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Library/Homebrew/ast_constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }],
Expand Down
4 changes: 2 additions & 2 deletions Library/Homebrew/brew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 3 additions & 1 deletion Library/Homebrew/dev-cmd/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/env_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
127 changes: 88 additions & 39 deletions Library/Homebrew/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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`
#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
10 changes: 8 additions & 2 deletions Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 14 additions & 2 deletions Library/Homebrew/sandbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading