Fix misuse of fork in sandbox causing crashes
This commit is contained in:
parent
45be393f95
commit
6a0db5035f
@ -81,21 +81,21 @@ module Homebrew
|
|||||||
|
|
||||||
exec_args << "--HEAD" if f.head?
|
exec_args << "--HEAD" if f.head?
|
||||||
|
|
||||||
Utils.safe_fork do |error_pipe|
|
if Sandbox.available?
|
||||||
if Sandbox.available?
|
sandbox = Sandbox.new
|
||||||
sandbox = Sandbox.new
|
f.logs.mkpath
|
||||||
f.logs.mkpath
|
sandbox.record_log(f.logs/"test.sandbox.log")
|
||||||
sandbox.record_log(f.logs/"test.sandbox.log")
|
sandbox.allow_write_temp_and_cache
|
||||||
sandbox.allow_write_temp_and_cache
|
sandbox.allow_write_log(f)
|
||||||
sandbox.allow_write_log(f)
|
sandbox.allow_write_xcode
|
||||||
sandbox.allow_write_xcode
|
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/cache")
|
||||||
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/cache")
|
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/homebrew/locks")
|
||||||
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/homebrew/locks")
|
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/log")
|
||||||
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/log")
|
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/run")
|
||||||
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/run")
|
sandbox.deny_all_network unless f.class.network_access_allowed?(:test)
|
||||||
sandbox.deny_all_network_except_pipe(error_pipe) unless f.class.network_access_allowed?(:test)
|
sandbox.run(*exec_args)
|
||||||
sandbox.exec(*exec_args)
|
else
|
||||||
else
|
Utils.safe_fork do
|
||||||
exec(*exec_args)
|
exec(*exec_args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -755,21 +755,14 @@ class BottleFormulaUnavailableError < RuntimeError
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Raised when a child process sends us an exception over its error pipe.
|
# Raised when a `Utils.safe_fork` exits with a non-zero code.
|
||||||
class ChildProcessError < RuntimeError
|
class ChildProcessError < RuntimeError
|
||||||
attr_reader :inner, :inner_class
|
attr_reader :status
|
||||||
|
|
||||||
def initialize(inner)
|
def initialize(status)
|
||||||
@inner = inner
|
@status = status
|
||||||
@inner_class = Object.const_get inner["json_class"]
|
|
||||||
|
|
||||||
super <<~EOS
|
super "Forked child process failed: #{status}"
|
||||||
An exception occurred within a child process:
|
|
||||||
#{inner_class}: #{inner["m"]}
|
|
||||||
EOS
|
|
||||||
|
|
||||||
# Clobber our real (but irrelevant) backtrace with that of the inner exception.
|
|
||||||
set_backtrace inner["b"]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -931,21 +931,21 @@ on_request: installed_on_request?, options:)
|
|||||||
formula.specified_path,
|
formula.specified_path,
|
||||||
].concat(build_argv)
|
].concat(build_argv)
|
||||||
|
|
||||||
Utils.safe_fork do |error_pipe|
|
if Sandbox.available?
|
||||||
if Sandbox.available?
|
sandbox = Sandbox.new
|
||||||
sandbox = Sandbox.new
|
formula.logs.mkpath
|
||||||
formula.logs.mkpath
|
sandbox.record_log(formula.logs/"build.sandbox.log")
|
||||||
sandbox.record_log(formula.logs/"build.sandbox.log")
|
sandbox.allow_write_path(Dir.home) if interactive?
|
||||||
sandbox.allow_write_path(Dir.home) if interactive?
|
sandbox.allow_write_temp_and_cache
|
||||||
sandbox.allow_write_temp_and_cache
|
sandbox.allow_write_log(formula)
|
||||||
sandbox.allow_write_log(formula)
|
sandbox.allow_cvs
|
||||||
sandbox.allow_cvs
|
sandbox.allow_fossil
|
||||||
sandbox.allow_fossil
|
sandbox.allow_write_xcode
|
||||||
sandbox.allow_write_xcode
|
sandbox.allow_write_cellar(formula)
|
||||||
sandbox.allow_write_cellar(formula)
|
sandbox.deny_all_network unless formula.network_access_allowed?(:build)
|
||||||
sandbox.deny_all_network_except_pipe(error_pipe) unless formula.network_access_allowed?(:build)
|
sandbox.run(*args)
|
||||||
sandbox.exec(*args)
|
else
|
||||||
else
|
Utils.safe_fork do
|
||||||
exec(*args)
|
exec(*args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1161,22 +1161,22 @@ on_request: installed_on_request?, options:)
|
|||||||
|
|
||||||
args << post_install_formula_path
|
args << post_install_formula_path
|
||||||
|
|
||||||
Utils.safe_fork do |error_pipe|
|
if Sandbox.available?
|
||||||
if Sandbox.available?
|
sandbox = Sandbox.new
|
||||||
sandbox = Sandbox.new
|
formula.logs.mkpath
|
||||||
formula.logs.mkpath
|
sandbox.record_log(formula.logs/"postinstall.sandbox.log")
|
||||||
sandbox.record_log(formula.logs/"postinstall.sandbox.log")
|
sandbox.allow_write_temp_and_cache
|
||||||
sandbox.allow_write_temp_and_cache
|
sandbox.allow_write_log(formula)
|
||||||
sandbox.allow_write_log(formula)
|
sandbox.allow_write_xcode
|
||||||
sandbox.allow_write_xcode
|
sandbox.deny_write_homebrew_repository
|
||||||
sandbox.deny_write_homebrew_repository
|
sandbox.allow_write_cellar(formula)
|
||||||
sandbox.allow_write_cellar(formula)
|
sandbox.deny_all_network unless formula.network_access_allowed?(:postinstall)
|
||||||
sandbox.deny_all_network_except_pipe(error_pipe) unless formula.network_access_allowed?(:postinstall)
|
Keg::KEG_LINK_DIRECTORIES.each do |dir|
|
||||||
Keg::KEG_LINK_DIRECTORIES.each do |dir|
|
sandbox.allow_write_path "#{HOMEBREW_PREFIX}/#{dir}"
|
||||||
sandbox.allow_write_path "#{HOMEBREW_PREFIX}/#{dir}"
|
end
|
||||||
end
|
sandbox.run(*args)
|
||||||
sandbox.exec(*args)
|
else
|
||||||
else
|
Utils.safe_fork do
|
||||||
exec(*args)
|
exec(*args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -5,12 +5,19 @@ require "erb"
|
|||||||
require "io/console"
|
require "io/console"
|
||||||
require "pty"
|
require "pty"
|
||||||
require "tempfile"
|
require "tempfile"
|
||||||
|
require "utils/fork"
|
||||||
|
|
||||||
# Helper class for running a sub-process inside of a sandboxed environment.
|
# Helper class for running a sub-process inside of a sandboxed environment.
|
||||||
class Sandbox
|
class Sandbox
|
||||||
SANDBOX_EXEC = "/usr/bin/sandbox-exec"
|
SANDBOX_EXEC = "/usr/bin/sandbox-exec"
|
||||||
private_constant :SANDBOX_EXEC
|
private_constant :SANDBOX_EXEC
|
||||||
|
|
||||||
|
# This is defined in the macOS SDK but Ruby unfortunately does not expose it.
|
||||||
|
# This value can be found by compiling a C program that prints TIOCSCTTY.
|
||||||
|
# The value is different on Linux but that's not a problem as we only support macOS in this file.
|
||||||
|
TIOCSCTTY = 0x20007461
|
||||||
|
private_constant :TIOCSCTTY
|
||||||
|
|
||||||
sig { returns(T::Boolean) }
|
sig { returns(T::Boolean) }
|
||||||
def self.available?
|
def self.available?
|
||||||
false
|
false
|
||||||
@ -125,108 +132,122 @@ class Sandbox
|
|||||||
add_rule allow: false, operation: "network*"
|
add_rule allow: false, operation: "network*"
|
||||||
end
|
end
|
||||||
|
|
||||||
sig { params(path: T.any(String, Pathname)).void }
|
|
||||||
def deny_all_network_except_pipe(path)
|
|
||||||
deny_all_network
|
|
||||||
allow_network path:, type: :literal
|
|
||||||
end
|
|
||||||
|
|
||||||
sig { params(args: T.any(String, Pathname)).void }
|
sig { params(args: T.any(String, Pathname)).void }
|
||||||
def exec(*args)
|
def run(*args)
|
||||||
seatbelt = Tempfile.new(["homebrew", ".sb"], HOMEBREW_TEMP)
|
Dir.mktmpdir("homebrew-sandbox", HOMEBREW_TEMP) do |tmpdir|
|
||||||
seatbelt.write(@profile.dump)
|
allow_network path: File.join(tmpdir, "socket"), type: :literal # Make sure we have access to the error pipe.
|
||||||
seatbelt.close
|
|
||||||
@start = T.let(Time.now, T.nilable(Time))
|
|
||||||
|
|
||||||
begin
|
seatbelt = File.new(File.join(tmpdir, "homebrew.sb"), "wx")
|
||||||
command = [SANDBOX_EXEC, "-f", seatbelt.path, *args]
|
seatbelt.write(@profile.dump)
|
||||||
# Start sandbox in a pseudoterminal to prevent access of the parent terminal.
|
seatbelt.close
|
||||||
PTY.spawn(*command) do |r, w, pid|
|
@start = T.let(Time.now, T.nilable(Time))
|
||||||
# Set the PTY's window size to match the parent terminal.
|
|
||||||
# Some formula tests are sensitive to the terminal size and fail if this is not set.
|
|
||||||
winch = proc do |_sig|
|
|
||||||
w.winsize = if $stdout.tty?
|
|
||||||
# We can only use IO#winsize if the IO object is a TTY.
|
|
||||||
$stdout.winsize
|
|
||||||
else
|
|
||||||
# Otherwise, default to tput, if available.
|
|
||||||
# This relies on ncurses rather than the system's ioctl.
|
|
||||||
[Utils.popen_read("tput", "lines").to_i, Utils.popen_read("tput", "cols").to_i]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
write_to_pty = proc do
|
begin
|
||||||
# Don't hang if stdin is not able to be used - throw EIO instead.
|
command = [SANDBOX_EXEC, "-f", seatbelt.path, *args]
|
||||||
old_ttin = trap(:TTIN, "IGNORE")
|
# Start sandbox in a pseudoterminal to prevent access of the parent terminal.
|
||||||
|
PTY.open do |controller, worker|
|
||||||
# Update the window size whenever the parent terminal's window size changes.
|
# Set the PTY's window size to match the parent terminal.
|
||||||
old_winch = trap(:WINCH, &winch)
|
# Some formula tests are sensitive to the terminal size and fail if this is not set.
|
||||||
winch.call(nil)
|
winch = proc do |_sig|
|
||||||
|
controller.winsize = if $stdout.tty?
|
||||||
stdin_thread = Thread.new do
|
# We can only use IO#winsize if the IO object is a TTY.
|
||||||
IO.copy_stream($stdin, w)
|
$stdout.winsize
|
||||||
rescue Errno::EIO
|
else
|
||||||
# stdin is unavailable - move on.
|
# Otherwise, default to tput, if available.
|
||||||
|
# This relies on ncurses rather than the system's ioctl.
|
||||||
|
[Utils.popen_read("tput", "lines").to_i, Utils.popen_read("tput", "cols").to_i]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
r.each_char { |c| print(c) }
|
write_to_pty = proc do
|
||||||
|
# Don't hang if stdin is not able to be used - throw EIO instead.
|
||||||
|
old_ttin = trap(:TTIN, "IGNORE")
|
||||||
|
|
||||||
Process.wait(pid)
|
# Update the window size whenever the parent terminal's window size changes.
|
||||||
ensure
|
old_winch = trap(:WINCH, &winch)
|
||||||
stdin_thread&.kill
|
winch.call(nil)
|
||||||
trap(:TTIN, old_ttin)
|
|
||||||
trap(:WINCH, old_winch)
|
|
||||||
end
|
|
||||||
|
|
||||||
if $stdin.tty?
|
stdin_thread = Thread.new do
|
||||||
# If stdin is a TTY, use io.raw to set stdin to a raw, passthrough
|
IO.copy_stream($stdin, controller)
|
||||||
# mode while we copy the input/output of the process spawned in the
|
rescue Errno::EIO
|
||||||
# PTY. After we've finished copying to/from the PTY process, io.raw
|
# stdin is unavailable - move on.
|
||||||
# will restore the stdin TTY to its original state.
|
end
|
||||||
begin
|
|
||||||
# Ignore SIGTTOU as setting raw mode will hang if the process is in the background.
|
stdout_thread = Thread.new do
|
||||||
old_ttou = trap(:TTOU, "IGNORE")
|
controller.each_char { |c| print(c) }
|
||||||
$stdin.raw(&write_to_pty)
|
end
|
||||||
|
|
||||||
|
Utils.safe_fork(directory: tmpdir, yield_parent: true) do |error_pipe|
|
||||||
|
if error_pipe
|
||||||
|
# Child side
|
||||||
|
Process.setsid
|
||||||
|
controller.close
|
||||||
|
worker.ioctl(TIOCSCTTY, 0) # Make this the controlling terminal.
|
||||||
|
File.open("/dev/tty", Fcntl::O_WRONLY).close # Workaround for https://developer.apple.com/forums/thread/663632
|
||||||
|
worker.close_on_exec = true
|
||||||
|
exec(*command, in: worker, out: worker, err: worker) # And map everything to the PTY.
|
||||||
|
else
|
||||||
|
# Parent side
|
||||||
|
worker.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue ChildProcessError => e
|
||||||
|
raise ErrorDuringExecution.new(command, status: e.status)
|
||||||
ensure
|
ensure
|
||||||
trap(:TTOU, old_ttou)
|
stdin_thread&.kill
|
||||||
|
stdout_thread&.kill
|
||||||
|
trap(:TTIN, old_ttin)
|
||||||
|
trap(:WINCH, old_winch)
|
||||||
end
|
end
|
||||||
else
|
|
||||||
write_to_pty.call
|
|
||||||
end
|
|
||||||
end
|
|
||||||
raise ErrorDuringExecution.new(command, status: $CHILD_STATUS) unless $CHILD_STATUS.success?
|
|
||||||
rescue
|
|
||||||
@failed = true
|
|
||||||
raise
|
|
||||||
ensure
|
|
||||||
seatbelt.unlink
|
|
||||||
sleep 0.1 # wait for a bit to let syslog catch up the latest events.
|
|
||||||
syslog_args = [
|
|
||||||
"-F", "$((Time)(local)) $(Sender)[$(PID)]: $(Message)",
|
|
||||||
"-k", "Time", "ge", @start.to_i.to_s,
|
|
||||||
"-k", "Message", "S", "deny",
|
|
||||||
"-k", "Sender", "kernel",
|
|
||||||
"-o",
|
|
||||||
"-k", "Time", "ge", @start.to_i.to_s,
|
|
||||||
"-k", "Message", "S", "deny",
|
|
||||||
"-k", "Sender", "sandboxd"
|
|
||||||
]
|
|
||||||
logs = Utils.popen_read("syslog", *syslog_args)
|
|
||||||
|
|
||||||
# These messages are confusing and non-fatal, so don't report them.
|
if $stdin.tty?
|
||||||
logs = logs.lines.grep_v(/^.*Python\(\d+\) deny file-write.*pyc$/).join
|
# If stdin is a TTY, use io.raw to set stdin to a raw, passthrough
|
||||||
|
# mode while we copy the input/output of the process spawned in the
|
||||||
unless logs.empty?
|
# PTY. After we've finished copying to/from the PTY process, io.raw
|
||||||
if @logfile
|
# will restore the stdin TTY to its original state.
|
||||||
File.open(@logfile, "w") do |log|
|
begin
|
||||||
log.write logs
|
# Ignore SIGTTOU as setting raw mode will hang if the process is in the background.
|
||||||
log.write "\nWe use time to filter sandbox log. Therefore, unrelated logs may be recorded.\n"
|
old_ttou = trap(:TTOU, "IGNORE")
|
||||||
|
$stdin.raw(&write_to_pty)
|
||||||
|
ensure
|
||||||
|
trap(:TTOU, old_ttou)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
write_to_pty.call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
rescue
|
||||||
|
@failed = true
|
||||||
|
raise
|
||||||
|
ensure
|
||||||
|
sleep 0.1 # wait for a bit to let syslog catch up the latest events.
|
||||||
|
syslog_args = [
|
||||||
|
"-F", "$((Time)(local)) $(Sender)[$(PID)]: $(Message)",
|
||||||
|
"-k", "Time", "ge", @start.to_i.to_s,
|
||||||
|
"-k", "Message", "S", "deny",
|
||||||
|
"-k", "Sender", "kernel",
|
||||||
|
"-o",
|
||||||
|
"-k", "Time", "ge", @start.to_i.to_s,
|
||||||
|
"-k", "Message", "S", "deny",
|
||||||
|
"-k", "Sender", "sandboxd"
|
||||||
|
]
|
||||||
|
logs = Utils.popen_read("syslog", *syslog_args)
|
||||||
|
|
||||||
if @failed && Homebrew::EnvConfig.verbose?
|
# These messages are confusing and non-fatal, so don't report them.
|
||||||
ohai "Sandbox Log", logs
|
logs = logs.lines.grep_v(/^.*Python\(\d+\) deny file-write.*pyc$/).join
|
||||||
$stdout.flush # without it, brew test-bot would fail to catch the log
|
|
||||||
|
unless logs.empty?
|
||||||
|
if @logfile
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
if @failed && Homebrew::EnvConfig.verbose?
|
||||||
|
ohai "Sandbox Log", logs
|
||||||
|
$stdout.flush # without it, brew test-bot would fail to catch the log
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -16,7 +16,7 @@ RSpec.describe Sandbox, :needs_macos do
|
|||||||
|
|
||||||
specify "#allow_write" do
|
specify "#allow_write" do
|
||||||
sandbox.allow_write path: file
|
sandbox.allow_write path: file
|
||||||
sandbox.exec "touch", file
|
sandbox.run "touch", file
|
||||||
|
|
||||||
expect(file).to exist
|
expect(file).to exist
|
||||||
end
|
end
|
||||||
@ -65,10 +65,10 @@ RSpec.describe Sandbox, :needs_macos do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#exec" do
|
describe "#run" do
|
||||||
it "fails when writing to file not specified with ##allow_write" do
|
it "fails when writing to file not specified with ##allow_write" do
|
||||||
expect do
|
expect do
|
||||||
sandbox.exec "touch", file
|
sandbox.run "touch", file
|
||||||
end.to raise_error(ErrorDuringExecution)
|
end.to raise_error(ErrorDuringExecution)
|
||||||
|
|
||||||
expect(file).not_to exist
|
expect(file).not_to exist
|
||||||
@ -80,7 +80,7 @@ RSpec.describe Sandbox, :needs_macos do
|
|||||||
allow(Utils).to receive(:popen_read).and_call_original
|
allow(Utils).to receive(:popen_read).and_call_original
|
||||||
allow(Utils).to receive(:popen_read).with("syslog", any_args).and_return("foo")
|
allow(Utils).to receive(:popen_read).with("syslog", any_args).and_return("foo")
|
||||||
|
|
||||||
expect { sandbox.exec "false" }
|
expect { sandbox.run "false" }
|
||||||
.to raise_error(ErrorDuringExecution)
|
.to raise_error(ErrorDuringExecution)
|
||||||
.and output(/foo/).to_stdout
|
.and output(/foo/).to_stdout
|
||||||
end
|
end
|
||||||
@ -96,7 +96,7 @@ RSpec.describe Sandbox, :needs_macos do
|
|||||||
allow(Utils).to receive(:popen_read).and_call_original
|
allow(Utils).to receive(:popen_read).and_call_original
|
||||||
allow(Utils).to receive(:popen_read).with("syslog", any_args).and_return(with_bogus_error)
|
allow(Utils).to receive(:popen_read).with("syslog", any_args).and_return(with_bogus_error)
|
||||||
|
|
||||||
expect { sandbox.exec "false" }
|
expect { sandbox.run "false" }
|
||||||
.to raise_error(ErrorDuringExecution)
|
.to raise_error(ErrorDuringExecution)
|
||||||
.and output(a_string_matching(/foo/).and(matching(/bar/).and(not_matching(/Python/)))).to_stdout
|
.and output(a_string_matching(/foo/).and(matching(/bar/).and(not_matching(/Python/)))).to_stdout
|
||||||
end
|
end
|
||||||
@ -104,28 +104,28 @@ RSpec.describe Sandbox, :needs_macos do
|
|||||||
|
|
||||||
describe "#disallow chmod on some directory" do
|
describe "#disallow chmod on some directory" do
|
||||||
it "formula does a chmod to opt" do
|
it "formula does a chmod to opt" do
|
||||||
expect { sandbox.exec "chmod", "ug-w", HOMEBREW_PREFIX }.to raise_error(ErrorDuringExecution)
|
expect { sandbox.run "chmod", "ug-w", HOMEBREW_PREFIX }.to raise_error(ErrorDuringExecution)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allows chmod on a path allowed to write" do
|
it "allows chmod on a path allowed to write" do
|
||||||
mktmpdir do |path|
|
mktmpdir do |path|
|
||||||
FileUtils.touch path/"foo"
|
FileUtils.touch path/"foo"
|
||||||
sandbox.allow_write_path(path)
|
sandbox.allow_write_path(path)
|
||||||
expect { sandbox.exec "chmod", "ug-w", path/"foo" }.not_to raise_error(ErrorDuringExecution)
|
expect { sandbox.run "chmod", "ug-w", path/"foo" }.not_to raise_error(ErrorDuringExecution)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#disallow chmod SUID or SGID on some directory" do
|
describe "#disallow chmod SUID or SGID on some directory" do
|
||||||
it "formula does a chmod 4000 to opt" do
|
it "formula does a chmod 4000 to opt" do
|
||||||
expect { sandbox.exec "chmod", "4000", HOMEBREW_PREFIX }.to raise_error(ErrorDuringExecution)
|
expect { sandbox.run "chmod", "4000", HOMEBREW_PREFIX }.to raise_error(ErrorDuringExecution)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allows chmod 4000 on a path allowed to write" do
|
it "allows chmod 4000 on a path allowed to write" do
|
||||||
mktmpdir do |path|
|
mktmpdir do |path|
|
||||||
FileUtils.touch path/"foo"
|
FileUtils.touch path/"foo"
|
||||||
sandbox.allow_write_path(path)
|
sandbox.allow_write_path(path)
|
||||||
expect { sandbox.exec "chmod", "4000", path/"foo" }.not_to raise_error(ErrorDuringExecution)
|
expect { sandbox.run "chmod", "4000", path/"foo" }.not_to raise_error(ErrorDuringExecution)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -6,33 +6,37 @@ require "socket"
|
|||||||
|
|
||||||
module Utils
|
module Utils
|
||||||
def self.rewrite_child_error(child_error)
|
def self.rewrite_child_error(child_error)
|
||||||
error = if child_error.inner["cmd"] &&
|
inner_class = Object.const_get(child_error["json_class"])
|
||||||
child_error.inner_class == ErrorDuringExecution
|
error = if child_error["cmd"] && inner_class == ErrorDuringExecution
|
||||||
ErrorDuringExecution.new(child_error.inner["cmd"],
|
ErrorDuringExecution.new(child_error["cmd"],
|
||||||
status: child_error.inner["status"],
|
status: child_error["status"],
|
||||||
output: child_error.inner["output"])
|
output: child_error["output"])
|
||||||
elsif child_error.inner["cmd"] &&
|
elsif child_error["cmd"] && inner_class == BuildError
|
||||||
child_error.inner_class == BuildError
|
|
||||||
# We fill `BuildError#formula` and `BuildError#options` in later,
|
# We fill `BuildError#formula` and `BuildError#options` in later,
|
||||||
# when we rescue this in `FormulaInstaller#build`.
|
# when we rescue this in `FormulaInstaller#build`.
|
||||||
BuildError.new(nil, child_error.inner["cmd"],
|
BuildError.new(nil, child_error["cmd"], child_error["args"], child_error["env"])
|
||||||
child_error.inner["args"], child_error.inner["env"])
|
elsif inner_class == Interrupt
|
||||||
elsif child_error.inner_class == Interrupt
|
|
||||||
Interrupt.new
|
Interrupt.new
|
||||||
else
|
else
|
||||||
# Everything other error in the child just becomes a RuntimeError.
|
# Everything other error in the child just becomes a RuntimeError.
|
||||||
RuntimeError.new(child_error.message)
|
RuntimeError.new <<~EOS
|
||||||
|
An exception occurred within a child process:
|
||||||
|
#{inner_class}: #{child_error["m"]}
|
||||||
|
EOS
|
||||||
end
|
end
|
||||||
|
|
||||||
error.set_backtrace child_error.backtrace
|
error.set_backtrace child_error["b"]
|
||||||
|
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.safe_fork
|
# When using this function, remember to call `exec` as soon as reasonably possible.
|
||||||
|
# This function does not protect against the pitfalls of what you can do pre-exec in a fork.
|
||||||
|
# See `man fork` for more information.
|
||||||
|
def self.safe_fork(directory: nil, yield_parent: false)
|
||||||
require "json/add/exception"
|
require "json/add/exception"
|
||||||
|
|
||||||
Dir.mktmpdir("homebrew", HOMEBREW_TEMP) do |tmpdir|
|
block = proc do |tmpdir|
|
||||||
UNIXServer.open("#{tmpdir}/socket") do |server|
|
UNIXServer.open("#{tmpdir}/socket") do |server|
|
||||||
read, write = IO.pipe
|
read, write = IO.pipe
|
||||||
|
|
||||||
@ -78,6 +82,8 @@ module Utils
|
|||||||
pid = T.must(pid)
|
pid = T.must(pid)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
yield(nil) if yield_parent
|
||||||
|
|
||||||
begin
|
begin
|
||||||
socket = server.accept_nonblock
|
socket = server.accept_nonblock
|
||||||
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
|
||||||
@ -100,14 +106,17 @@ module Utils
|
|||||||
|
|
||||||
if data.present?
|
if data.present?
|
||||||
error_hash = JSON.parse(T.must(data.lines.first))
|
error_hash = JSON.parse(T.must(data.lines.first))
|
||||||
|
raise rewrite_child_error(error_hash)
|
||||||
e = ChildProcessError.new(error_hash)
|
|
||||||
|
|
||||||
raise rewrite_child_error(e)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "Forked child process failed: #{$CHILD_STATUS}" unless $CHILD_STATUS.success?
|
raise ChildProcessError, $CHILD_STATUS unless $CHILD_STATUS.success?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if directory
|
||||||
|
block.call(directory)
|
||||||
|
else
|
||||||
|
Dir.mktmpdir("homebrew-fork", HOMEBREW_TEMP, &block)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user