Bug #22243
closedChange fork behavior when Process._fork raises an exception during fork with a block argument
Description
Problem¶
Ruby exposes Process.fork in two formats - one with block arguments and one without. When a block is passed in, the implicit contract is that a child process will terminate once the block ends. However, it is possible for the child to "escape" the enclosed block in certain circumstances when Process._fork raises an exception. Here's a demonstration of what may occur:
module BadForkTracker
def _fork
pid = super
if pid == 0
raise "exception during fork in child" #
end
pid
end
end
Process.singleton_class.prepend(BadForkTracker)
loop do
sleep(1)
puts "I'm parent #{Process.pid}"
fork do
puts "I'm child #{Process.pid} and I'm exiting"
end
rescue StandardError => e
puts "#{Process.pid} Received Error: #{e}" # <- child ends up in this block, then resumes the loop. most users will not be expecting this.
end
In practice, there are many libraries that implement "fork tracking" (active_support, datadog, connection_pool, to name a few) by prepending to the Process module. Many of which are designed to run user specified callbacks which increases the surface area of errors occurring. This means that on _fork, there's a good chance that something will throw and cause a child to resume execution in the "parent" context. In addition, many workloads (sidekiq, puma) have global rescues that are similar to the example case I provided above, which would unintentionally cause the child to continue execution.
Proposal¶
I am proposing a change in behavior: when Process.fork is called with a block, if Process._fork raises an exception in the child, immediately terminate the child with an error exit code (instead of relying on an enclosing rescue to handle it). This guarantees that a child cannot escape out of the block it was meant to execute. In my opinion, this is more in line with how libraries generally expect this to work (e.g. there are very few libraries that are being defensive and attempting not to throw in def _fork overrides).
Updated by luke-gru (Luke Gruber) 2 days ago
- Tracker changed from Feature to Bug
- Backport set to 4.0: REQUIRED
Thanks for the report. I'm changing this from feature request to bug because this behavior is very surprising and shouldn't happen.
Updated by nobu (Nobuyoshi Nakada) 2 days ago
Updated by nobu (Nobuyoshi Nakada) 2 days ago
- Status changed from Open to Closed
Applied in changeset git|fff4e38e4a44a1e9748e9f47b87cbe393b4495a1.
[Bug #22243] Ensure forked process to terminate
The forked process should terminate reliably even if an exception
occurs.
Updated by rpeng (Richard Peng) 2 days ago
nobu (Nobuyoshi Nakada) wrote in #note-3:
Applied in changeset git|fff4e38e4a44a1e9748e9f47b87cbe393b4495a1.
[Bug #22243] Ensure forked process to terminate
The forked process should terminate reliably even if an exception
occurs.
Thanks everyone for the quick triage and fix!!