29

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?

oguz ismail
  • 1
  • 16
  • 47
  • 69

1 Answers1

34

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 duping 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
John Kugelman
  • 349,597
  • 67
  • 533
  • 578