Class: Sandbox Private

Inherits:
Object show all
Defined in:
extend/os/mac/sandbox.rb,
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

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.



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

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)


6
7
8
# File 'extend/os/mac/sandbox.rb', line 6

def self.available?
  File.executable?(SANDBOX_EXEC)
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)


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

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.



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

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.



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

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.



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

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:



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

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:



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

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:



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

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:



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

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:



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

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.



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

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.



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

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.



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

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:



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

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:



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

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.



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

def deny_write_homebrew_repository
  deny_write path: HOMEBREW_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:



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

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)


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

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:



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

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:



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
254
# File 'sandbox.rb', line 136

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