brew/Library/Homebrew/sandbox.rb
2015-08-06 17:23:56 +08:00

161 lines
3.9 KiB
Ruby

require "erb"
require "tempfile"
class Sandbox
SANDBOX_EXEC = "/usr/bin/sandbox-exec".freeze
def self.available?
OS.mac? && File.executable?(SANDBOX_EXEC)
end
# there are times the sandbox cannot be used.
def self.auto_disable?
@auto_disable ||= ARGV.interactive? || ARGV.debug?
end
def self.print_autodisable_warning
unless @printed_autodisable_warning
opoo "The sandbox cannot be used in debug or interactive mode."
@printed_autodisable_warning = true
end
end
def self.print_sandbox_message
unless @printed_sandbox_message
ohai "Using the sandbox"
@printed_sandbox_message = true
end
end
def initialize
@profile = SandboxProfile.new
end
def record_log(file)
@log = file
end
def add_rule(rule)
@profile.add_rule(rule)
end
def allow_write(path, options = {})
add_rule :allow => true, :operation => "file-write*", :filter => path_filter(path, options[:type])
end
def deny_write(path, options = {})
add_rule :allow => false, :operation => "file-write*", :filter => path_filter(path, options[:type])
end
def allow_write_path(path)
allow_write path, :type => :subpath
end
def deny_write_path(path)
deny_write path, :type => :subpath
end
def allow_write_temp_and_cache
allow_write_path "/private/tmp"
allow_write "^/private/var/folders/[^/]+/[^/]+/[C,T]/", :type => :regex
allow_write_path HOMEBREW_TEMP
allow_write_path HOMEBREW_CACHE
end
def allow_write_cellar(formula)
allow_write_path formula.rack
allow_write_path formula.etc
allow_write_path formula.var
end
def allow_write_log(formula)
allow_write_path formula.logs
end
def deny_write_homebrew_library
deny_write_path HOMEBREW_LIBRARY
deny_write_path HOMEBREW_REPOSITORY/".git"
deny_write HOMEBREW_BREW_FILE
end
def exec(*args)
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
if ARGV.verbose?
ohai "Sandbox profile:"
puts @profile.dump
end
raise
ensure
seatbelt.unlink
unless @log.nil?
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 Sender kernel
-o
-k Time ge #{@start.to_i}
-k Sender sandboxd
]
quiet_system "syslog #{syslog_args * " "} | grep deny > #{@log}"
end
end
private
def expand_realpath(path)
raise unless path.absolute?
path.exist? ? path.realpath : expand_realpath(path.parent)/path.basename
end
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
class SandboxProfile
SEATBELT_ERB = <<-EOS.undent
(version 1)
(debug deny) ; log all denied operations to /var/log/system.log
<%= rules.join("\n") %>
(allow file-write*
(literal "/dev/ptmx")
(literal "/dev/dtracehelper")
(literal "/dev/null")
(regex #"^/dev/fd/[0-9]+$")
(regex #"^/dev/ttys?[0-9]*$")
)
(deny file-write*) ; deny non-whitelist file write operations
(allow default) ; allow everything else
EOS
attr_reader :rules
def initialize
@rules = []
end
def add_rule(rule)
s = "("
s << (rule[:allow] ? "allow": "deny")
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