Project

General

Profile

Actions

Bug #22243

closed

Change fork behavior when Process._fork raises an exception during fork with a block argument

Bug #22243: Change fork behavior when Process._fork raises an exception during fork with a block argument

Added by rpeng (Richard Peng) 2 days ago. Updated 2 days ago.

Status:
Closed
Assignee:
-
Target version:
-
ruby -v:
Backport:
[ruby-core:126379]

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).

Actions

Also available in: PDF Atom