oracle / truffleruby

A high performance implementation of the Ruby programming language, built on GraalVM.
https://www.graalvm.org/ruby/
Other
3.02k stars 185 forks source link

Process.spawn with a single argument doesn't recognize shell built-ins #2568

Open dazuma opened 2 years ago

dazuma commented 2 years ago

If you pass a single string argument to Process.spawn, it should interpret it as a command line to pass to the standard shell (see e.g. the documentation at https://ruby-doc.org/core-2.7.5/Process.html#method-c-spawn). Only if you pass multiple arguments, or if the first argument is an array, should Process.spawn bypass the shell and attempt to spawn the process directly.

This is the behavior of MRI (versions 2.7.5 and 3.1.0 tested). However, TruffleRuby 21.3.0 sometimes appears to attempt to parse the command line and spawn it directly. without using a shell.

Example:

Process.spawn("exit 0")  # use a bourne-shell built-in

In MRI (on a MacOS X machine), the above succeeds, spawning a shell process that exits with a 0 status code. However, on TruffleRuby, it raises because it can't find a binary called "exit", apparently not recognizing it as a shell built-in:

irb(main):001:0> Process.spawn("exit 0")
Traceback (most recent call last):
    from /Users/dazuma/opt/asdf/installs/ruby/truffleruby-21.3.0/bin/irb:42:in `<main>'
    from <internal:core> core/kernel.rb:376:in `load'
    from <internal:core> core/kernel.rb:376:in `load'
    from /Users/dazuma/opt/asdf/installs/ruby/truffleruby-21.3.0/lib/gems/gems/irb-1.3.3/exe/irb:11:in `<top (required)>'
    from <internal:core> core/throw_catch.rb:36:in `catch'
    from <internal:core> core/throw_catch.rb:36:in `catch'
    from <internal:core> core/kernel.rb:407:in `loop'
    from (irb):1:in `<top (required)>'
    from <internal:core> core/process.rb:653:in `spawn'
    from <internal:core> core/truffle/process_operations.rb:84:in `spawn'
<internal:core> core/truffle/process_operations.rb:454:in `spawn': No such file or directory - No such file or directory - exit (Errno::ENOENT)

It looks like Kernel#system behaves similarly. Consider the following:

system "exit 0"
puts $?.exitstatus

On MRI, this prints 0. On TruffleRuby, this incorrectly prints 127.

Interestingly, if the command includes multiple processes, shell pipelines, or other features, it runs correctly. The two examples below do behave correctly on TruffleRuby. It makes me wonder if there's an optimization gone awry.

system "echo hi; exit 0"
system "echo hi | cat"
eregon commented 2 years ago

Thanks for the report. The semantics are more complicated (i.e., the docs are wrong), a single argument String can run without an extra shell, this is the behavior and optimization of CRuby, and TruffleRuby tries to replicate it. So in CRuby there is some logic which specifically checks for shell builtins like exit, which we need to replicate in TruffleRuby as well: https://github.com/ruby/ruby/blob/v2_7_2/process.c#L2449-L2530