Hello bash team!

I found a bug in my beloved bash and hope that you can fix it!
Everything else is described in the attached bashbug.txt

Cheers, Justus
From: justus (justuskohl at gmail.com)
To: bug-bash@gnu.org
Subject: Backticked, nested command will steal piped stdin

Configuration Information [Automatically generated, do not change]:
Machine: x86_64
OS: linux-gnu
Compiler: gcc
Compilation CFLAGS: -march=x86-64 -mtune=generic -O2 -pipe -fno-plt 
-fexceptions         -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security   
      -fstack-clash-protection -fcf-protection         -fno-omit-frame-pointer 
-mno-omit-leaf-frame-pointer -g 
-ffile-prefix-map=/build/bash/src=/usr/src/debug/bash -flto=auto 
-DDEFAULT_PATH_VALUE='/usr/local/sbin:/usr/local/bin:/usr/bin' 
-DSTANDARD_UTILS_PATH='/usr/bin' -DSYS_BASHRC='/etc/bash.bashrc' 
-DSYS_BASH_LOGOUT='/etc/bash.bash_logout' -DNON_INTERACTIVE_LOGIN_SHELLS
uname output: Linux archkohlio 6.9.10-arch1-1 #1 SMP PREEMPT_DYNAMIC Thu, 18 
Jul 2024 18:06:13 +0000 x86_64 GNU/Linux
Machine Type: x86_64-pc-linux-gnu

Bash Version: 5.2
Patch Level: 26
Release Status: release

Description:

Original problem:
      $ seq 3 | head -n $(calc -dp "1+1")
      $ seq 3 | head -n 2     # expected behaviour
should print "1 \n 2 \n"
instead calls:
      $ head -n $(seq 3 | calc -dp "1+1")
      $ head -n 2 1 2 3       # actual behaviour, tries opening files 1,2,3
$(calc -p) manages to steal the piped stdin from head!
This is not a problem with apcalc, I couldn't manage to
reproduce the expected behaviour from steal.cpp at all ...
      $ seq 3 | echo $(tee out.txt)
tee steals seq 3 in this case as well, instead of reading from the terminal
      $ echo $(tee out.txt)       # like so
                                                                            
Maybe I am ignorant and this is actually the expected behaviour,
but I doubt it. Also, I am not sure if this should be fixed at all,
as bash is at the core of so many machines and a fix for this
might break some scripts in a subtle way; but that's not for me to decide.
                                                                            
Neither `backticks` nor $(eval calc) make a difference in any case.
Both bash and sh let the $(inner process) steal piped stdin.
zsh and fish act as expected, some other shells I tested can't
manage $() properly due to "2\n" as arg to head etc.
                                                                            
It looks like the pipe is passed to the first process that is running,
instead of the "correct" one. Maybe this is even intended ...?
fish and zsh save themselves by (incorrectly) asserting that steal.cpp
is getting stdin input from a tty ( isatty(stdin) == true )




Repeat-By:

Attached below are bash_bug.sh (running bash, sh, zsh, fish) with the line:
        $ bash -c 'seq 3 | ./print_args -n $(./steal arg)'
as well as two short C++ programs print_args.cpp and steal.cpp,
reading command line arguments as well as piped stdin input and printing them
to stdout as a demonstration. Most programs I tested in place of ./steal will
also take the output of "seq 3", with the exception of calc (without -p flag).

print_args.cpp and steal.cpp are different files for clarity, although
"bash -c 'seq 3 | ./print_args $(./print_args)'" does the same trick


-------#######------- bash_bug.sh -------#######-------
#!/bin/bash

echo " -------------- Broken behaviour, stealing 'seq 3 |' --------------------"
echo    'seq 3 | ./print_args -n $(./steal arg)'
echo -------------------------------- bash ----------------------------------
bash -c 'seq 3 | ./print_args -n $(./steal arg)'
echo --------------------------------- sh -----------------------------------
sh   -c 'seq 3 | ./print_args -n $(./steal arg)'
echo -------------------------------- fish ----------------------------------
fish -c 'seq 3 | ./print_args -n $(./steal arg)' # works
echo --------------------------------- zsh ----------------------------------
zsh  -c 'seq 3 | ./print_args -n $(./steal arg)' # works

