Quoting the POSIX specification for the sh
language:
2.7.6 Duplicating an Output File Descriptor
The redirection operator:
[n]>&word
shall duplicate one output file descriptor from another, or shall close one. If word evaluates to one or more digits, the file descriptor denoted by n, or standard output if n is not specified, shall be made to be a copy of the file descriptor denoted by word; if the digits in word do not represent a file descriptor already open for output, a redirection error shall result; see Consequences of Shell Errors. If word evaluates to '-', file descriptor n, or standard output if n is not specified, is closed. Attempts to close a file descriptor that is not open shall not constitute an error. If word evaluates to something else, the behavior is unspecified.
(emphasis mine).
So >&-
is short for 1>&-
¹, 1 being the file descriptor for stdout.
So in:
cmd >&-
The shell does close(1)
in the process in which it later executes cmd
. So cmd
will have its fd 0 and 2 likely open upon start (as those are stdin and stderr respectively, which like stdout are generally and by convention always upon on something as that's where commands expect to take input by default and send errors by default), but fd 1 closed. File descriptors 3 and above will likely also be closed.
What that would mean is that if ever cmd
opened some file (as in open("some-file", O_WRONLY, 0600)
), it would be opened on the first free file descriptor and that fd would be 1! Meaning that some-file
would become where its standard output goes thereafter. For instance, if it did printf("Please enter your name: ")
later on, that would go into some-file
!
That used to be (decades a go) a way to exploit setuid executables.
For instance, chsh
is a setuid executable that edits /etc/passwd
to change the login shell of a user upon their request.
With:
chsh >&-
chsh
could end up opening a /etc/passwd
on fd 1 and the message it intends to send to the user be written there.
Realising that issue, software started to guard against that by, upon startup, check if fd 0, 1 or 2 are closed and if so, open them on /dev/null
instead.
I seem to remember that's what the GNU libc (that is used by any executable on GNU systems) used to do when it detected it was running as a setuid executable or other sensitive context at least, but I can't verify it now, so either I misremember or they've changed the behaviour.
In any case, if you look at the source code of GNU tee
now, upon opening the files for writing, it uses a fopen_safer()
variant of fopen()
from gnulib (not GNU libc) which if the fd used by fopen()
is <= 2, moves it to a fd above 2.
So in tee aa bb cc >&-
, with that implementation of tee
at least, aa
will be open on the first fd above 2 (likely 3), bb
on the next one and so on and 1 remains closed, so we see:
$ echo test | tee aa bb cc >&-
tee: 'standard output': Bad file descriptor
Where tee
reports an error when it can't write on fd 1 (stdout) as it's closed.
With the tee
from busybox, which doesn't use that fopen_safer()
from gnulib,
$ echo test | busybox tee aa bb cc >&-
$ cat aa
test
test
aa
was actually upon on fd 1, which explains why test
was written twice in it: once because it was written on stdout (fd 1) and once because it was written to the fd on which aa
was opened (fd 1 as well).
I hope it makes it clear that if you want to discard tee
's standard output (or any command's), you should not close it, but redirect it to /dev/null instead:
echo test | tee aa bb cc > /dev/null
Though, here, rather than discarding it, you could do:
echo test | tee aa bb > cc
echo test | tee aa bb cc >&-
Would only work with those tee
implementations that detect stdout is closed and take it upon themselves to reopen it on /dev/null as a safeguard which as seen above is neither the case of GNU tee
nor busybox tee
(at least when linked to current versions of the GNU libc).
Some of the reasons you might want to close stdout (or stdin or stderr):
exploit setuid/setgid or more generally software that gets its privileges elevated upon execution that don't behave properly in those pathological situations.
test your software to check it behaves OK in those situations.
in zsh
, cmd1 > file | cmd2
sends the output of cmd1
to both file
and a pipe to cmd2
(similar to cmd1 | tee file | cmd2
except the tee
ing is done internally and is more flexible.
That gets in the way if you want to send cmd1
's stderr to cmd2
and stdout to file
as cmd1 2>&1 > file | cmd2
would send stderr
to cmd2
but stdout to both file
and cmd2
.
That tee
ing can be disabled by doing:
cmd1 2>&1 >&- > file | cmd2
Though you could also do:
{ cmd1 2>&1 > file; } | cmd2
Or
cmd1 > file 2> >(cmd2)
¹ or 1<&-
for that matters, the only difference between >&-
and <&-
being the fd they operate on when not specified (1 and 0 respectively). They both just do a close(fd)
.