Class: Sandbox Private

Inherits:
Object show all
Includes:
OS::Mac::Sandbox, Utils::Output::Mixin
Defined in:
sandbox.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.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::Output::Mixin

#issue_reporting_message, #odebug, #odeprecated, #odie, #odisabled, #ofail, #oh1, #oh1_title, #ohai, #ohai_title, #onoe, #opoo, #opoo_outside_github_actions, #pretty_deprecated, #pretty_disabled, #pretty_duration, #pretty_install_status, #pretty_installed, #pretty_outdated, #pretty_uninstalled, #pretty_upgradable

Constructor Details

#initializevoid

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.



83
84
85
86
87
88
# File 'sandbox.rb', line 83

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

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.

Returns:

  • (Boolean)


73
74
75
# File 'sandbox.rb', line 73

def self.available?
  false
end

.terminal_ioctl_requestInteger

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.

Returns:

Raises:

  • (NotImplementedError)


78
79
80
# File 'sandbox.rb', line 78

def self.terminal_ioctl_request
  raise NotImplementedError, "Sandbox is not implemented for this OS."
end

Instance Method Details

#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.

Parameters:

  • allow (Boolean)
  • operation (String)
  • filter (SandboxPathFilter, nil) (defaults to: nil)
  • modifier (String, nil) (defaults to: nil)


99
100
101
102
# File 'sandbox.rb', line 99

def add_rule(allow:, operation:, filter: nil, modifier: nil)
  rule = SandboxRule.new(allow:, operation:, filter:, modifier:)
  @profile.add_rule(rule)
end

#allow_cvsvoid

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.



133
134
135
# File 'sandbox.rb', line 133

def allow_cvs
  allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.cvspass"
end

#allow_fossilvoid

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.



138
139
140
141
# File 'sandbox.rb', line 138

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.

Parameters:



170
171
172
# File 'sandbox.rb', line 170

def allow_network(path:, type: :literal)
  add_rule allow: true, operation: "network*", filter: path_filter(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.

Parameters:



105
106
107
108
109
# File 'sandbox.rb', line 105

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.

Parameters:



144
145
146
147
148
# File 'sandbox.rb', line 144

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.

Parameters:



154
155
156
# File 'sandbox.rb', line 154

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.

Parameters:



117
118
119
# File 'sandbox.rb', line 117

def allow_write_path(path)
  allow_write path:, type: :subpath
end

#allow_write_temp_and_cachevoid

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.



127
128
129
130
# File 'sandbox.rb', line 127

def allow_write_temp_and_cache
  allow_write_path HOMEBREW_TEMP
  allow_write_path HOMEBREW_CACHE
end

#allow_write_xcodevoid

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.



151
# File 'sandbox.rb', line 151

def allow_write_xcode; end

#deny_all_networkvoid

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.



175
176
177
# File 'sandbox.rb', line 175

def deny_all_network
  add_rule allow: false, operation: "network*"
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.

Parameters:



112
113
114
# File 'sandbox.rb', line 112

def deny_write(path:, type: :literal)
  add_rule allow: false, operation: "file-write*", filter: path_filter(path, type)
end

#deny_write_homebrew_repositoryvoid

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 deny_write_homebrew_repository
  deny_write path: HOMEBREW_ORIGINAL_BREW_FILE
  if HOMEBREW_PREFIX.to_s == HOMEBREW_REPOSITORY.to_s
    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.

Parameters:



122
123
124
# File 'sandbox.rb', line 122

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.

Parameters:

Returns:

  • (SandboxPathFilter)

Raises:

  • (ArgumentError)


272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'sandbox.rb', line 272

def path_filter(path, type)
  invalid_char = ['"', "'", "(", ")", "\n", "\\"].find do |c|
    path.to_s.include?(c)
  end
  raise ArgumentError, "Invalid character '#{invalid_char}' in path: #{path}" if invalid_char

  filter_path = case type
  when :regex   then path.to_s
  when :subpath, :literal
    expand_realpath(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.

Parameters:



91
92
93
# File 'sandbox.rb', line 91

def record_log(file)
  @logfile = file
end

#run(*args) ⇒ 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.

Parameters:



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'sandbox.rb', line 180

def run(*args)
  Dir.mktmpdir("homebrew-sandbox", HOMEBREW_TEMP) do |tmpdir|
    allow_network path: File.join(tmpdir, "socket"), type: :literal if allow_network_for_error_pipe?
    @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)

          stdin_thread = Thread.new do
            IO.copy_stream($stdin, controller)
          rescue Errno::EIO
            # stdin is unavailable - move on.
          end

          stdout_thread = Thread.new do
            controller.each_char { |c| print(c) }
          end

          Utils.safe_fork(directory: tmpdir, yield_parent: true) 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

              worker.close_on_exec = true
              exec(*command, in: worker, out: worker, err: worker) # And map everything to the PTY.
            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?
          # If stdin is a TTY, use io.raw to set stdin to a raw, passthrough
          # mode while we copy the input/output of the process spawned in the
          # PTY. After we've finished copying to/from the PTY process, io.raw
          # will restore the stdin TTY to its original state.
          begin
            # Ignore SIGTTOU as setting raw mode will hang if the process is in the background.
            old_ttou = trap(:TTOU, "IGNORE")
            $stdin.raw(&write_to_pty)
          ensure
            trap(:TTOU, old_ttou)
          end
        else
          write_to_pty.call
        end
      end
    rescue
      @failed = true
      raise
    ensure
      record_sandbox_log
    end
  end
end