echo " ---------- Expected behaviour, including /dev/null fix ----------------"
echo    'seq 3 | ./print_args -n $(./steal arg </dev/null)'
echo -------------------------------- bash ----------------------------------
bash -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)'
echo --------------------------------- sh -----------------------------------
sh   -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)'
echo -------------------------------- fish ----------------------------------
fish -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)'
echo --------------------------------- zsh ----------------------------------
zsh  -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)'

# Various user side fixes
#       $ bash -c 'seq 3 | head -n $(calc -dp 2 </dev/null)'
#       $ bash -c 'seq 3 | head -n $(echo -n "" | calc -dp 2)'
#       $ bash -c 'seq 3 | head -n $(seq 0 | calc -dp 2)'
#       $ bash -c 'seq 3 | head -n $(calc -dp "reinitialize()" 2)'
# without the calc -p flag , everything works as expected, whyever
#       $ bash -c 'seq 3 | head -n $(calc -d 2)'


-------#######------- print_args.cpp -------#######-------
#include <sstream>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>

int main(int argc, char *argv[])
{
  /* Output command line args */
  std::stringstream arg_stream;
  arg_stream << "Command line args:\n";
  for(int i = 0; i < argc; ++i)
    arg_stream << "    " << i << ": " << argv[i] << "\n";
  printf(arg_stream.str().c_str());

  /* Read->output if input comes from a pipe */
  bool stdin_pipe = false;
  if (not isatty(fileno(stdin))) {
    struct stat stats;
    int r = fstat(fileno(stdin), &stats);
    if (r < 0)
      printf("print_args:fstat failed!\n");
    else if (S_ISFIFO(stats.st_mode))
      stdin_pipe = true;
  }
  else {
    printf("print_args:stdin is a tty!\n");
  }
  if (not stdin_pipe) {
    printf("print_args:stdin is not a pipe!\n");
    return 0;
  }

  /* stdin is a pipe! just cat it to stdout */
  printf("Reading from pipe:\n+");
  /* Both versions (fgetc and fread) produce identical results */
#if 0
  while((feof(stdin) == 0) && (ferror(stdin) == 0)) {
    char ch = fgetc(stdin);
    if (ch == EOF) break;
    else putchar(ch);
  }
#else
  const size_t BUF_SIZE = 1; /* no rush */
  char buffer[BUF_SIZE];
  size_t read_bytes;
  while( (read_bytes = fread(buffer, 1, BUF_SIZE, stdin)) == BUF_SIZE )
    for(int i = 0; i < read_bytes; ++i)
      putchar(buffer[i]);
  for(int i = 0; i < read_bytes; ++i)
    putchar(buffer[i]);
#endif
  putchar('+');
  if (feof(stdin) != 0)
    printf("\nEnd of stdin reached!\n");
  if (ferror(stdin) != 0)
    printf("\nERROR encountered for stdin!\n");
  return 0;
}


-------#######------- steal.cpp -------#######-------
#include <sstream>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>

int main(int argc, char *argv[])
{
  /* Output command line args */
  std::stringstream arg_stream;
  for(int i = 1; i < argc; ++i)
    arg_stream << argv[i] << "\n";
  printf(arg_stream.str().c_str());

  /* Read->output if input comes from a pipe */
  bool stdin_pipe = false;
  if (not isatty(fileno(stdin))) {
    struct stat stats;
    int r = fstat(fileno(stdin), &stats);
    if (r < 0)
      printf("steal:fstat_failed!\n");
    else if (S_ISFIFO(stats.st_mode))
      stdin_pipe = true;
  }
  else {
    printf("steal:stdin_is_a_tty!\n");
  }
  if (not stdin_pipe) {
    printf("steal:stdin_is_not_a_pipe!\n");
    return 0;
  }

  /* stdin is a pipe! just cat it to stdout */
  while((feof(stdin) == 0) && (ferror(stdin) == 0)) {
    char ch = fgetc(stdin);
    if (ch == EOF) break;
    else putchar(ch);
  }
  return 0;
}





Fix:
    I guess pipe() - fork() - dup2(.. STDIN/OUT_FILENO) - execvp() would have
    to be shuffled around somewhat, I didn't read too deep into the bash 
sources.
    Looking forward to a reply whether this is even worthy a fix, expected
    behaviour, or actually a bug. Take care!


Reply via email to