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
B
var=echo A
$ dash < f
pos: 85
A
B
var=
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"
whoami
end
end-of-script
or better:
sudo sh -c '
su test -c whoami
'
or even better:
sudo -u test whoami
echo 'rev\nuptime' | some_shell.printf 'rev\nuptime\n' | some_shellprintf.