| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | require "erb" | 
					
						
							|  |  |  | require "tempfile" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Sandbox | 
					
						
							|  |  |  |   SANDBOX_EXEC = "/usr/bin/sandbox-exec".freeze | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.available? | 
					
						
							|  |  |  |     OS.mac? && File.executable?(SANDBOX_EXEC) | 
					
						
							|  |  |  |   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) | 
					
						
							|  |  |  |     @log = file | 
					
						
							|  |  |  |   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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def allow_write(path, options={}) | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |     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) | 
					
						
							| 
									
										
										
										
											2015-04-25 22:07:06 -04:00
										 |  |  |     allow_write_path formula.logs | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-23 12:33:54 +08:00
										 |  |  |   def deny_write_homebrew_library | 
					
						
							|  |  |  |     deny_write_path HOMEBREW_LIBRARY | 
					
						
							|  |  |  |     deny_write_path HOMEBREW_REPOSITORY/".git" | 
					
						
							|  |  |  |     deny_write HOMEBREW_BREW_FILE | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   def exec(*args) | 
					
						
							|  |  |  |     begin | 
					
						
							|  |  |  |       seatbelt = Tempfile.new(["homebrew", ".sb"], HOMEBREW_TEMP) | 
					
						
							|  |  |  |       seatbelt.write(@profile.dump) | 
					
						
							|  |  |  |       seatbelt.close | 
					
						
							| 
									
										
										
										
											2015-04-16 21:41:59 +08:00
										 |  |  |       @start = Time.now | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |       safe_system SANDBOX_EXEC, "-f", seatbelt.path, *args | 
					
						
							|  |  |  |     rescue | 
					
						
							|  |  |  |       if ARGV.verbose? | 
					
						
							|  |  |  |         ohai "Sandbox profile:" | 
					
						
							|  |  |  |         puts @profile.dump | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       raise | 
					
						
							|  |  |  |     ensure | 
					
						
							|  |  |  |       seatbelt.unlink | 
					
						
							| 
									
										
										
										
											2015-04-16 21:41:59 +08:00
										 |  |  |       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.to_s} | 
					
						
							|  |  |  |           -k Sender kernel | 
					
						
							|  |  |  |           -o | 
					
						
							|  |  |  |           -k Time ge #{@start.to_i.to_s} | 
					
						
							|  |  |  |           -k Sender sandboxd | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         quiet_system "syslog #{syslog_args * " "} | grep deny > #{@log}" | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def expand_realpath(path) | 
					
						
							|  |  |  |     raise unless path.absolute? | 
					
						
							|  |  |  |     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 | 
					
						
							|  |  |  |     SEATBELT_ERB = <<-EOS.undent
 | 
					
						
							|  |  |  |       (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") | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |       (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 |