Bash – C system(“bash”) ignores stdin

bashclinux

I have a file input:

$ cat input
1echo 12345

and I have the following program

1st version

#include <stdio.h>
#include <stdlib.h>

int main() {
  system("/bin/bash -i");
  return 0;
}

Now If I run it,

$ gcc -o program program.c
$ ./program < input
bash: line 1: 1echo: command not found
$ exit

Everything works as expected.

Now I want to ignore the 1st character of the file input, so I make a call to getchar() before invoking system().

2nd version:

#include <stdio.h>
#include <stdlib.h>

int main() {
  getchar();
  system("/bin/bash -i");
  return 0;
}

Surprisingly, the bash exits instantly like there is no input.

$ gcc -o program program.c
$ ./program < input
$ exit

Question why bash is not receiving the input ?

NOTE
I tried some stuff and I figured out that forking a new child for the main process solves the problem:

3rd Version

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
  getchar();
  if (fork() > 0) {
    system("/bin/bash -i");
    wait(NULL);
  }
  return 0;
}

$ gcc -o program program.c
$ ./program < input
$ 12345
$ exit

OS Ubuntu 16.04 64bit, gcc 5.4

Best Answer

A file stream is defined to be:

fully buffered if and only if it can be determined not to refer to an interactive device

Since you're redirecting into standard input, stdin is non-interactive and so it's buffered.

getchar is a stream function, and will cause the buffer to be filled from the stream, consuming those bytes, and then return a single byte to you. system just runs fork-exec, so the subprocess inherits all your open file descriptors as-is. When bash tries to read from its standard input, it will find that it's already at the end of file because all the content has been read by your parent process already.


In your case, you want to consume only that single byte before handing over to the child process, so:

The setvbuf() function may be used after the stream pointed to by stream is associated with an open file but before any other operation [...] is performed on the stream.

Thus adding a suitable call before the getchar():

#include <stdio.h>
#include <stdlib.h>

int main() {
  setvbuf(stdin, NULL, _IONBF, 0 );
  getchar();
  system("/bin/bash -i");
  return 0;
}

will do what you want, by setting stdin to be unbuffered (_IONBF). getchar will cause only a single byte to be read, and the rest of the input will be available to the subprocess. It might be better to use read instead, avoiding the whole streams interface, in this case.


POSIX mandates certain behaviours when the handle can be accessed from both processes after a fork, but explicitly notes that

If the only action performed by one of the processes is one of the exec functions [...], the handle is never accessed in that process.

which means that system() doesn't (have to) do anything special with it, since it's just fork-exec.

This is probably what your fork workaround is hitting. If the handle is accessed on both sides, then for the first one:

If the stream is open with a mode that allows reading and the underlying open file description refers to a device that is capable of seeking, the application shall either perform an fflush(), or the stream shall be closed.

Calling fflush() on a read stream will mean that:

the file offset of the underlying open file description shall be set to the file position of the stream

so the descriptor position should be reset back to 1 byte, the same as the stream's, and the subsequent subprocess will get its standard input starting from that point.

Additionally, for the second (child's) handle:

If any previous active handle has been used by a function that explicitly changed the file offset, except as required above for the first handle, the application shall perform an lseek() or fseek() (as appropriate to the type of handle) to an appropriate location.

and I suppose "an appropriate location" might be the same (though it isn't further specified). The getchar() call "explicitly changed the file offset", so this case should apply. The intention of the passage is that working in either branch of the fork should have the same effect, so both fork() > 0 and fork() == 0 should work the same. Since nothing actually happens in this branch, though, it's arguable that none of these rules should be used at all for either parent or child.

The exact result is probably platform-dependent - at least, exactly what counts as "can ever be accessed" isn't directly specified, nor which handle is first and second. There is also an earlier, overriding case for the parent process:

If the only further action to be performed on any handle to this open file descriptor is to close it, no action need be taken.

which arguably applies for your program since it just terminates afterwards. If it does then all the remaining cases, including the fflush(), should be skipped, and the behaviour you're seeing would be a deviation from the specification. It's arguable that calling fork() constitutes performing an action on the handle, but not explicit or obvious, so I wouldn't trust that. There's also enough "either" and "or" in the requirements that a lot of variation seems allowable.

For multiple reasons I think the behaviour you're seeing may be a bug, or at least a generous interpretation of the specification. My overall reading is that, since in every case one branch of the fork doesn't do anything, none of these rules should have been applied and the descriptor position should have been ignored where it was. I can't be definitive about that, but it seems like the most straightforward reading.


I wouldn't rely on the fork technique working. Your third version doesn't work for me here. Use setbuf/setvbuf instead. If possible, I'd even use popen or similar to set up the process with the necessary filtering explicitly, rather than relying on the vagaries of stream and file descriptor interactions.

Related Question