-1

I wrote code below

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {

    int fd = 3;
    char c[100] = "Testing\n";
    ssize_t nbytes = write(fd, (void *) c, strlen(c));
    return 0;
}

compiled/linked, and executed

$ ./io
$ ./io 3> io_3.txt

The first line produced no output. The second line gave me file io_3.txt containing Testing. This is all expected behaviour (I guess).

Even if in my tests it produced the expected output, I am not certain if, to avoid potential problems, undefined behavior, etc., I should do anything prior to the first write, like checking if fd=3 is in use (and in that case, how... this may apply), if it is suitably open, etc.

And I am not certain if I should perform some action after the last write, for the same reasons.

Perhaps the way I did is "non-risky", the only potential issue being that nothing is written, which I could detect by checking the value of nbytes... I wouldn't know.

Any clarification is welcome.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
  • You must pass an open file descriptor. So normally you would use `open` or `socket`, or any other valid file descriptor. In this case you're depending on your shell interpreter to do some magic for you before executing the program. That is not portable. – Cheatah May 18 '20 at 20:33
  • 1
    There's no UB. Syscalls with unopened filedescriptor arguments return `EBADF`. Checking the error (negative return value and errno) would have revealed that. – Petr Skocik May 18 '20 at 20:37
  • Redirecting fd 3 to a file is fine. But if fd is not open for writing, errno is set to [EBADF](https://linux.die.net/man/2/write) – Tony Tannous May 18 '20 at 20:40
  • @PSkocik - Right!! Even if the text is actually written to the redirection target, nbytes is set to -1 and errno to EBADF. – sancho.s ReinstateMonicaCellio May 18 '20 at 21:11

1 Answers1

4

If you write a program like this, executing it without fd 3 open is a usage bug. Normally the only file descriptors that should be used by number without having opened them yourself are 0 (stdin), 1 (stdout), and 2 (stderr). If a program needs to take additional pre-opened file descriptors as input, the standard idiom is to pass the fd numbers on the command line or environment variables rather than hard-coding them. For example:

int main(int argc, char **argv) {
    if (argc<2 || !isdigit(argv[1][0])) return 1;
    int fd = strtol(argv[1], 0, 0);
    char c[100] = "Testing\n";
    ssize_t nbytes = write(fd, (void *) c, strlen(c));
    return 0;
}

In practice, a trivial program like yours is probably safe with the write just failing if fd 3 wasn't open. But as soon as you do anything that might open file descriptors (possibly internal to the implementation, like syslog, or date/time functions opening timezone data, or message translation catalogs, etc.), it might happen that fd 3 now refers to such an open file, and you wrongly attempt a write to it. Using file descriptors like this is a serious bug.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • Great. As I understand it, your expanded code gets the FD number from argv, but otherwise still writes to an FD which is not guaranteed to be open, and thus may have the same issues you describe. Is this correct? – sancho.s ReinstateMonicaCellio May 18 '20 at 20:58
  • Yes, that's correct. You could probe (e.g. with `fcntl(fd, F_GETFD)`) whether the fd is bad very early, before performing any other operations, and error out on erroneous input. But this becomes harder to do with a nontrivial program that may have ctors doing things that cause file descriptors to be opened. Generally unless it's a security boundary (e.g. suid program) I would just trust that the caller honored the contract. – R.. GitHub STOP HELPING ICE May 19 '20 at 00:23