Class: Sandbox Private

Inherits:
Object show all
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.

Constant Summary collapse

SANDBOX_EXEC =

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.

"/usr/bin/sandbox-exec"

Class Method Summary collapse

Instance Method Summary collapse

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.



26
27
28
29
# File 'sandbox.rb', line 26

def initialize
  @profile = T.let(SandboxProfile.new, SandboxProfile)
  @failed = T.let(false, T::Boolean)
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)


21
22
23
# File 'sandbox.rb', line 21

def self.available?
  false
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 (String, nil) (defaults to: nil)
  • modifier (String, nil) (defaults to: nil)


37
38
39
40
# File 'sandbox.rb', line 37

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

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



125
126
127
# File 'sandbox.rb', line 125

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



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

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.



79
80
81
82
# File 'sandbox.rb', line 79

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:



115
116
117
# File 'sandbox.rb', line 115

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:



43
44
45
46
47
# File 'sandbox.rb', line 43

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:



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

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:



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

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:



55
56
57
# File 'sandbox.rb', line 55

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.



65
66
67
68
69
70
71
# File 'sandbox.rb', line 65

def allow_write_temp_and_cache
  allow_write_path "/private/tmp"
  allow_write_path "/private/var/tmp"
  allow_write path: "^/private/var/folders/[^/]+/[^/]+/[C,T]/", type: :regex
  allow_write_path HOMEBREW_TEMP
  allow_write_path HOMEBREW_CACHE
end

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

Xcode projects expect access to certain cache/archive dirs.



93
94
95
96
# File 'sandbox.rb', line 93

def allow_write_xcode
  allow_write_path "#{Dir.home(ENV.fetch("USER"))}/Library/Developer"
  allow_write_path "#{Dir.home(ENV.fetch("USER"))}/Library/Caches/org.swift.swiftpm"
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.



130
131
132
# File 'sandbox.rb', line 130

def deny_all_network
  add_rule allow: false, operation: "network*"
end

#deny_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:



120
121
122
# File 'sandbox.rb', line 120

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



50
51
52
# File 'sandbox.rb', line 50

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.



104
105
106
107
108
109
110
111
112
# File 'sandbox.rb', line 104

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:



60
61
62
# File 'sandbox.rb', line 60

def deny_write_path(path)
  deny_write path:, type: :subpath
end

#path_filter(path, type) ⇒ 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.

Parameters:

Returns:

Raises:

  • (ArgumentError)


257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'sandbox.rb', line 257

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

  case type
  when :regex   then "regex #\"#{path}\""
  when :subpath then "subpath \"#{expand_realpath(Pathname.new(path))}\""
  when :literal then "literal \"#{expand_realpath(Pathname.new(path))}\""
  else raise ArgumentError, "Invalid path filter type: #{type}"
  end
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:



32
33
34
# File 'sandbox.rb', line 32

def record_log(file)
  @logfile = T.let(file, T.nilable(T.any(String, Pathname)))
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:



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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
# File 'sandbox.rb', line 135

def run(*args)
  Dir.mktmpdir("homebrew-sandbox", HOMEBREW_TEMP) do |tmpdir|
    allow_network path: File.join(tmpdir, "socket"), type: :literal # Make sure we have access to the error pipe.

    seatbelt = File.new(File.join(tmpdir, "homebrew.sb"), "wx")
    seatbelt.write(@profile.dump)
    seatbelt.close
    @start = T.let(Time.now, T.nilable(Time))

    begin
      command = [SANDBOX_EXEC, "-f", seatbelt.path, *args]
      # 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(TIOCSCTTY, 0) # Make this the controlling terminal.
              File.open("/dev/tty", Fcntl::O_WRONLY).close # Workaround for https://developer.apple.com/forums/thread/663632
              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
      sleep 0.1 # wait for a bit to let syslog catch up the latest events.
      syslog_args = [
        "-F", "$((Time)(local)) $(Sender)[$(PID)]: $(Message)",
        "-k", "Time", "ge", @start.to_i.to_s,
        "-k", "Message", "S", "deny",
        "-k", "Sender", "kernel",
        "-o",
        "-k", "Time", "ge", @start.to_i.to_s,
        "-k", "Message", "S", "deny",
        "-k", "Sender", "sandboxd"
      ]
      logs = Utils.popen_read("syslog", *syslog_args)

      # These messages are confusing and non-fatal, so don't report them.
      logs = logs.lines.grep_v(/^.*Python\(\d+\) deny file-write.*pyc$/).join

      unless logs.empty?
        if @logfile
          File.open(@logfile, "w") do |log|
            log.write logs
            log.write "\nWe use time to filter sandbox log. Therefore, unrelated logs may be recorded.\n"
          end
        end

        if @failed && Homebrew::EnvConfig.verbose?
          ohai "Sandbox Log", logs
          $stdout.flush # without it, brew test-bot would fail to catch the log
        end
      end
    end
  end
end