Skip to content

Process.kill(:TERM, Process.pid) kills the process instead of raising SignalException — ~77 examples behind one defect #691

Closed

Description

@tannevaled

Process.kill(:TERM, Process.pid) terminates the process where MRI raises a
rescuable SignalException. Measured against MRI 4.0.5, three runs each:

begin
  Process.kill(:TERM, Process.pid)
rescue SignalException => e
  puts "raised #{e.class}: #{e.message} signo=#{e.signo}"
end
puts "still alive"
output exit status
MRI 4.0.5 raised SignalException: SIGTERM signo=15 then still alive 0, all three runs
rbgo (nothing) ×2, NO RAISE - kept running ×1 143, all three runs

Two things are wrong, and the second is the surprising one:

  1. The signal is never turned into a rescuable Ruby exception — the process just
    dies with 128+15.
  2. On the one run in three where the raise path was skipped and the program
    printed and continued, it still exited 143. So the death is not only "the
    raise is missing"; the signal disposition itself terminates regardless of what
    Ruby code does afterwards.

It is racy, which is why it has been easy to miss: 1-in-5 on one sweep,
0-in-5 on another. It is also the entire ±1 difference between two otherwise
identical ratchet readings, so it has been showing up as measurement noise rather
than as a defect.

≈77 examples sit behind this one defect

Nine spec files currently report nothing at all — not failures, a silent
exit. Measured as what MRI 4.0.5 reaches through our own shim, so these are
examples our harness can actually score today:

file MRI through our shim
core/signal/trap_spec.rb 36
core/signal/signal_exception_spec.rb 12
core/kernel/exit_spec.rb 12
core/process/exit_spec.rb 8
core/mutex/lock_spec.rb 4
core/signal/interrupt_spec.rb 3
core/signal/signm_spec.rb 1
core/signal/signo_spec.rb 1
total ≈77

A tenth file, core/string/unpack/carret_spec.rb, yields nothing for MRI under
our shim either — that one is a shim gap, not this defect. Worth stating, because
grouping it in would have inflated the claim.

This is the largest single lever identified today by some margin, and it is one
defect rather than a subsystem: SignalException already exists as a class, and
Signal.trap already has a binding.

Owning files: internal/vm/process.go:306 (Process.kill) and
internal/vm/signal.go (Signal.trap).

Related, and probably the same boundary

#676 — a deliberate exit 3 also fails to reach the process (rbgo exits 0 where
MRI exits 3), and there the VM half was entirely correct while the front-end
never translated the terminal SystemExit. This is the mirror image: the signal
reaches the process when it should have become an exception. Both live at the
boundary between the Go runtime's process state and the VM's exception
machinery, and it is worth fixing them together rather than one at a time.

Activity

  1. tannevaled commented on Sep 26, 2026

    @tannevaled
    ContributorAuthor

    Two corrections to figures and paths in this issue, both mine.

    1. Four of the file paths are wrong. These are under core/exception/, not
    core/signal/:

    • core/exception/signal_exception_spec.rb
    • core/exception/interrupt_spec.rb
    • core/exception/signm_spec.rb
    • core/exception/signo_spec.rb

    2. "≈77 examples" was the MRI ceiling, not what this defect withholds. The two
    are different quantities and I conflated them. Measured after the fix in #693:

    recovered by fixing signals + exit status 58
    behind a different defect in internal/vm/thread.go 24
    needs Thread#group 1

    3. Three of the nine files were never failing — they TIME OUT, before and
    after, and still do: core/kernel/exit_spec.rb, core/process/exit_spec.rb,
    core/mutex/lock_spec.rb. They were only distinguishable because the measuring
    tool separates TIMEOUT from a zero result from a missing RBGO_RESULT line. A
    file reporting nothing has at least three causes here and they need different
    fixes.

    Those 24 examples have a single cause, and it is not a mutex defect: an uncaught
    SystemExit in a non-main thread must reach the main thread and interrupt a
    blocking sleep
    , which rbgo does not do.

    ready = false
    t = Thread.new { Thread.pass until ready; exit 42 }
    begin; ready = true; sleep
    rescue SystemExit; puts "main saw SystemExit"; end
    # MRI prints; rbgo hangs for ever

    Recursive and cross-fiber deadlock detection were checked and already match MRI
    (both raise ThreadError matching /deadlock/), so core/mutex/lock_spec.rb
    belongs to the thread-interruption family rather than to Mutex.

    That work is in flight against internal/vm/thread.go, together with the two
    defects routed from the shim repair (#692) and the TestServerTLSLoopback hang
    (#695) — all four are candidates for one cause: a blocking operation that does not
    yield control as MRI's does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions