7

I am trying to understand why some shells seem to receive a special treatment when called with sudo. For instance, there seem to be two possible behaviours:

The "implicit" group (pstree is a direct child of sudo, no shell in between):

$ sudo pstree -s $$
systemd───login───bash───sudo───pstree
$ sudo bash -c 'pstree -s $$'
systemd───login───bash───sudo───pstree
$ sudo zsh -c 'pstree -s $$'
systemd───login───bash───sudo───pstree
$ sudo dash -c 'pstree -s $$'
systemd───login───bash───sudo───pstree

The "explicit" group (the shell is a direct child of sudo):

$ sudo ksh -c 'pstree -s $$'
systemd───login───bash───sudo───ksh───pstree
$ sudo tcsh -c 'pstree -s $$'
systemd───login───bash───sudo───tcsh───pstree
$ sudo fish -c 'pstree -s $fish_pid'
systemd───login───bash───sudo───fish───pstree

There is obviously seem to be some kind of integration happening between sudo and some shells, but I could find no documentation on it. I also grepped the source code of both sudo and bash but could find no clue there either.

This other question seems related: Why (...) doesn't spawn a new child process when run in background?

My versions of sudo and bash are:

$ sudo --version
Sudo version 1.8.29
...
$ bash --version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
...
chicks
  • 1,112
mesr
  • 399
  • 1
  • 12

1 Answers1

21

nope, that's not an interaction between your shell and sudo; it's that the shell tasked with executing replaces itself with the command you run!

You can remove sudo from the whole thing to the same result; for example, I run a zsh in an alacritty terminal:

$> bash -c 'pstree -s $$'
systemd───alacritty───zsh───pstree

no bash here!

We can verify that's what happens by running

$> strace -o /tmp/bash-pstree.strace bash -c 'pstree -s $$'
systemd───alacritty───zsh───pstree
$> bat /tmp/bash-pstree.strace # or just less /tmp/.... ; bat is just a nice code highlighter

There we see

$> grep execve /tmp/bash-pstree.strace
execve("/usr/bin/bash", ["bash", "-c", "pstree -s $$"], 0x7ffc02c52d30 /* 102 vars */) = 0
execve("/usr/bin/pstree", ["pstree", "-s", "35735"], 0x55b90ec78d00 /* 102 vars */) = 

So, the first execve is bash being called, the second one is bash replacing itself with pstree – there's literally no bash exisiting after that! No fork/clone happens in between.

Of course, that only works with the last command in a command chain. If we replaced ourselves with the command to be run earlier, we couldn't execute anything after. We can actually verify this pretty easily:

$> bash -c 'pstree -p -s $$; pstree -p -s $$'                                                                                                                                                                                                        
systemd(1)───alacritty(34604)───zsh(34609)───bash(39257)───pstree(39258)
systemd(1)───alacritty(34604)───zsh(34609)───pstree(39257)

See, here the first pstree is run actually in a process created by bash, the second by the same process, replacing bash.

Replacing the shell with the program it runs is of course nice, resource-wise: we yield all the file handles memory, locks etc early on.

I don't know why some shells do that, and others don't (they probably use fork or clone to copy the own process, before execve'ing the command specified). Possibly, it's an optimization that never occurred to the developers (if you have a proudly rich shell like fish, why safe a couple of kB of RAM a bit earlier? Nobody will try to spawn 10000 fish from a shell script to run a command!), or it was software-architecturally awkward to introduce a special case, or it was impossible, because the shell maintains some control over the started process, and thus still needs to exist to be able to e.g. receive signals or do IPC. Of course, some shells are simply very old, and maybe a bit simplistic as a result (the last ksh release was 10 years ago!).

  • It explains a lot, thank you. – mesr Jul 10 '22 at 13:53
  • 2
    strace -f -e clone,fork,execve bash -c 'pstree -s $$' would be another way to verify that it doesn't fork before exec. Your way is also solid proof, though: you're not using strace -f so it's not following into child processes, but you do still see an execve. So it must have replaced itself. You can also see that from strace -f -e execve (not displaying the clone system call that implements fork), just from looking at the PID number if there is a fork involved at all. Tracing clone rules out the possibility you mentioned of forking but then replacing the original. – Peter Cordes Jul 11 '22 at 11:58
  • 1
    You can of course trace bash -c 'pstree -s $$ && echo foo' to make bash fork so it can run echo or not after collecting the exit status, as a way to test a tracing method to see how it looks when a shell does fork. – Peter Cordes Jul 11 '22 at 12:01
  • 1
    I believe that ksh was one of the more aggressive users of various kinds of optimization, such as tail-call optimization. @Stéphane Chazelas may have more to say about that if this draws his attention. – jrw32982 Jul 13 '22 at 20:51