2015-04-09 17:42:54 +08:00
|
|
|
require "erb"
|
|
|
|
require "tempfile"
|
|
|
|
|
|
|
|
class Sandbox
|
|
|
|
SANDBOX_EXEC = "/usr/bin/sandbox-exec".freeze
|
|
|
|
|
|
|
|
def self.available?
|
2019-01-26 17:13:14 +00:00
|
|
|
OS.mac? && File.executable?(SANDBOX_EXEC)
|
2015-04-09 17:42:54 +08:00
|
|
|
end
|
|
|
|
|
2017-07-14 17:00:06 +01:00
|
|
|
def self.formula?(_formula)
|
2016-08-14 17:34:54 +01:00
|
|
|
return false unless available?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2017-07-14 17:00:06 +01:00
|
|
|
!ARGV.no_sandbox?
|
2016-08-14 17:34:54 +01:00
|
|
|
end
|
|
|
|
|
2016-08-14 17:33:05 +01:00
|
|
|
def self.test?
|
|
|
|
return false unless available?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2016-08-14 17:33:05 +01:00
|
|
|
!ARGV.no_sandbox?
|
|
|
|
end
|
|
|
|
|
2015-04-13 18:05:15 +08:00
|
|
|
def initialize
|
2015-04-09 17:42:54 +08:00
|
|
|
@profile = SandboxProfile.new
|
2015-04-13 18:05:15 +08:00
|
|
|
end
|
|
|
|
|
2015-04-16 21:41:59 +08:00
|
|
|
def record_log(file)
|
2015-08-28 17:30:14 +08:00
|
|
|
@logfile = file
|
2015-04-16 21:41:59 +08:00
|
|
|
end
|
|
|
|
|
2015-04-13 18:05:15 +08:00
|
|
|
def add_rule(rule)
|
|
|
|
@profile.add_rule(rule)
|
2015-04-09 17:42:54 +08:00
|
|
|
end
|
|
|
|
|
2015-08-03 13:09:07 +01:00
|
|
|
def allow_write(path, options = {})
|
2016-09-17 15:32:44 +01:00
|
|
|
add_rule allow: true, operation: "file-write*", filter: path_filter(path, options[:type])
|
2015-04-13 18:05:15 +08:00
|
|
|
end
|
|
|
|
|
2015-08-03 13:09:07 +01:00
|
|
|
def deny_write(path, options = {})
|
2016-09-17 15:32:44 +01:00
|
|
|
add_rule allow: false, operation: "file-write*", filter: path_filter(path, options[:type])
|
2015-04-13 18:05:15 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def allow_write_path(path)
|
2016-09-17 15:32:44 +01:00
|
|
|
allow_write path, type: :subpath
|
2015-04-13 18:05:15 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def deny_write_path(path)
|
2016-09-17 15:32:44 +01:00
|
|
|
deny_write path, type: :subpath
|
2015-04-13 18:05:15 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def allow_write_temp_and_cache
|
|
|
|
allow_write_path "/private/tmp"
|
2015-08-25 17:34:52 +01:00
|
|
|
allow_write_path "/private/var/tmp"
|
2016-09-17 15:32:44 +01:00
|
|
|
allow_write "^/private/var/folders/[^/]+/[^/]+/[C,T]/", type: :regex
|
2015-04-13 18:05:15 +08:00
|
|
|
allow_write_path HOMEBREW_TEMP
|
|
|
|
allow_write_path HOMEBREW_CACHE
|
|
|
|
end
|
|
|
|
|
2018-07-01 23:35:29 +02:00
|
|
|
def allow_cvs
|
|
|
|
allow_write_path "/Users/#{ENV["USER"]}/.cvspass"
|
|
|
|
end
|
|
|
|
|
|
|
|
def allow_fossil
|
|
|
|
allow_write_path "/Users/#{ENV["USER"]}/.fossil"
|
|
|
|
allow_write_path "/Users/#{ENV["USER"]}/.fossil-journal"
|
|
|
|
end
|
|
|
|
|
2015-04-13 18:05:15 +08:00
|
|
|
def allow_write_cellar(formula)
|
|
|
|
allow_write_path formula.rack
|
|
|
|
allow_write_path formula.etc
|
|
|
|
allow_write_path formula.var
|
|
|
|
end
|
|
|
|
|
2015-08-25 17:34:52 +01:00
|
|
|
# Xcode projects expect access to certain cache/archive dirs.
|
|
|
|
def allow_write_xcode
|
2016-09-22 05:11:41 +01:00
|
|
|
allow_write_path "/Users/#{ENV["USER"]}/Library/Developer"
|
2015-08-25 17:34:52 +01:00
|
|
|
end
|
|
|
|
|
2015-04-13 18:05:15 +08:00
|
|
|
def allow_write_log(formula)
|
2015-04-25 22:07:06 -04:00
|
|
|
allow_write_path formula.logs
|
2015-04-09 17:42:54 +08:00
|
|
|
end
|
|
|
|
|
2016-09-23 08:26:49 +01:00
|
|
|
def deny_write_homebrew_repository
|
2015-04-23 12:33:54 +08:00
|
|
|
deny_write HOMEBREW_BREW_FILE
|
2016-09-23 08:26:49 +01:00
|
|
|
if HOMEBREW_PREFIX.to_s != HOMEBREW_REPOSITORY.to_s
|
|
|
|
deny_write_path HOMEBREW_REPOSITORY
|
|
|
|
else
|
|
|
|
deny_write_path HOMEBREW_LIBRARY
|
|
|
|
deny_write_path HOMEBREW_REPOSITORY/".git"
|
|
|
|
end
|
2015-04-23 12:33:54 +08:00
|
|
|
end
|
|
|
|
|
2015-04-09 17:42:54 +08:00
|
|
|
def exec(*args)
|
2015-08-03 13:09:07 +01:00
|
|
|
seatbelt = Tempfile.new(["homebrew", ".sb"], HOMEBREW_TEMP)
|
|
|
|
seatbelt.write(@profile.dump)
|
|
|
|
seatbelt.close
|
|
|
|
@start = Time.now
|
|
|
|
safe_system SANDBOX_EXEC, "-f", seatbelt.path, *args
|
|
|
|
rescue
|
2015-08-28 17:30:14 +08:00
|
|
|
@failed = true
|
2015-08-03 13:09:07 +01:00
|
|
|
raise
|
|
|
|
ensure
|
|
|
|
seatbelt.unlink
|
2015-08-28 17:30:14 +08:00
|
|
|
sleep 0.1 # wait for a bit to let syslog catch up the latest events.
|
|
|
|
syslog_args = %W[
|
|
|
|
-F $((Time)(local))\ $(Sender)[$(PID)]:\ $(Message)
|
|
|
|
-k Time ge #{@start.to_i}
|
|
|
|
-k Message S deny
|
|
|
|
-k Sender kernel
|
|
|
|
-o
|
|
|
|
-k Time ge #{@start.to_i}
|
|
|
|
-k Message S deny
|
|
|
|
-k Sender sandboxd
|
|
|
|
]
|
|
|
|
logs = Utils.popen_read("syslog", *syslog_args)
|
2016-08-11 00:22:18 -07:00
|
|
|
|
|
|
|
# These messages are confusing and non-fatal, so don't report them.
|
2016-09-17 15:17:27 +01:00
|
|
|
logs = logs.lines.reject { |l| l.match(/^.*Python\(\d+\) deny file-write.*pyc$/) }.join
|
2016-08-11 00:22:18 -07:00
|
|
|
|
2015-08-28 17:30:14 +08:00
|
|
|
unless logs.empty?
|
|
|
|
if @logfile
|
2018-03-07 16:14:55 +00:00
|
|
|
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
|
2015-08-28 17:30:14 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
if @failed && ARGV.verbose?
|
|
|
|
ohai "Sandbox log"
|
|
|
|
puts logs
|
2015-08-29 18:59:08 +08:00
|
|
|
$stdout.flush # without it, brew test-bot would fail to catch the log
|
2015-08-28 17:30:14 +08:00
|
|
|
end
|
2015-04-09 17:42:54 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def expand_realpath(path)
|
|
|
|
raise unless path.absolute?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2015-04-09 17:42:54 +08:00
|
|
|
path.exist? ? path.realpath : expand_realpath(path.parent)/path.basename
|
|
|
|
end
|
|
|
|
|
2015-04-13 18:05:15 +08:00
|
|
|
def path_filter(path, type)
|
|
|
|
case type
|
|
|
|
when :regex then "regex \#\"#{path}\""
|
|
|
|
when :subpath then "subpath \"#{expand_realpath(Pathname.new(path))}\""
|
|
|
|
when :literal, nil then "literal \"#{expand_realpath(Pathname.new(path))}\""
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-04-09 17:42:54 +08:00
|
|
|
class SandboxProfile
|
2018-07-11 15:17:40 +02:00
|
|
|
SEATBELT_ERB = <<~ERB.freeze
|
2015-04-09 17:42:54 +08:00
|
|
|
(version 1)
|
|
|
|
(debug deny) ; log all denied operations to /var/log/system.log
|
|
|
|
<%= rules.join("\n") %>
|
|
|
|
(allow file-write*
|
2015-05-10 17:39:53 +08:00
|
|
|
(literal "/dev/ptmx")
|
2015-04-09 17:42:54 +08:00
|
|
|
(literal "/dev/dtracehelper")
|
|
|
|
(literal "/dev/null")
|
2017-07-11 01:47:36 -07:00
|
|
|
(literal "/dev/random")
|
2015-08-27 21:25:27 -07:00
|
|
|
(literal "/dev/zero")
|
2015-05-10 17:39:53 +08:00
|
|
|
(regex #"^/dev/fd/[0-9]+$")
|
|
|
|
(regex #"^/dev/ttys?[0-9]*$")
|
2015-04-09 17:42:54 +08:00
|
|
|
)
|
|
|
|
(deny file-write*) ; deny non-whitelist file write operations
|
2015-09-15 11:46:56 +08:00
|
|
|
(allow process-exec
|
|
|
|
(literal "/bin/ps")
|
|
|
|
(with no-sandbox)
|
|
|
|
) ; allow certain processes running without sandbox
|
2015-04-09 17:42:54 +08:00
|
|
|
(allow default) ; allow everything else
|
2018-07-11 15:17:40 +02:00
|
|
|
ERB
|
2015-04-09 17:42:54 +08:00
|
|
|
|
|
|
|
attr_reader :rules
|
|
|
|
|
|
|
|
def initialize
|
|
|
|
@rules = []
|
|
|
|
end
|
|
|
|
|
|
|
|
def add_rule(rule)
|
|
|
|
s = "("
|
2017-09-24 19:24:46 +01:00
|
|
|
s << (rule[:allow] ? "allow" : "deny")
|
2015-04-09 17:42:54 +08:00
|
|
|
s << " #{rule[:operation]}"
|
|
|
|
s << " (#{rule[:filter]})" if rule[:filter]
|
|
|
|
s << " (with #{rule[:modifier]})" if rule[:modifier]
|
|
|
|
s << ")"
|
|
|
|
@rules << s
|
|
|
|
end
|
|
|
|
|
|
|
|
def dump
|
|
|
|
ERB.new(SEATBELT_ERB).result(binding)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|