Search code examples
bashshellpipeshposix

Background process, with stdin a pipe connected to fd 3 in the parent shell script


Is it possible, in portable shell, without using a named pipe, to start a background process and have that process's stdin be a pipe which is open on file descriptor 3 (or some other number greater than 2) in the parent shell? Another way to put it is that I want to do in portable shell what popen("some-program", "w") does in C.

Concretely, do what this snippet does, without using mkfifo:

mkfifo fifo
some-program < fifo &
exec 3> fifo
rm fifo

Note: "bash" tag added primarily for visibility. I understand that this is possible using the "coprocess" extension in bash, ksh, or zsh; I am looking for a technique that works in an environment where only the facilities of XPG Issue 6 (POSIX.1-2001) Shell and Utilities are available. In addition, answers that use a named pipe (via mkfifo most obviously) will not be accepted, as will answers that require root privileges.

Answers that use any of the following are discouraged, but I'll listen to an argument that it's not possible to do it without one or more of them:

  • anything that creates a machine-language executable (e.g. c99)
  • anything intended primarily for interactive use (e.g. vi)
  • anything that's not part of the "Base" spec in Issue 6, or is obsolescent in either Issue 6 or any later revision of POSIX (e.g. uucp)

I'll also listen to an answer that makes a convincing argument that this is impossible within the restrictions above.

(Why no named pipes? Because they don't behave quite the same as normal pipes, particularly with the older, buggier OSes where "just use #!/bin/bash" isn't an option. Also, exposing the pipe in the file system, however briefly, means you cannot completely exclude the possibility of some unrelated process opening the pipe.)


Solution

  • Does it have to be backgrounded per se, and can you tolerate a different parent process?

    #! /bin/sh
    
    if [ -z "$REINVOKED" ]; then
      # First time we're running.
      # Save our stdout and launch some-program on a pipe connected to
      # our second invocation.  (A new shell will replace this one.)
      exec 4>&1
      REINVOKED=true
      export REINVOKED
      exec /bin/sh -c "$0 | some-program"  # <- WARNING
      exit 1
    fi
    
    # Second invocation.
    #
    exec 3>&1    # dup the pipe (our stdout) to fd3
    exec 1>&4    # restore stdout from inherited fd4
    exec 4>&-    # close fd4
    
    echo "READY"     # goes to stdout
    echo "READY" >&3 # goes to some-program
    ...
    

    That dodgy environment variable usage to detect a second invocation of the script could be more robustly reimplemented as separate scripts. The WARNING line is where you will need to watch future revisions for command injection (CWE-78).

    The processes ancestry will look like this:

    /bin/sh -c "/rather/dodgy.sh | some-program"
       |
       +- /bin/sh -c /rather/dodgy.sh
       |
       +- some-program
    

    All in the same process group, and presumably spawned from your interactive shell (-bash ?).