Why don’t I get a broken pipe when the child process terminates?

A customer was having a problem with named pipes.

Our main process creates a named pipe with ACCESS_INBOUND and passes the write handle to the child process. The main process keeps reading from the pipe until it gets ERROR_PIPE_BROKEN. We expect that when the child process terminates, the main process will get the ERROR_PIPE_BROKEN error. However, we are finding that sometimes the main proecss doesn't get the ERROR_PIPE_BROKEN error, even though the child process has definitely terminated. Are there cases where the process with the write end of the pipe terminates, but the read doesn't error out with ERROR_PIPE_BROKEN?

You won't get ERROR_PIPE_BROKEN until all the write handles are closed. One common reason why you don't get the error is that there's still a write handle open in the parent process. Another possibility is that the child process launched a subprocess which inherited the write handle, or more generally, the handle got duplicated into another process by some means.

The customer wrote back.

Thanks. That is indeed the issue. The main process spawns many child processes simultaneously, so depending on race conditions, the write handle for one pipe could inadvertently be inherited by an unrelated child process. We could explicitly serialize our Create­Process calls, but is there another way to specify that a child process should inherit only certain handles and not others?

Yes. You can use the PROC_THREAD_ATTRIBUTE_LIST structure to exercise finer control over which handles are inherited.

Comments (7)
  1. Kevin says:

    On Unix, fixing the customer’s problem would be technically possible, but far more complicated than it needs to be. The short version is that you always open all file descriptors with O_CLOEXEC or the moral equivalent, and then when you need to inherit them, you do a fork(2) (which always inherits all file descriptors) and then use fcntl(2) to un-CLOEXEC the ones you really do want to inherit before you call one of the exec* functions.

    Naturally, if there are libraries involved which don’t respect this pattern, or if you want to do fork-without-exec, you are screwed. The Good News is that O_CLOEXEC is a reasonably safe default, so a number of high-level programming languages will set it for you. This reduces the error surface somewhat, but unfortunately a lot of Unix libraries are written in C to maximize interoperability.

    Meanwhile, cursory Googling shows that fork-without-exec leaves the child process in an infelicitous state under a variety of circumstances not involving file descriptor inheritance (typically because some mutex or other synchronization primitive gets stuck when its owning thread vanishes), and is also nontrivial to port to Windows. I’m inclined to call it a lost cause.

    1. Joshua says:

      I wrote the code on the child side of fork() to clean up all unwanted handles. (Enum over open handles in /process gets the job done.) Your O_CLOEXEC doesn’t work too well because pipe() doesn’t set it. :(

      1. Joshua says:

        Gee my phone autocotrect doesn’t know /proc yet.

      2. Cesar says:

        You should use pipe2() instead of pipe().

    2. Lars Viklund says:

      Depending on who spawns your initial process, you may have file descriptors you don’t expect.

      There was a fun incident with node.js and GNU Make a while ago. Make has a “makeserver” which communicates with the initial Make process to coordinate the starting of jobs to adhere to the specified parallelism count. This communication is done via an inherited FD which normally passes unharmed through all child processes.

      The call chain here was something like Make -> node.js -> Make. This would be fine and properly job rate limited, except that node.js explicitly looped over the first N file descriptors and closed them for some obscure reason I don’t remember. The end result was a lovely explosion of make processes, each thinking it had free reigns to spawn -j tasks.

  2. Neil says:

    I’m slightly curious as to why you don’t get ERROR_HANDLE_EOF in such a case.

    1. Joker_vD says:

      Because nobody closed it yet, that’s how pipes and sockets works: you get EOF only after the sending end has accurately closed the writing side of it.

Comments are closed.

Skip to main content