linux - Does 3>&1 imply 4>&3 5>&3 etc.?




bash shell (2)

I'd expect

echo foo | tee /proc/self/fd/{3..6} 3>&1

to fail with errors like /proc/self/fd/4: No such file or directory etc., but to my surprise, it outputs

foo
foo
foo
foo
foo

It's like 3>&1 causes all following descriptors to be redirected to stdout, except it doesn't work if I change 3 to something else, like

$ echo foo | tee /proc/self/fd/{3..6} 4>&1
tee: /proc/self/fd/3: No such file or directory
tee: /proc/self/fd/5: No such file or directory
tee: /proc/self/fd/6: No such file or directory
foo
foo
$ echo foo | tee /proc/self/fd/{4..6} 4>&1
tee: /proc/self/fd/5: No such file or directory
tee: /proc/self/fd/6: No such file or directory
foo
foo

Is there an explanation for this behavior?


A tee bug?

John Kugelman's answer is fine, but as problem is complex, I will go a little further:

bash -c 'exec 2> >(exec sed -ue "s/^/StdErr: /");
    exec 1> >(exec sed -ue "s/^/StdOut: /");
    tee /dev/fd/{2..6} <<<foo'
StdErr: foo
StdOut: foo
StdErr: foo
StdErr: foo
StdErr: foo
StdErr: foo

Showing input ( foo ) muliplied 5x on* STDERR * AND 1x on STDOUT . So all /dev/fd/{3..6} are binded to /dev/fd/2 .

strace -o >(grep dev/fd) -e openat tee /dev/fd/{2..6} <<<foo 4>/dev/null
foo
foo
foo
foo
openat(AT_FDCWD, "/dev/fd/2", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
openat(AT_FDCWD, "/dev/fd/3", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 5
openat(AT_FDCWD, "/dev/fd/4", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
openat(AT_FDCWD, "/dev/fd/5", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 7
openat(AT_FDCWD, "/dev/fd/6", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 8

So tee produce 4x input on tty instead of 5 ( 2..6 + STDOUT = 6 , - 1x /dev/null => 5 attended foo ):

  1. Access /dev/fd/2 , openning file descriptor 3 ... and so create /dev/fd/3 , then
  2. Access /dev/fd/3 , wich exist now, from previous operation, openning file descriptor 5 (because /dev/fd/4 is binded to /dev/null by command line... and so create /dev/fd/5 , then
  3. Access /dev/fd/4 (binded to /dev/null by command line) and create /dev/fd/6 , then
  4. Access /dev/fd/5 , wich exist now, (binded to 3, binded to 2...), then
  5. Access /dev/fd/6 , wich exist now, but binded to /dev/fd/4 wich is binded to /dev/null .

Expected output could be:

tee /dev/fd/{2..6} <<<foo
foo
tee: /dev/fd/3: No such file or directory
tee: /dev/fd/4: No such file or directory
tee: /dev/fd/5: No such file or directory
tee: /dev/fd/6: No such file or directory
foo

With ouptut 1 time on STDERR and 1 time on STDOUT, and 4 error lines.

So tee don't check existence of targets before begin. But is this a bug!?

As openining a file in write mode could be ok even if file don't exist (creating new file).

Reading Time-of-check Time-of-use (TOCTOU) Race Condition pointed by John Kugelman's comment help understanding why doing pre-check is a wrong good idea .

So if a bug, I think refering /dev/fd/ directly by tee is THE bug here (on command line). ... Not a tee bug, just a buggy user ;-)


strace shows this sequence of system calls:

$ strace -o strace.log tee /proc/self/fd/{3..6} 3>&1
...
$ cat strace.log
...
openat(AT_FDCWD, "/proc/self/fd/3", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
openat(AT_FDCWD, "/proc/self/fd/4", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 5
openat(AT_FDCWD, "/proc/self/fd/5", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
openat(AT_FDCWD, "/proc/self/fd/6", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 7
...

The first line opens /proc/self/fd/3 and assigns it the next available fd number, 4. /proc/self/fd/3 is a special path. Opening it has an effect similar to dup ing fd 3: fd 4 points to the same place as fd 3, the tty.

The same thing happens for each successive openat() call. When the dust settles fds 4, 5, 6, and 7 are all duplicates of fd 3.

  • 1 → tty
  • 3 → tty
  • 4 → tty
  • 5 → tty
  • 6 → tty
  • 7 → tty

Note that the 3>&1 redirection isn't important. What's important is that we're asking tee to open /proc/self/fd/N where N is already in use. We should get the same result if we get rid of 3>&1 and have tee start at /proc/self/fd/2 instead. Let's see:

$ echo foo | tee /proc/self/fd/{2..6}
foo
foo
foo
foo
foo
foo

Confirmed! Same result.

We can also repeat the same fd number over and over. We get the same result when we hit fd 6. By the time it reaches the last one it has opened enough descriptors to make the jump to 6 possible.

$ echo foo | tee /proc/self/fd/{2,2,2,2,6}
foo
foo
foo
foo
foo
foo




proc-filesystem