I'm run on debian stretch, all below commands(dash and bash) is input to bash.
The whoami seem never run as the user test in dash as in below codes.

$ sudo dash << 'end'
> su test
> whoami
> end
$ sudo bash << 'end'
> su test
> whoami
> end
  • Simpler proof of concept: echo 'rev\nuptime' | some_shell.
    – marcelm
    Aug 25 '19 at 10:04
  • 1
    @marcelm, you mean printf 'rev\nuptime\n' | some_shell
    – ilkkachu
    Aug 25 '19 at 11:54
  • @ilkkachu You're right, I did a zshism. Perhaps I should get in the habit of defaulting to printf.
    – marcelm
    Aug 25 '19 at 19:34

Consider this example instead:

$ cat f
grep pos /proc/self/fdinfo/0
IFS= read -r var
echo A
echo B
printf '%s\n' "var=$var"
$ bash < f
pos:    29
var=echo A
$ dash < f
pos:    85

As you can see, at the time the grep command is run, the position within stdin is at the end of the file with dash, and just after the newline that follows the grep command in bash.

The echo A command is run by dash but in the case of bash, it's fed as input to read.

What happened is that dash read the whole input (actually, a block of text) while bash read one line at a time before running commands.

To do that, bash would need to read one byte at a time to make sure it doesn't read past the newline, but when the input is a regular file (like in the case of my f file above, but also for here-documents which bash implements as temporary files, while dash uses pipes), bash optimises it by reading by blocks and seek back to the end of the line, which you can see with strace on Linux:

$ strace -e read,lseek bash < f
lseek(0, 0, SEEK_CUR)                   = 0
read(0, "grep pos /proc/self/fdinfo/0\nIFS"..., 85) = 85
lseek(0, -56, SEEK_CUR)                 = 29
pos:    29

$ strace -e read,lseek dash < f
read(0, "grep pos /proc/self/fdinfo/0\nIFS"..., 8192) = 85
pos:    85
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12422, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
read(0, "", 1)                          = 0

When stdin is a terminal device, each read() returns lines as sent by the terminal, so you generally see a similar behaviour in bash and dash.

In your case, you could do:

sudo dash << 'end-of-script'
su test <<"end"

or better:

sudo sh -c '
  su test -c whoami

or even better:

sudo -u test whoami

There's a difference in how Bash and Dash handle input from stdin: When starting a command (su here) Bash takes care to leave the read position on stdin just after the line that caused the command to run, either by reading a byte at a time for pipes, or by seeking back if the input is from a file. Dash doesn't care, it just reads full blocks.

This matters because the stdin and the read position within it is shared between the shell and the child process.

So, with Bash, it reads su test\nwhoami\n, seeks back to just after the first newline, then launches su, which now sees whoami\n on the input.

With Dash, it reads su test\nwhoami\n, launches su, which doesn't see any input and exits, and then Dash launches whoami.

You can see the same thing here, with Dash, the date command is executed and the read gets an empty input, while with Bash that line is read to x.

$ cat test.sh
read x
echo "variable x = '$x'"
$ cat test.sh | bash
variable x = 'date'
$ cat test.sh | dash
Sat Aug 24 12:25:23 EEST 2019
variable x = ''

If I interpret the POSIX description of sh correctly, the careful behaviour of Bash is required, and Dash's laxer approach doesn't conform with it:

[...] When the shell is using standard input and it invokes a command that also uses standard input, the shell shall ensure that the standard input file pointer points directly after the command it has read when the command begins execution. It shall not read ahead in such a manner that any characters intended to be read by the invoked command are consumed by the shell

For what it's worth, Busybox's shell behaves like Dash here, all others I tested do what Bash does.

  • "So, with Bash, it reads su test\nwhoami\n, seeks back to just after the first newline ..." - I was skeptical on bash actually seeking backwards, so I checked with strace, and sure enough, it did! (My shell puts the heredoc in a temporary file and attaches it to bash' stdin, so this is actually possible). I then tested with echo 'su foo\nbar' | bash, which gives bash an unseekable stdin. Then bash switched to reading one byte at a time from stdin, preserving the behaviour. I'm surprised. zsh actually behaves the same, too.
    – marcelm
    Aug 25 '19 at 10:02
  • @marcelm, yep, Stéphane's answer has the sample strace, so I didn't bother adding it. If I read the POSIX text correctly, it requires Bash's behaviour here, but contrast with the requirements for standard utilities, which are allowed to over-read from pipes: unix.stackexchange.com/q/409462/170373
    – ilkkachu
    Aug 25 '19 at 12:11
  • Yeah, I only read Stéphane's answer after I had written that comment... I considered deleting it, but it has a little extra info in it so I decided to leave it in case anyone cares about that tidbit ¯\_(ツ)_/¯
    – marcelm
    Aug 25 '19 at 19:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.