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
- #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
-
#deny_write_temp_cellar ⇒ void
private
Deny writes to the download queue's temporary Cellar so sandboxed steps cannot plant kegs or markers that
pourwould move into the Cellar. - #initialize ⇒ void constructor private
- #path_filter(path, type) ⇒ SandboxPathFilter private
- #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_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.
375 376 377 378 379 380 |
# File 'sandbox.rb', line 375 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.
860 861 862 |
# File 'sandbox.rb', line 860 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.
238 239 240 241 242 243 244 245 246 |
# File 'sandbox.rb', line 238 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.
279 280 281 282 283 |
# File 'sandbox.rb', line 279 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.
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'sandbox.rb', line 333 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.
351 352 353 |
# File 'sandbox.rb', line 351 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.
325 326 327 328 329 330 |
# File 'sandbox.rb', line 325 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.
320 321 322 |
# File 'sandbox.rb', line 320 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.
356 357 358 |
# File 'sandbox.rb', line 356 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.
291 292 293 294 295 |
# File 'sandbox.rb', line 291 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 |
# 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 (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.
298 |
# File 'sandbox.rb', line 298 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.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'sandbox.rb', line 301 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.
286 287 288 |
# File 'sandbox.rb', line 286 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.
361 362 363 |
# File 'sandbox.rb', line 361 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.
369 370 371 372 |
# File 'sandbox.rb', line 369 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.
251 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 |
# File 'sandbox.rb', line 251 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.
602 603 604 605 606 607 |
# File 'sandbox.rb', line 602 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.
391 392 393 394 |
# File 'sandbox.rb', line 391 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.
610 611 612 |
# File 'sandbox.rb', line 610 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.
615 616 617 618 |
# File 'sandbox.rb', line 615 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.
656 657 658 |
# File 'sandbox.rb', line 656 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.
402 403 404 405 |
# File 'sandbox.rb', line 402 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.
397 398 399 |
# File 'sandbox.rb', line 397 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.
553 554 555 556 557 558 |
# File 'sandbox.rb', line 553 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.
561 562 563 564 565 |
# File 'sandbox.rb', line 561 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.
621 622 623 624 625 |
# File 'sandbox.rb', line 621 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.
640 641 642 |
# File 'sandbox.rb', line 640 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.
573 574 575 |
# File 'sandbox.rb', line 573 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.
578 579 580 581 582 583 |
# File 'sandbox.rb', line 578 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.
591 592 593 |
# File 'sandbox.rb', line 591 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.
596 597 598 599 |
# File 'sandbox.rb', line 596 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.
637 |
# File 'sandbox.rb', line 637 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.
701 |
# File 'sandbox.rb', line 701 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.
704 |
# File 'sandbox.rb', line 704 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.
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 |
# File 'sandbox.rb', line 676 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.
707 |
# File 'sandbox.rb', line 707 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.
863 864 865 866 867 868 |
# File 'sandbox.rb', line 863 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 |
#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.
661 662 663 |
# File 'sandbox.rb', line 661 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.
408 409 410 |
# File 'sandbox.rb', line 408 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.
418 419 420 421 422 423 424 425 426 427 428 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 |
# File 'sandbox.rb', line 418 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.
413 414 415 |
# File 'sandbox.rb', line 413 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.
568 569 570 |
# File 'sandbox.rb', line 568 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.
645 646 647 648 649 650 651 652 653 |
# File 'sandbox.rb', line 645 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.
586 587 588 |
# File 'sandbox.rb', line 586 def deny_write_path(path) deny_write path:, type: :subpath end |
#deny_write_temp_cellar ⇒ 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.
Deny writes to the download queue's temporary Cellar so sandboxed steps
cannot plant kegs or markers that pour would move into the Cellar. Call
this after allow_write_cellar: the temporary Cellar is inside the
granted var tree and macOS applies the last matching rule.
632 633 634 |
# File 'sandbox.rb', line 632 def deny_write_temp_cellar deny_write_path HOMEBREW_TEMP_CELLAR 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.
844 845 846 847 848 849 850 851 852 853 854 855 856 857 |
# File 'sandbox.rb', line 844 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 |
#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.
383 384 385 |
# File 'sandbox.rb', line 383 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.
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 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 |
# File 'sandbox.rb', line 718 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 |