Class: Sandbox Private
- Extended by:
- Utils::Output::Mixin
- Includes:
- OS::Linux::Sandbox, OS::Mac::Sandbox, Utils::Output::Mixin
- Defined in:
- sandbox.rb,
extend/os/linux/sandbox/backend.rb,
extend/os/linux/sandbox/landlock.rb
Overview
This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.
Helper class for running a sub-process inside of a sandboxed environment.
Defined Under Namespace
Classes: Landlock, LinuxBackend
Constant Summary collapse
- PRIVILEGED_GROUPS =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
Privileged groups that are expected to be able to use a working sandbox.
%w[admin staff root wheel].freeze
- INHERITANCE_FD =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
A read-only descriptor identifies Homebrew's sandbox across exec, without trusting ENV.
198
Instance Attribute Summary collapse
- #profile ⇒ SandboxProfile readonly private
Class Method Summary collapse
- .available? ⇒ Boolean private
-
.avoid_nested_sandboxing? ⇒ Boolean
private
Skip Homebrew's own sandbox when it is opted into via
$HOMEBREW_AVOID_NESTED_SANDBOXINGand already running inside another sandbox. - .capture(executable, args: [], read_paths: [], write_paths: [], env: {}, input: [], must_succeed: true, print_stdout: false, print_stderr: true, chdir: nil) ⇒ SystemCommand::Result private
- .ensure_sandbox_available! ⇒ void private
- .executable ⇒ Pathname? private
- .executable! ⇒ Pathname private
- .executable_candidate_paths ⇒ ::PATH private
- .executable_name ⇒ String private
- .executable_usable?(_candidate) ⇒ Boolean private
- .failure_reason ⇒ String? private
- .for_operation(read_paths: [], write_paths: [], network_access: false, home_read_exception: nil) ⇒ Sandbox private
- .full_write_isolation? ⇒ Boolean private
-
.inherited_sandbox? ⇒ Boolean
private
Only the sandbox launcher passes this descriptor to its child.
- .isolate_operation? ⇒ Boolean private
-
.nested_sandbox? ⇒ Boolean
private
Whether Homebrew is itself running inside another sandbox, which would make its own nested sandbox hang (macOS) or fail to start (Linux).
-
.operation(action, payload, read_paths: [], write_paths: [], temporary_directory: HOMEBREW_TEMP) ⇒ String
private
The child accepts structured arguments and returns JSON, never Ruby objects.
- .reset_state! ⇒ void private
-
.ruby_command(file, *args) ⇒ Array<String, Pathname>
private
Launch Homebrew's worker scripts with the same Ruby and library paths.
- .run_command(*command, writable_path:, deny_network: false) ⇒ void private
- .run_or_fork(*args, step:, warn_without_sandbox: true, retain_tmp: false, debug: false, &_block) ⇒ void private
- .state ⇒ Symbol private
- .terminal_ioctl_request ⇒ Integer private
-
.tty_state ⇒ String?
private
The terminal state to restore after a PTY passthrough.
- .use_for?(step, warn_without_sandbox: true) ⇒ Boolean private
-
.with_preserved_brew_file(&block) ⇒ void
private
Landlock cannot protect
bin/brewwhile allowing writes tobin, so a sandboxed install hook could replacebrewto persist into later commands.
Instance Method Summary collapse
- #add_install_hook_rules(network_access_allowed:) ⇒ void private
- #add_rule(allow:, operation:, filter: nil, modifier: nil) ⇒ void private
- #allow_cvs ⇒ void private
- #allow_fossil ⇒ void private
- #allow_network(path:, type: :literal) ⇒ void private
- #allow_process_exec(path, no_sandbox: false) ⇒ void private
- #allow_read(path:, type: :literal) ⇒ void private
- #allow_read_if_exists(path:, type: :literal) ⇒ void private
- #allow_write(path:, type: :literal) ⇒ void private
- #allow_write_cellar(formula) ⇒ void private
- #allow_write_log(formula) ⇒ void private
- #allow_write_path(path) ⇒ void private
- #allow_write_path_if_exists(path) ⇒ void private
- #allow_write_system_temp ⇒ void private
- #allow_write_temp_and_cache ⇒ void private
- #allow_write_xcode ⇒ void private
-
#apply! ⇒ void
private
Only called in a forked child immediately before exec on Linux.
- #apply_before_exec? ⇒ Boolean private
-
#capture(executable, args: [], env: {}, input: [], must_succeed: true, print_stdout: false, print_stderr: true, debug: nil, verbose: nil, secrets: [], chdir: nil, timeout: nil, temporary_directory: HOMEBREW_TEMP) ⇒ SystemCommand::Result
private
Capture a non-interactive command without a PTY or process-wide signal handlers.
- #cleanup_sandbox ⇒ void private
- #copy_pty_output(controller) ⇒ void private
-
#denied_read_paths ⇒ Array<String>
private
Explicit read restrictions on literal paths and subpaths.
- #deny_all_network ⇒ void private
- #deny_read(path:, type: :literal) ⇒ void private
- #deny_read_home(except: nil) ⇒ void private
- #deny_read_path(path) ⇒ void private
- #deny_write(path:, type: :literal) ⇒ void private
- #deny_write_homebrew_repository ⇒ void private
- #deny_write_path(path) ⇒ void private
- #initialize ⇒ void constructor private
- #path_filter(path, type) ⇒ SandboxPathFilter private
-
#protect_homebrew_state ⇒ void
private
Protect locks and the temporary Cellar so sandboxed steps cannot disrupt other operations or plant kegs that
pourmoves into the Cellar. - #record_log(file) ⇒ void private
- #run(*args, passthrough_stdin: true, child_message_handler: nil, retain_tmp: false, debug: false) ⇒ void private
Methods included from Utils::Output::Mixin
issue_reporting_message, odebug, odeprecated, odie, odisabled, ofail, oh1, oh1_title, ohai, ohai_title, onoe, opoo, opoo_once, opoo_outside_github_actions, opoo_without_github_actions_annotation, pretty_cannot_install, pretty_deprecated, pretty_disabled, pretty_duration, pretty_install_status, pretty_installed, pretty_removed, pretty_uninstalled, pretty_unmarked, pretty_upgradable, pretty_warning
Constructor Details
#initialize ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
376 377 378 379 380 381 |
# File 'sandbox.rb', line 376 def initialize @profile = T.let(SandboxProfile.new, SandboxProfile) @failed = T.let(false, T::Boolean) @logfile = T.let(nil, T.nilable(T.any(String, Pathname))) @start = T.let(nil, T.nilable(Time)) end |
Instance Attribute Details
#profile ⇒ SandboxProfile (readonly)
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
877 878 879 |
# File 'sandbox.rb', line 877 def profile @profile end |
Class Method Details
.available? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
88 89 90 |
# File 'sandbox.rb', line 88 def self.available? false end |
.avoid_nested_sandboxing? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
Skip Homebrew's own sandbox when it is opted into via
$HOMEBREW_AVOID_NESTED_SANDBOXING and already running inside another
sandbox. The skip is only supported for an unprivileged user in a custom
prefix; error out explaining why rather than silently sandboxing (and
hanging) when either is not the case.
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'sandbox.rb', line 107 def self.avoid_nested_sandboxing? return false unless Homebrew::EnvConfig.avoid_nested_sandboxing? return false unless nested_sandbox? if Homebrew.default_prefix? odie "Refusing to skip the sandbox: `$HOMEBREW_AVOID_NESTED_SANDBOXING` is set " \ "inside another sandbox but Homebrew is using its default prefix " \ "(#{HOMEBREW_PREFIX}); this is only supported in a custom prefix." end privileged_group = PRIVILEGED_GROUPS.find do |name| group = Etc.getgrnam(name) group && Process.groups.include?(group.gid) rescue ArgumentError false end if privileged_group odie "Refusing to skip the sandbox: `$HOMEBREW_AVOID_NESTED_SANDBOXING` is set " \ "inside another sandbox but you are in the privileged `#{privileged_group}` " \ "group; this is only supported for an unprivileged user." end true end |
.capture(executable, args: [], read_paths: [], write_paths: [], env: {}, input: [], must_succeed: true, print_stdout: false, print_stderr: true, chdir: nil) ⇒ SystemCommand::Result
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
239 240 241 242 243 244 245 246 247 |
# File 'sandbox.rb', line 239 def self.capture(executable, args: [], read_paths: [], write_paths: [], env: {}, input: [], must_succeed: true, print_stdout: false, print_stderr: true, chdir: nil) if isolate_operation? for_operation(read_paths:, write_paths:).capture(executable, args:, env:, input:, must_succeed:, print_stdout:, print_stderr:, chdir:) else SystemCommand.run(executable, args:, env:, input:, must_succeed:, print_stdout:, print_stderr:, chdir:) end end |
.ensure_sandbox_available! ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
280 281 282 283 284 |
# File 'sandbox.rb', line 280 def self.ensure_sandbox_available! return if available? raise failure_reason || "The sandbox is not available." end |
.executable ⇒ Pathname?
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'sandbox.rb', line 334 def self.executable executable_candidate_paths.each do |path| begin candidate = Pathname.new(File.(executable_name, path)) rescue ArgumentError next end next if !candidate.file? || !candidate.executable? next unless executable_usable?(candidate) return candidate end nil end |
.executable! ⇒ Pathname
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
352 353 354 |
# File 'sandbox.rb', line 352 def self.executable! executable || raise("#{executable_name} is required to use the sandbox.") end |
.executable_candidate_paths ⇒ ::PATH
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
326 327 328 329 330 331 |
# File 'sandbox.rb', line 326 def self.executable_candidate_paths executable_path = Pathname.new(executable_name) return PATH.new(executable_path.dirname) if executable_path.absolute? PATH.new(ORIGINAL_PATHS, ENV.fetch("PATH"), HOMEBREW_BREW_FILE.dirname) end |
.executable_name ⇒ String
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
321 322 323 |
# File 'sandbox.rb', line 321 def self.executable_name raise NotImplementedError, "Sandbox is not implemented for this OS." end |
.executable_usable?(_candidate) ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
357 358 359 |
# File 'sandbox.rb', line 357 def self.executable_usable?(_candidate) true end |
.failure_reason ⇒ String?
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
292 293 294 295 296 |
# File 'sandbox.rb', line 292 def self.failure_reason return if state == :available "The sandbox is not available." end |
.for_operation(read_paths: [], write_paths: [], network_access: false, home_read_exception: nil) ⇒ Sandbox
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
219 220 221 222 223 224 225 226 227 228 |
# File 'sandbox.rb', line 219 def self.for_operation(read_paths: [], write_paths: [], network_access: false, home_read_exception: nil) new.tap do |sandbox| sandbox.deny_read_home(except: home_read_exception) sandbox.deny_all_network unless network_access sandbox.allow_network(path: "/", type: :subpath) if network_access && home_read_exception == :git (read_paths | write_paths).each { |path| sandbox.allow_read(path:, type: :subpath) } write_paths.each { |path| sandbox.allow_write_path(path) } sandbox.deny_write_homebrew_repository end end |
.full_write_isolation? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
93 |
# File 'sandbox.rb', line 93 def self.full_write_isolation? = true |
.inherited_sandbox? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
Only the sandbox launcher passes this descriptor to its child.
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'sandbox.rb', line 174 def self.inherited_sandbox? return false unless File.identical?(IO.new(INHERITANCE_FD, autoclose: false), __FILE__) # Ordinary file permissions cannot establish sandbox confinement. # When they already deny writes, rely on the inherited descriptor instead. return true unless File.stat(HOMEBREW_BREW_FILE).writable? # Probe without creating, truncating or modifying the executable. File.open(HOMEBREW_BREW_FILE, File::WRONLY) do raise "Inherited sandbox permits writes to #{HOMEBREW_BREW_FILE}" end rescue Errno::EBADF false rescue Errno::EACCES, Errno::EPERM, Errno::EROFS true end |
.isolate_operation? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
170 |
# File 'sandbox.rb', line 170 def self.isolate_operation? = use_for?("processing downloaded files") |
.nested_sandbox? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
Whether Homebrew is itself running inside another sandbox, which would make its own nested sandbox hang (macOS) or fail to start (Linux). Overridden per-OS.
99 |
# File 'sandbox.rb', line 99 def self.nested_sandbox? = false |
.operation(action, payload, read_paths: [], write_paths: [], temporary_directory: HOMEBREW_TEMP) ⇒ String
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
The child accepts structured arguments and returns JSON, never Ruby objects.
196 197 198 199 200 201 202 203 204 205 206 |
# File 'sandbox.rb', line 196 def self.operation(action, payload, read_paths: [], write_paths: [], temporary_directory: HOMEBREW_TEMP) sandbox = for_operation(read_paths:, write_paths:) command = ruby_command("sandbox_operation.rb", action) sandbox.capture( command.fetch(0), args: command.drop(1), input: payload, env: { "HOMEBREW_NO_BOOTSNAP" => "1" }, temporary_directory:, ).stdout end |
.reset_state! ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
299 |
# File 'sandbox.rb', line 299 def self.reset_state!; end |
.ruby_command(file, *args) ⇒ Array<String, Pathname>
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
Launch Homebrew's worker scripts with the same Ruby and library paths.
210 211 212 213 |
# File 'sandbox.rb', line 210 def self.ruby_command(file, *args) [*HOMEBREW_RUBY_EXEC_ARGS, "-I", $LOAD_PATH.join(File::PATH_SEPARATOR), "--", HOMEBREW_LIBRARY_PATH/file, *args] end |
.run_command(*command, writable_path:, deny_network: false) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'sandbox.rb', line 302 def self.run_command(*command, writable_path:, deny_network: false) ensure_sandbox_available! writable_path = Pathname(writable_path). if !writable_path.directory? || !writable_path.writable? raise UsageError, "`#{writable_path}` is not a writable directory." end writable_path = writable_path.realpath sandbox = new sandbox.allow_write_temp_and_cache sandbox.allow_write_path writable_path sandbox.deny_read_home sandbox.deny_all_network if deny_network sandbox.run "/bin/sh", "-c", "cd \"$1\" && shift && exec \"$@\"", "brew-sandbox-exec", writable_path, *command end |
.run_or_fork(*args, step:, warn_without_sandbox: true, retain_tmp: false, debug: false, &_block) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
159 160 161 162 163 164 165 166 167 |
# File 'sandbox.rb', line 159 def self.run_or_fork(*args, step:, warn_without_sandbox: true, retain_tmp: false, debug: false, &_block) if use_for?(step, warn_without_sandbox:) sandbox = new yield sandbox sandbox.run(*args, retain_tmp:, debug:) else Utils.safe_fork { exec(*args) } end end |
.state ⇒ Symbol
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
287 288 289 |
# File 'sandbox.rb', line 287 def self.state available? ? :available : :unavailable end |
.terminal_ioctl_request ⇒ Integer
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
362 363 364 |
# File 'sandbox.rb', line 362 def self.terminal_ioctl_request raise NotImplementedError, "Sandbox is not implemented for this OS." end |
.tty_state ⇒ String?
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
The terminal state to restore after a PTY passthrough. It cannot change
in the background while brew runs (each passthrough restores it), so
capture it once per process. nil when it cannot be captured.
370 371 372 373 |
# File 'sandbox.rb', line 370 def self.tty_state @tty_state ||= T.let(Utils.popen_read("stty", "-g", in: :in).chomp, T.nilable(String)) @tty_state.presence end |
.use_for?(step, warn_without_sandbox: true) ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'sandbox.rb', line 133 def self.use_for?(step, warn_without_sandbox: true) return false if inherited_sandbox? unless available? opoo_once "Sandbox unavailable: #{step} without sandboxing!" if warn_without_sandbox return false end if avoid_nested_sandboxing? opoo_once "#{step.capitalize} without Homebrew's sandbox; relying on the outer sandbox." if warn_without_sandbox return false end true end |
.with_preserved_brew_file(&block) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
Landlock cannot protect bin/brew while allowing writes to bin, so a
sandboxed install hook could replace brew to persist into later commands.
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'sandbox.rb', line 252 def self.with_preserved_brew_file(&block) return yield if full_write_isolation? brew_file = HOMEBREW_PREFIX/"bin/brew" File.open(brew_file.dirname) do |brew_directory| brew_directory_mode = brew_directory.stat.mode & 07777 symlink = brew_file.symlink? contents = symlink ? brew_file.readlink.to_s : brew_file.binread brew_file_mode = brew_file.lstat.mode & 07777 begin yield ensure brew_directory.chmod brew_directory_mode if symlink && (!brew_file.symlink? || brew_file.readlink.to_s != contents) FileUtils.rm_rf brew_file brew_file.make_symlink contents elsif !symlink && (brew_file.symlink? || !brew_file.file? || brew_file.binread != contents || (brew_file.lstat.mode & 07777) != brew_file_mode) FileUtils.rm_rf brew_file brew_file.atomic_write contents brew_file.chmod brew_file_mode end end end end |
Instance Method Details
#add_install_hook_rules(network_access_allowed:) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
613 614 615 616 617 618 |
# File 'sandbox.rb', line 613 def add_install_hook_rules(network_access_allowed:) allow_write_temp_and_cache deny_write_homebrew_repository deny_read_home deny_all_network unless network_access_allowed end |
#add_rule(allow:, operation:, filter: nil, modifier: nil) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
392 393 394 395 |
# File 'sandbox.rb', line 392 def add_rule(allow:, operation:, filter: nil, modifier: nil) rule = SandboxRule.new(allow:, operation:, filter:, modifier:) @profile.add_rule(rule) end |
#allow_cvs ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
621 622 623 |
# File 'sandbox.rb', line 621 def allow_cvs allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.cvspass" end |
#allow_fossil ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
626 627 628 629 |
# File 'sandbox.rb', line 626 def allow_fossil allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.fossil" allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.fossil-journal" end |
#allow_network(path:, type: :literal) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
673 674 675 |
# File 'sandbox.rb', line 673 def allow_network(path:, type: :literal) add_rule allow: true, operation: "network*", filter: path_filter(path, type) end |
#allow_process_exec(path, no_sandbox: false) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
403 404 405 406 |
# File 'sandbox.rb', line 403 def allow_process_exec(path, no_sandbox: false) modifier = "no-sandbox" if no_sandbox add_rule allow: true, operation: "process-exec", filter: path_filter(path, :literal), modifier: end |
#allow_read(path:, type: :literal) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
398 399 400 |
# File 'sandbox.rb', line 398 def allow_read(path:, type: :literal) add_rule allow: true, operation: "file-read*", filter: path_filter(path, type) end |
#allow_read_if_exists(path:, type: :literal) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
564 565 566 567 568 569 |
# File 'sandbox.rb', line 564 def allow_read_if_exists(path:, type: :literal) return unless path return unless File.exist?(path) allow_read path:, type: end |
#allow_write(path:, type: :literal) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
572 573 574 575 576 |
# File 'sandbox.rb', line 572 def allow_write(path:, type: :literal) add_rule allow: true, operation: "file-write*", filter: path_filter(path, type) add_rule allow: true, operation: "file-write-setugid", filter: path_filter(path, type) add_rule allow: true, operation: "file-write-mode", filter: path_filter(path, type) end |
#allow_write_cellar(formula) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
632 633 634 635 636 |
# File 'sandbox.rb', line 632 def allow_write_cellar(formula) allow_write_path formula.rack allow_write_path formula.etc allow_write_path formula.var end |
#allow_write_log(formula) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
657 658 659 |
# File 'sandbox.rb', line 657 def allow_write_log(formula) allow_write_path formula.logs end |
#allow_write_path(path) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
584 585 586 |
# File 'sandbox.rb', line 584 def allow_write_path(path) allow_write path:, type: :subpath end |
#allow_write_path_if_exists(path) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
589 590 591 592 593 594 |
# File 'sandbox.rb', line 589 def allow_write_path_if_exists(path) return unless path return unless File.exist?(path) allow_write_path path end |
#allow_write_system_temp ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
602 603 604 |
# File 'sandbox.rb', line 602 def allow_write_system_temp allow_write_path HOMEBREW_TEMP end |
#allow_write_temp_and_cache ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
607 608 609 610 |
# File 'sandbox.rb', line 607 def allow_write_temp_and_cache allow_write_system_temp allow_write_path HOMEBREW_CACHE end |
#allow_write_xcode ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
654 |
# File 'sandbox.rb', line 654 def allow_write_xcode; end |
#apply! ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
Only called in a forked child immediately before exec on Linux.
718 |
# File 'sandbox.rb', line 718 def apply!; end |
#apply_before_exec? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
721 |
# File 'sandbox.rb', line 721 def apply_before_exec? = false |
#capture(executable, args: [], env: {}, input: [], must_succeed: true, print_stdout: false, print_stderr: true, debug: nil, verbose: nil, secrets: [], chdir: nil, timeout: nil, temporary_directory: HOMEBREW_TEMP) ⇒ SystemCommand::Result
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
Capture a non-interactive command without a PTY or process-wide signal handlers.
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 |
# File 'sandbox.rb', line 693 def capture(executable, args: [], env: {}, input: [], must_succeed: true, print_stdout: false, print_stderr: true, debug: nil, verbose: nil, secrets: [], chdir: nil, timeout: nil, temporary_directory: HOMEBREW_TEMP) require "extend/ENV" Dir.mktmpdir("homebrew-sandbox", temporary_directory) do |tmpdir| env = ENV.sensitive_environment.transform_values { nil }.merge("HOME" => tmpdir) .merge(env).merge(sandbox_environment(tmpdir)) sandbox_executable, *sandbox_args = sandbox_command([executable, *args.map(&:to_s)], tmpdir) raise "Missing sandbox command" unless sandbox_executable command = SystemCommand.new(sandbox_executable, args: sandbox_args, env:, input:, must_succeed:, print_stdout:, print_stderr:, debug:, verbose:, secrets:, chdir: chdir || tmpdir, timeout:) command.sandbox = self if apply_before_exec? File.open(__FILE__) do |inheritance| command.sandbox_inheritance = inheritance command.run! end end ensure cleanup_sandbox end |
#cleanup_sandbox ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
724 |
# File 'sandbox.rb', line 724 def cleanup_sandbox; end |
#copy_pty_output(controller) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
880 881 882 883 884 885 |
# File 'sandbox.rb', line 880 def copy_pty_output(controller) controller.each_char { |c| print(c) } rescue Errno::EIO # Linux marks a PTY as an I/O error when its peer closes, so treat this as EOF: # https://github.com/torvalds/linux/blob/master/drivers/tty/pty.c end |
#denied_read_paths ⇒ Array<String>
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
Explicit read restrictions on literal paths and subpaths.
420 421 422 423 424 425 426 |
# File 'sandbox.rb', line 420 def denied_read_paths profile.rules.filter_map do |rule| next if rule.allow || rule.operation != "file-read*" || rule.filter&.type == :regex rule.filter&.path end end |
#deny_all_network ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
678 679 680 |
# File 'sandbox.rb', line 678 def deny_all_network add_rule allow: false, operation: "network*" end |
#deny_read(path:, type: :literal) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
409 410 411 |
# File 'sandbox.rb', line 409 def deny_read(path:, type: :literal) add_rule allow: false, operation: "file-read*", filter: path_filter(path, type) end |
#deny_read_home(except: nil) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 |
# File 'sandbox.rb', line 429 def deny_read_home(except: nil) if !except.nil? && except != :git raise ArgumentError, "Unknown home credential exception: #{except.inspect}" end require "trust" home = Pathname(Dir.home(ENV.fetch("USER"))).realpath readable_paths = [ HOMEBREW_PREFIX, HOMEBREW_REPOSITORY, HOMEBREW_CACHE, HOMEBREW_LOGS, HOMEBREW_TEMP, ENV.fetch("GITHUB_WORKSPACE", nil), ENV.fetch("RUNNER_WORKSPACE", nil), ENV.fetch("RUNNER_TEMP", nil), Homebrew::Trust.trust_file, *home_write_paths.select { |path| File.exist?(path) }, ].compact.flat_map do |path| path = Pathname(path) [path., (path.realpath if path.exist?)].compact end if except == :git || readable_paths.any? { |path| path.ascend.include?(home) } # When credentials, Homebrew or CI need `$HOME` paths to stay readable, deny only # well-known credential and personal-data paths instead of enumerating all # of `$HOME`. [ *GIT_CREDENTIAL_PATHS, ".aws", ".azure", ".boto", ".docker", ".config/fish", ".config/gcloud", ".config/huggingface", ".config/pip", ".config/pypoetry", ".config/rclone", ".config/containers/auth.json", ".config/composer/auth.json", ".config/sops/age/keys.txt", ".gnupg", ".gsutil", ".kube", ".npmrc", ".yarnrc", ".yarnrc.yml", ".pnpmrc", ".bunfig.toml", ".pypirc", ".pip", ".poetry", ".local/share/pypoetry", ".gem/credentials", ".bundle/config", ".cargo/credentials", ".cargo/credentials.toml", ".composer/auth.json", ".condarc", ".m2/settings.xml", ".gradle/gradle.properties", ".sbt/1.0/credentials.sbt", ".terraform.d/credentials.tfrc.json", ".pulumi/credentials.json", ".oci/config", ".huggingface/token", ".cache/huggingface/token", ".claude", ".claude.json", ".kiro", ".bash_login", ".bash_logout", ".bash_profile", ".bashrc", ".bash_history", ".profile", ".zlogin", ".zlogout", ".zprofile", ".zshenv", ".zshrc", ".zsh_history", ".python_history", ".mysql_history", ".psql_history", ".env", ".env.local", "Documents", "Movies", "Music", "Pictures", "Library/Keychains", "Library/Mobile Documents", "Library/CloudStorage", "Dropbox", "Google Drive", "OneDrive", ].each do |path| next if except == :git && GIT_CREDENTIAL_PATHS.include?(path) path = home/path next unless path.exist? path = path.realpath next unless path.ascend.include?(home) if (readable_path = readable_paths.find { |required_path| required_path.ascend.include?(path) }) opoo <<~EOS The sandbox cannot prevent formulae from reading: #{path} because this required path is inside it: #{readable_path} Formulae may access personal data in this directory. EOS next end deny_read_path path rescue Errno::ENOENT nil end if except == :git GIT_CREDENTIAL_PATHS.each do |path| path = home/path allow_read(path:) if path.symlink? && path.file? end end return end deny_read_path home end |
#deny_read_path(path) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
414 415 416 |
# File 'sandbox.rb', line 414 def deny_read_path(path) deny_read path:, type: :subpath end |
#deny_write(path:, type: :literal) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
579 580 581 |
# File 'sandbox.rb', line 579 def deny_write(path:, type: :literal) add_rule allow: false, operation: "file-write*", filter: path_filter(path, type) end |
#deny_write_homebrew_repository ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
662 663 664 665 666 667 668 669 670 |
# File 'sandbox.rb', line 662 def deny_write_homebrew_repository deny_write path: HOMEBREW_BREW_FILE if (HOMEBREW_PREFIX).ascend.include?((HOMEBREW_REPOSITORY)) deny_write_path HOMEBREW_LIBRARY deny_write_path HOMEBREW_REPOSITORY/".git" else deny_write_path HOMEBREW_REPOSITORY end end |
#deny_write_path(path) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
597 598 599 |
# File 'sandbox.rb', line 597 def deny_write_path(path) deny_write path:, type: :subpath end |
#path_filter(path, type) ⇒ SandboxPathFilter
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
861 862 863 864 865 866 867 868 869 870 871 872 873 874 |
# File 'sandbox.rb', line 861 def path_filter(path, type) # Any character is allowed: the OS-specific renderer quotes paths safely # (the seatbelt renderer escapes the `"` and `\` string delimiters; the # Linux sandbox passes each path as a separate argument), so even paths # with spaces, parentheses, quotes, backslashes or newlines are expressible. filter_path = case type when :regex then path.to_s when :subpath, :literal (Pathname.new(path)).to_s else raise ArgumentError, "Invalid path filter type: #{type}" end SandboxPathFilter.new(path: filter_path, type:) end |
#protect_homebrew_state ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
Protect locks and the temporary Cellar so sandboxed steps cannot
disrupt other operations or plant kegs that pour moves into the Cellar.
Call this after allow_write_cellar: both paths are inside the
granted var tree and macOS applies the last matching rule.
643 644 645 646 647 648 649 650 651 |
# File 'sandbox.rb', line 643 def protect_homebrew_state deny_write_path HOMEBREW_TEMP_CELLAR # Landlock cannot exclude locks without preventing new directories under var. return unless self.class.full_write_isolation? deny_write_path HOMEBREW_LOCKS # Advisory locks can also be acquired through read-only descriptors. deny_read_path HOMEBREW_LOCKS end |
#record_log(file) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
384 385 386 |
# File 'sandbox.rb', line 384 def record_log(file) @logfile = file end |
#run(*args, passthrough_stdin: true, child_message_handler: nil, retain_tmp: false, debug: false) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 |
# File 'sandbox.rb', line 735 def run(*args, passthrough_stdin: true, child_message_handler: nil, retain_tmp: false, debug: false) Mktemp.new("sandbox", retain: retain_tmp, compact: true).run(chdir: false) do |staging| temporary = staging.tmpdir raise "Sandbox temporary directory is unexpectedly unset." if temporary.nil? tmpdir = temporary.to_s env = sandbox_environment(tmpdir) @start = T.let(Time.now, T.nilable(Time)) begin command = sandbox_command(args, tmpdir) # Start sandbox in a pseudoterminal to prevent access of the parent terminal. PTY.open do |controller, worker| # Set the PTY's window size to match the parent terminal. # Some formula tests are sensitive to the terminal size and fail if this is not set. winch = proc do |_sig| controller.winsize = if $stdout.tty? # We can only use IO#winsize if the IO object is a TTY. $stdout.winsize else # Otherwise, default to tput, if available. # This relies on ncurses rather than the system's ioctl. [Utils.popen_read("tput", "lines").to_i, Utils.popen_read("tput", "cols").to_i] end end write_to_pty = proc do # Don't hang if stdin is not able to be used - throw EIO instead. old_ttin = trap(:TTIN, "IGNORE") # Update the window size whenever the parent terminal's window size changes. old_winch = trap(:WINCH, &winch) winch.call(nil) if passthrough_stdin stdin_thread = Thread.new do IO.copy_stream($stdin, controller) rescue Errno::EIO # stdin is unavailable - move on. end end stdout_thread = Thread.new do copy_pty_output(controller) end Utils.safe_fork(directory: tmpdir, yield_parent: true, child_message_handler:) do |error_pipe| if error_pipe # Child side Process.setsid controller.close worker.ioctl(self.class.terminal_ioctl_request, 0) # Make this the controlling terminal. ensure_child_tty_available # Move into a non-denied directory before `exec` so subsequent # `getcwd(3)` calls (which walk every parent) never cross a # `deny_read_home` path inherited from the caller's CWD. Dir.chdir(tmpdir) worker.close_on_exec = true apply! # Map the terminal and inheritance descriptor into the sandboxed child. File.open(__FILE__) do |inheritance| exec(env, *command, INHERITANCE_FD => inheritance, in: worker, out: worker, err: worker) end else # Parent side worker.close end end rescue ChildProcessError => e raise ErrorDuringExecution.new(command, status: e.status) ensure stdin_thread&.kill stdout_thread&.kill trap(:TTIN, old_ttin) trap(:WINCH, old_winch) end if $stdin.tty? && passthrough_stdin # If stdin is a TTY, set it to a raw, passthrough mode while we # copy the input/output of the process spawned in the PTY, then # restore its original state afterwards. Keep `opost` set, unlike # `IO#raw`: clearing it stops LF -> CRLF translation for the whole # terminal, so anything written outside the PTY meanwhile (e.g. # our own `$stdout` when piped) renders staircased — and set the # mode in one `stty` call so there is no window where `opost` is # clear. begin # Ignore SIGTTOU as setting raw mode will hang if the process is in the background. old_ttou = trap(:TTOU, "IGNORE") if (tty_state = Sandbox.tty_state) begin # `-echo` matches `IO#raw`; `stty raw` alone leaves echo on. Utils.popen_read("stty", "raw", "-echo", "opost", in: :in) write_to_pty.call ensure Utils.popen_read("stty", tty_state, in: :in) end else # Cannot get the terminal state, so don't change it either. write_to_pty.call end ensure trap(:TTOU, old_ttou) end else write_to_pty.call end end # Preserve temporary files for debugging, including interrupted commands. rescue StandardError, SignalException staging.retain! if debug @failed = true raise ensure record_sandbox_log end end ensure cleanup_sandbox end |