Skip to content

END { } crashes the process with a Go panic: valid Ruby source takes the embedder's host down #805

Closed

Description

@tannevaled

Summary

END { } — ordinary Ruby, in the language since 1.0 — ends the process with an
uncontained Go runtime panic:

$ cat p.rb
END { puts "E" }
$ rbgo p.rb
panic: runtime error: slice bounds out of range [:1] with capacity 0 [recovered, repanicked]
$ echo $?
2

MRI prints E and exits 0.

For a program this is a crash instead of an answer. For an embedder it is
worse
: ruby.Run does not return an error, it takes the host process down,
and nothing the host can write around the call prevents it.

Isolated

source rbgo
BEGIN { puts "B" } + puts "main" B, main — correct
at_exit { puts "A" } main, A — correct
at_exit { |x| puts x.inspect } nil — correct
END { puts "E" } panic, exit 2
END { 1 }; puts "ok" prints ok, then panics
END { } ×2 panic

The last row of that table is the useful one: the block registers fine and
the program runs to completion — ok is printed — and the panic happens when
the handler is invoked at exit.

Where

internal/vm.(*VM).exec
	internal/vm/vm.go:1915
internal/vm.(*VM).callBlockSelf
	internal/vm/object_model.go:2505
internal/vm.(*VM).runAtExitOne.func1
	internal/vm/kernel_introspect.go:782
internal/vm.(*VM).Run.func1
	internal/vm/vm.go:1523        <- repanics, by design

vm.go:1915 is in the rest-parameter binding:

copy(env.slots[:nbind], args[:nbind])
rest := []object.Value{}
if avail > si { rest = append(rest, args[si:avail]...) }
env.slots[si] = object.NewArrayFromSlice(rest)

capacity 0 with nbind == 1 says env.slots is empty while the ISeq claims a
parameter slot. So the END block's ISeq and its environment disagree about how
many locals it has — and at_exit, whose block takes the same path with the
same zero arguments, does not, which is what points at how END is compiled
rather than at the binder.

The repanic at vm.go:1523 is not the bug

That arm is deliberate policy, and TestRunPropagatesInternalPanic encodes it:
a fault built from Ruby input is converted to a Ruby exception, while a
broken VM invariant stays loud rather than being dressed up as a
RuntimeError the program might rescue. This is an inconsistent ISeq, which is
the second kind. The fix belongs where the ISeq is built, not in the recover.

The README says something else, and it is the kinder story

BEGIN { } / END { } — do not parse (parse error: unexpected "{" after statement).

Measured today, BEGIN { } works and END { } parses and then crashes. A
reader checking whether they can use it is told it is unavailable, which is a
gap; what they get is a process death. Being corrected in #801.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions