William Woodruff 367629d289
utils: Use JSON to marshal child errors
Replaces our serialization of child process
errors via Marshal with JSON, preventing
unintentional or malicious code execution outside
of the build sandbox.

Additionally, adds tests for the new behavior.
2018-09-04 11:03:32 -04:00

88 lines
2.9 KiB
Ruby

require "fcntl"
require "socket"
module Utils
def self.rewrite_child_error(child_error)
error = if child_error.inner_class == ErrorDuringExecution
ErrorDuringExecution.new(child_error.inner["cmd"],
status: child_error.inner["status"],
output: child_error.inner["output"])
elsif child_error.inner_class == BuildError
# We fill `BuildError#formula` and `BuildError#options` in later,
# when we rescue this in `FormulaInstaller#build`.
BuildError.new(nil, child_error.inner["cmd"],
child_error.inner["args"], child_error.inner["env"])
else
# Everything other error in the child just becomes a RuntimeError.
RuntimeError.new(child_error.message)
end
error.set_backtrace child_error.backtrace
error
end
def self.safe_fork(&_block)
Dir.mktmpdir("homebrew", HOMEBREW_TEMP) do |tmpdir|
UNIXServer.open("#{tmpdir}/socket") do |server|
read, write = IO.pipe
pid = fork do
begin
ENV["HOMEBREW_ERROR_PIPE"] = server.path
server.close
read.close
write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
yield
rescue Exception => e # rubocop:disable Lint/RescueException
error_hash = JSON.parse e.to_json
# Special case: We need to recreate ErrorDuringExecutions
# for proper error messages and because other code expects
# to rescue them further down.
if e.is_a?(ErrorDuringExecution)
error_hash["cmd"] = e.cmd
error_hash["status"] = e.status.exitstatus
error_hash["output"] = e.output
end
write.puts error_hash.to_json
write.close
exit!
else
exit!(true)
end
end
ignore_interrupts(:quietly) do # the child will receive the interrupt and marshal it back
begin
socket = server.accept_nonblock
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
retry unless Process.waitpid(pid, Process::WNOHANG)
else
socket.send_io(write)
socket.close
end
write.close
# Each line on the error pipe contains a JSON-serialized exception.
# We read the first, since only one is necessary for a failure.
data = read.gets
read.close
Process.wait(pid) unless socket.nil?
if data && !data.empty?
error_hash = JSON.parse(data) unless data.nil? || data.empty?
e = ChildProcessError.new(error_hash)
raise rewrite_child_error(e)
end
raise Interrupt if $CHILD_STATUS.exitstatus == 130
raise "Forked child process failed: #{$CHILD_STATUS}" unless $CHILD_STATUS.success?
end
end
end
end
end