0

I wrote a (simplistic) wrapper that executes another process in a child process. The child process closes (or redirects) standard error before it calls exec(). However, in case exec() fails, I want an error message to be printed on the parent process' standard error.

Below is my current solution. I dup()licate standard error before the fork. Since I want to be able to do formatted I/O, I call fdopen() on the duplicated handle. (I know that, as it stands, this program makes little sense, but I have only retained the relevant parts for my question.)

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int status;
    FILE *log = fdopen(dup(2), "w");
    switch (fork()) {
        case -1:
            fprintf(log, "fork: %s\n", strerror(errno));
            break;
        case 0:
            close(2);
            execvp(argv[1], &argv[1]);
            fprintf(log, "execvp: %s\n", strerror(errno));
            break;
        default:
            wait(&status);
            break;
    }
    return 0;
}

My question is now: what are the pitfalls of using the FILE * variable log, both in the parent and the child process? I've read the IEEE Std 1003.1-2008 Standard, XSH, Sections 2.5 and 2.5.1 ("Interaction of File Descriptors and Standard I/O Streams"), and from what I've understood, this should work, except that I may need to add fflush(log) right before the fork if standard error happened to be buffered. Is that correct? Are there other pitfalls to be aware of?

Also, the same standard states in Section 2.5 that

The address of the FILE object used to control a stream may be significant; a copy of a FILE object need not necessarily serve in place of the original.

Can I take this to mean that a FILE object inherited across fork() is safe to use?

I should add that I do not intend to do anything in the parent process between fork() and wait().

Edward
  • 486
  • 4
  • 14
  • "*Each file descriptor in the child refers to the same open file description as the corresponding file descriptor in the parent.*" (`man fork`) – David C. Rankin Sep 26 '17 at 04:21
  • 1
    You may also want to see [**In C, are file descriptors that the child closes also closed in the parent?**](https://stackoverflow.com/questions/35447474/in-c-are-file-descriptors-that-the-child-closes-also-closed-in-the-parent) (and `stderr` is not buffered by default) – David C. Rankin Sep 26 '17 at 04:50
  • 1
    I found a very peculiar corner case that I analysed in [this answer](https://stackoverflow.com/a/45657105/918959). TL;DR - yes, you want to `fflush` every shared stream before the fork - **including the input streams**. Note that the behaviour of flushing an input stream was specified only as recently as in POSIX.1-2008... – Antti Haapala -- Слава Україні Sep 26 '17 at 05:46
  • @DavidC.Rankin thank you for the tips; my question was specifically about the safety (or not) of using FILE *streams*. There is plenty of information out there on the interaction of file descriptors and fork() but hardly anything on stdio streams. – Edward Sep 26 '17 at 08:12
  • @AnttiHaapala thank you for the nugget of wisdom, exactly the sort of thing I was looking for. Thorough analysis of that problem, by the way! – Edward Sep 26 '17 at 08:14
  • I wasn't completely clear on the scope of your question. I suspect you are looking for a collection of corner cases to avoid -- which unfortunately I don't know of a concise reference for -- it will most likely be a case-by-case set of issues like reported by @AnttiHaapala above. – David C. Rankin Sep 26 '17 at 08:18
  • @DavidC.Rankin oh it's all right; I've already received quite useful feedback (including yours) so I'm happy :) – Edward Sep 26 '17 at 08:23
  • 1
    @Edward *my question was specifically about the safety (or not) of using FILE streams.* Close-on-exec is **highly** relevant. `FILE`-based streams are built on top of a standard file descriptor. If the file descriptor underlying your stream has the `FD_CLOEXEC` flag set, that descriptor will be closed on `exec()` and the `FILE` stream will be invalidated in a dangerous manner - the file descriptor value that *used to be for the `FILE` can be reused for something else*, causing the stream to silently access something completely different. – Andrew Henle Sep 26 '17 at 09:46
  • 1
    There are many reasons why avoiding stdio I/O, and using low-level POSIX I/O, would be a much better option here. Personally, I use a very simple wrapper around `write()` in this exact case (also using `fcntl()` to mark the duplicated descriptor `O_CLOEXEC`, so that it does not remain open for the new executable). This approach also allows porting the code to Mac OS X, where the environment between a `fork()` and an `exec()` is kind-of limited to async-signal-safe functions. – Nominal Animal Sep 26 '17 at 17:11
  • 1
    Using [`dprintf(descriptor, ...)`](http://man7.org/linux/man-pages/man3/dprintf.3.html) is another option (in POSIX.1-2008), for using the low-level I/O, but I'm not exactly clear which C libraries implement it. – Nominal Animal Sep 26 '17 at 17:15
  • @AndrewHenle I hadn't thought of that; thank you for raising this! – Edward Sep 28 '17 at 04:39
  • @NominalAnimal Nice tips, thank you! Judging by the feedback received, it would be wise to avoid stdio I/O altogether, it seems. – Edward Sep 28 '17 at 04:41

1 Answers1

3

Aside from the need to fflush before the fork, you should also consider the case where the standard error might not be a console/pipe, but a regular file.

If you write to a regular file from two file descriptors (and that's what your parent and child now have), the writes might overlap each other. If that happens, you may lose information.

I suggest using fcntl(F_SETFL) to make sure the file descriptor is in O_APPEND mode. In that mode, two processes can write to the same file and not overwrite each other.

Even in O_APPEND, if the writes are too big, the outputs might overlap. Since you're using buffered IO (which is what FILE* is), this is less likely, but not impossible. At this point your best bet is to just take that chance, and also to fflush relatively often, so that the buffers sent to write are not too big.

Shachar Shemesh
  • 8,193
  • 6
  • 25
  • 57
  • A very useful addition indeed; precisely the sort of thing I was looking for. – Edward Sep 26 '17 at 08:27
  • 2
    Even doing all that won't absolutely guarantee that `FILE *`-based streams won't corrupt data when two processes are writing to the same file. Any library call to `fwrite()` or `fprintf()` or similar can translate to one *or more* `write()` system calls to the file, thus allowing data to be intermixed even in append mode. There's also no guarantee that a single `fflush()` library call translates to a single `write()` system call, leading to the same problem. You're at the mercy of implementation details. – Andrew Henle Sep 26 '17 at 09:40