You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Kernel#fork exists and runs the block in-process: the 'child' mutates the parent, and exit inside it ends the program #774
rbgo: after fork: $g="CHILD-MUTATED-PARENT" pid_is=100001 mypid=26239
MRI : after fork: $g="parent-value" pid_is=26244 mypid=26241
The "child" overwrote the parent's global; the returned pid is synthetic (100001, counting up) and Process.pid is unchanged, so there is no second process at all. internal/vm/spawn.go states the design openly — "Without an OS fork we run the block in-process" — and the reason is sound: Go's runtime cannot be forked safely.
The problem is not the implementation choice, it is that fork { } silently does something with the opposite guarantee to what it is usually called for.fork is the standard way to isolate untrusted work and to drop privileges; a program that forks for either reason gets neither here, and gets no error either. Process.fork correctly raises NoMethodError, and Process.respond_to?(:fork) is correctly false — it is only the Kernel#fork spelling, which is the one almost every program uses, that pretends.
Two further observations from the same measurement, the second worth more attention than the first:
A SystemExit raised inside the block unwinds the whole program: fork { exit 1 } ends everything, where MRI exits only the child. runForkBlock's recover catches the exec sentinel but not this.
Because the block shares the parent's ENV and working directory, a fork whose block calls Dir.chdir or assigns to ENV — a completely routine thing to do in a child before exec — moves the parent.
Options, from least to most disruptive:
Raise NotImplementedError from Kernel#fork too, matching Process.fork. Honest, and loud at the right moment; breaks programs that currently "work" by accident.
Keep it, but make it not corrupt the parent: run the block with the globals/ENV/cwd saved and restored, and contain SystemExit inside it. Closer to a useful approximation, still not isolation.
Reopened: only half of this was fixed, and the tracker closed it anyway
c4ccb3d (the squash of #806) closed this issue. That was not intended — both
the PR body and the commit said so in as many words:
This does not close #774. An uncaught exception in the block still takes
the parent down, where MRI prints it in the child and exits 1.
A closing keyword elsewhere in the same message (Closes #772) carried the day;
writing "this does NOT close #774" next to a mention of it does not prevent
that. Worth knowing before the next PR that fixes one of two issues.
exit, exit! and abort inside a fork block now stop the "child" and
report its status, instead of ending the whole program at the fork call.
Measured against MRI 4.0.7, with a normally-returning block still 0 as the
control.
What remains open — measured again on v0.3.0
An uncaught exception in the block still takes the parent with it:
MRI prints the exception in the child and exits it 1; the parent carries on
and wait2 reports 1.
The reason it was left rather than done halfway: runForkBlock would have to render the exception the way the top level does, and the renderer that exists
lives in cmd/rbgo, not in internal/vm. Converting it to a status silently
would lose the diagnostic, which is worse than the crash — an embedder would get
a "child" that failed with no indication why.
And the larger point of this issue is untouched
Kernel#fork still does not fork. The block runs in the same process, so it
mutates the parent's globals, ENV and working directory, and returns a
synthetic pid. fork is the standard way to isolate untrusted work and to drop
privileges; a program that forks for either reason gets neither here, and gets
no error either. Process.fork correctly raises NoMethodError — it is the Kernel#fork spelling, the one almost every program uses, that pretends.
The "measured again on v0.3.0" above was written before I had re-run it against
the v0.3.0 binary. Done now, so here is the output rather than the assertion:
=== MRI 4.0.7 === === rbgo v0.3.0 ===
before before
forkraise2.rb:2:in 'block in <main>': forkraise2.rb:2:in 'block in <main>':
boom (RuntimeError) boom (RuntimeError)
from …:2:in 'Kernel#fork' from …:2:in '<main>'
from …:2:in '<main>'
after fork (nothing further)
child status=1
parent alive
exit=0 exit=1
The claim holds. Two details the earlier text did not have:
rbgo exits 1; MRI exits 0, because under MRI only the child failed.
MRI's backtrace carries a from …:2:in 'Kernel#fork' frame that rbgo's does
not. Not worth its own issue, but the two are not byte-identical and I would
rather say so than imply they are.
Activity
internal/vm: fork { exit n } stops the child, not the program; three …
tannevaled commentedon Oct 10, 2026
Reopened: only half of this was fixed, and the tracker closed it anyway
c4ccb3d(the squash of #806) closed this issue. That was not intended — boththe PR body and the commit said so in as many words:
A closing keyword elsewhere in the same message (
Closes #772) carried the day;writing "this does NOT close #774" next to a mention of it does not prevent
that. Worth knowing before the next PR that fixes one of two issues.
What IS fixed
exit,exit!andabortinside aforkblock now stop the "child" andreport its status, instead of ending the whole program at the
forkcall.Measured against MRI 4.0.7, with a normally-returning block still 0 as the
control.
What remains open — measured again on v0.3.0
An uncaught exception in the block still takes the parent with it:
MRI prints the exception in the child and exits it 1; the parent carries on
and
wait2reports 1.The reason it was left rather than done halfway:
runForkBlockwould have torender the exception the way the top level does, and the renderer that exists
lives in
cmd/rbgo, not ininternal/vm. Converting it to a status silentlywould lose the diagnostic, which is worse than the crash — an embedder would get
a "child" that failed with no indication why.
And the larger point of this issue is untouched
Kernel#forkstill does not fork. The block runs in the same process, so itmutates the parent's globals,
ENVand working directory, and returns asynthetic pid.
forkis the standard way to isolate untrusted work and to dropprivileges; a program that forks for either reason gets neither here, and gets
no error either.
Process.forkcorrectly raisesNoMethodError— it is theKernel#forkspelling, the one almost every program uses, that pretends.🤖 Generated with Claude Code
tannevaled commentedon Oct 10, 2026
The "measured again on v0.3.0" above was written before I had re-run it against
the v0.3.0 binary. Done now, so here is the output rather than the assertion:
The claim holds. Two details the earlier text did not have:
from …:2:in 'Kernel#fork'frame that rbgo's doesnot. Not worth its own issue, but the two are not byte-identical and I would
rather say so than imply they are.