From 2f529220e7febc9ce4f15ba5c542073592348487 Mon Sep 17 00:00:00 2001 From: Xu Cheng Date: Thu, 9 Apr 2015 17:42:54 +0800 Subject: [PATCH] preliminary write control only sandbox Closes Homebrew/homebrew#38361. Signed-off-by: Xu Cheng --- Library/Homebrew/extend/ARGV.rb | 4 ++ Library/Homebrew/formula_installer.rb | 8 ++- Library/Homebrew/sandbox.rb | 95 +++++++++++++++++++++++++++ Library/Homebrew/test/test_sandbox.rb | 28 ++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 Library/Homebrew/sandbox.rb create mode 100644 Library/Homebrew/test/test_sandbox.rb diff --git a/Library/Homebrew/extend/ARGV.rb b/Library/Homebrew/extend/ARGV.rb index 0e2bfac518..bc64635943 100644 --- a/Library/Homebrew/extend/ARGV.rb +++ b/Library/Homebrew/extend/ARGV.rb @@ -98,6 +98,10 @@ module HomebrewArgvExtension include? '--homebrew-developer' or !ENV['HOMEBREW_DEVELOPER'].nil? end + def sandbox? + include?("--sandbox") || !ENV["HOMEBREW_SANDBOX"].nil? + end + def ignore_deps? include? '--ignore-dependencies' end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 38bf169964..c87972d2e3 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -13,6 +13,7 @@ require 'hooks/bottles' require 'debrew' require 'fcntl' require 'socket' +require 'sandbox' class FormulaInstaller include FormulaCellarChecks @@ -484,7 +485,12 @@ class FormulaInstaller server.close read.close write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) - exec(*args) + if Sandbox.available? && ARGV.sandbox? + sandbox = Sandbox.new(formula) + sandbox.exec(*args) + else + exec(*args) + end rescue Exception => e Marshal.dump(e, write) write.close diff --git a/Library/Homebrew/sandbox.rb b/Library/Homebrew/sandbox.rb new file mode 100644 index 0000000000..75d62ccf29 --- /dev/null +++ b/Library/Homebrew/sandbox.rb @@ -0,0 +1,95 @@ +require "erb" +require "tempfile" + +class Sandbox + SANDBOX_EXEC = "/usr/bin/sandbox-exec".freeze + + def self.available? + OS.mac? && File.executable?(SANDBOX_EXEC) + end + + def initialize(formula=nil) + @profile = SandboxProfile.new + unless formula.nil? + allow_write "/private/tmp", :type => :subpath + allow_write "/private/var/folders", :type => :subpath + allow_write HOMEBREW_TEMP, :type => :subpath + allow_write HOMEBREW_LOGS/formula.name, :type => :subpath + allow_write HOMEBREW_CACHE, :type => :subpath + allow_write formula.rack, :type => :subpath + allow_write formula.etc, :type => :subpath + allow_write formula.var, :type => :subpath + end + end + + def allow_write(path, options={}) + case options[:type] + when :regex then filter = "regex \#\"#{path}\"" + when :subpath then filter = "subpath \"#{expand_realpath(Pathname.new(path))}\"" + when :literal, nil then filter = "literal \"#{expand_realpath(Pathname.new(path))}\"" + end + @profile.add_rule :allow => true, + :operation => "file-write*", + :filter => filter + end + + def exec(*args) + begin + seatbelt = Tempfile.new(["homebrew", ".sb"], HOMEBREW_TEMP) + seatbelt.write(@profile.dump) + seatbelt.close + safe_system SANDBOX_EXEC, "-f", seatbelt.path, *args + rescue + if ARGV.verbose? + ohai "Sandbox profile:" + puts @profile.dump + end + raise + ensure + seatbelt.unlink + end + end + + private + + def expand_realpath(path) + raise unless path.absolute? + path.exist? ? path.realpath : expand_realpath(path.parent)/path.basename + 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/dtracehelper") + (literal "/dev/null") + (regex #"^/dev/fd/\\d+$") + (regex #"^/dev/tty\\d*$") + ) + (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 diff --git a/Library/Homebrew/test/test_sandbox.rb b/Library/Homebrew/test/test_sandbox.rb new file mode 100644 index 0000000000..4564edb3bd --- /dev/null +++ b/Library/Homebrew/test/test_sandbox.rb @@ -0,0 +1,28 @@ +require "testing_env" +require "sandbox" + +class SandboxTest < Homebrew::TestCase + def setup + skip "sandbox not implemented" unless Sandbox.available? + end + + def test_allow_write + s = Sandbox.new + testpath = Pathname.new(TEST_TMPDIR) + foo = testpath/"foo" + s.allow_write "#{testpath}", :type => :subpath + s.exec "touch", foo + assert_predicate foo, :exist? + foo.unlink + end + + def test_deny_write + s = Sandbox.new + testpath = Pathname.new(TEST_TMPDIR) + bar = testpath/"bar" + shutup do + assert_raises(ErrorDuringExecution) { s.exec "touch", bar } + end + refute_predicate bar, :exist? + end +end