9

I'm not too knowledgeable in the Unix command line. I found this SO answer for using the tee command without outputting to stdout, by "closing stdout" with >&- like this:

echo 'hello world' | tee aa bb cc >&-

I did a lot of research and came across another post asking about the functionality of the operator. Unfortunately, I couldn't find any detailed answers.

All I know is that it closes stdout, but I'm unable to determine the exact implications of that and why someone would specifically want to do it. I'm curious to know in what other scenarios this might be useful.

Are there any resources available where I can learn more about the inner workings of operators like these?

2

1 Answer 1

11

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 teeing 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 teeing 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).

1
  • Interesting! So currently what prevents setuid exploit such as the chsh example you gave? If glibc does not check for it, does kernel / shell do anything or it just falls onto individual setuid programmers to safeguard it?
    – QnA
    Commented Jul 2, 2023 at 14:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .