2

I built a docker image from Ubuntu with OpenSSH server installed. Suppose I request a simple command over ssh

ssh root@172.17.0.2 "sleep 10"

Then, ps aux --forest inside the container gives me this:

SER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         18  0.3  0.0  18508  3500 pts/0    Ss   16:12   0:00 bash
root         37  0.0  0.0  34404  2884 pts/0    R+   16:12   0:00  \_ ps aux --forest
root         16  0.0  0.0  72300  3280 ?        Ss   16:12   0:00 /usr/sbin/sshd
root         34  0.0  0.0  74656  6648 ?        Ss   16:12   0:00  \_ sshd: root@notty
root         36  0.0  0.0   4532   744 ?        Ss   16:12   0:00      \_ sleep 1000

But when I execute a more complex command like

ssh root@172.1.70.2 "sleep 1000; sleep 1"

It now spawns a bash shell and passes my command to it:

root         18  0.1  0.0  18508  3500 pts/0    Ss   16:12   0:00 bash
root         43  0.0  0.0  34404  2896 pts/0    R+   16:13   0:00  \_ ps aux --forest
root         16  0.0  0.0  72300  3280 ?        Ss   16:12   0:00 /usr/sbin/sshd
root         39  0.0  0.0  74656  6712 ?        Ss   16:13   0:00  \_ sshd: root@notty
root         41  0.0  0.0   9920  1312 ?        Ss   16:13   0:00      \_ bash -c sleep 1000; sleep 1
root         42  0.0  0.0   4532   776 ?        S    16:13   0:00          \_ sleep 1000

So, which part decides whether to invoke a shell or not? Is that controlled by SSHd? If so, is there a way to force SSHd to always invoke shell?

P.S. I know that in Ruby, Kernel.exec is the function that chooses to spawn or not to spawn a shell based on meta-characters like ; and &, so maybe in my case the choice is not made on the application level?

1 Answers1

3

As far as I know sshd does not perform such optimization. If it did, it would make little sense because sshd is supposed to use the command interpreter specific to the user, in general it can be anything (not necessarily a real shell). Even if the interpreter is /bin/bash, it can be a modified bash that does something unusual when it starts. Because sshd cannot know this, it really shouldn't interfere.

Whatever is assigned to the user as their command interpreter is always spawned. In your case it's bash.

What you observed is an optimization in the Bash itself. In some cases, if Bash can tell there will be absolutely nothing to do during and after execution of the last command, then it will exec the last command rather than run it normally. This means the command will replace bash without creating a new process.

In your case bash is a child of sshd. When it replaces itself with some command, the command becomes the child. This creates an impression no shell was ever there.


See the following examples (tested with Bash 4.4.20):

  1. bash -c '
       exec sleep 5
    ' & sleep 1; ps -p "$!"

    You will see sleep because of exec. This was expected.

  2. bash -c '
      sleep 5
    ' & sleep 1; ps -p "$!"

    Also sleep because of the optimization.

  3. bash -c '
       sleep 5
       true
    ' & sleep 1; ps -p "$!"

    This time it's bash. The shell cannot implicitly exec sleep 5 because there is true to run later.

  4. bash -c '
       sleep 5
       sleep 4
    ' & sleep 1; ps -p "$!"; sleep 5; ps -p "$!"

    At first you will see bash; it's when the first sleep runs. Then you will see (the second) sleep with the same PID.

  5. This is kinda interesting (especially in comparison with the above):

    bash -c '
       sleep 5; sleep 4
    ' & sleep 1; ps -p "$!"; sleep 5; ps -p "$!"

    This time bash does not exec the second sleep, although the code seems equivalent to the previous one. I cannot tell if it's just a lack of optimization or there's a non-obvious reason not to exec.

  6. And finally:

    bash -c '
       trap "echo foo" SIGUSR1
       sleep 5
    ' & sleep 1; ps -p "$!"

    You will see bash. The shell knows it needs to remain in case a signal arrives, so it doesn't replace itself with sleep 5.