We’ve traced the pipe, and it’s coming from inside the process!

We saw last time one of the deadlocks you can run into when playing with pipes. Today we'll look at another one:

Our program runs a helper process with stdin/stdout/stderr redirected. The helper process takes input via stdin and prints the result to stdout. Sometimes we find that the Write­File from the controlling process into the stdin pipe hangs. Closer examination reveals that the helper process no longer exists. Under these conditions, should the Write­File fail, since the reader is no longer available?

If you attempt to write to a pipe when there is nobody around to call Read­File to read the data out the other end, the call to Write­File should fail with the error ERROR_BROKEN_PIPE (known in Unix-land as EPIPE). What does it mean when the write pends? It means that there is still somebody around who can read the data out of the pipe, but the internal pipe buffer is full, so the write call waits for the reader to drain the data.

But the helper process no longer exists. Maybe it crashed or exited prematurely. That means that there is nobody around to read the data out of the pipe. Why, then, does the call not return immediately with an error?

Because there is still somebody around to read the data out of the pipe.

Did you remember to close the controlling process's copy of the read end of the pipe?

If the controlling process hasn't closed its copy of the read end of the pipe, then the pipe is correct in believing that there is still somebody around to read the data out of the pipe, namely you. You have a handle to the read end of the pipe, so the pipe manager cannot declare the pipe dead; for all it knows, you intended for the controlling process to call Read­File to read the data out of the pipe. As far as the pipe is concerned, you simply haven't gotten around to it yet, so the pipe waits patiently.

Yes, our code calls Close­Handle on the controlling process's copy of the pipe handles. I've highlighted it below. (Error checking has been elided for simplicity.)

// create the pipe for stdout/stderr
CreatePipe(&hReadPipeTmp, &hWritePipeTmp, NULL, 0);

// duplicate the handles with bInheritHandle=FALSE to prevent
// them from being inherited
DuplicateHandle(GetCurrentProcess(), hWritePipeTmp,
                GetCurrentProcess(), &hWritePipe,
                0, FALSE, DUPLICATE_SAME_ACCESS);
DuplicateHandle(GetCurrentProcess(), hReadPipeTmp,
                GetCurrentProcess(), &hReadPipe,
                0, FALSE, DUPLICATE_SAME_ACCESS);

// create the pipe for stdin
CreatePipe(&hHelperReadPipe, &hHelperWritePipe,
           NULL, 0);

// disable inheritance on on the write end of the stdin pipe
SetHandleInformation(hHelperWritePipe, HANDLE_FLAG_INHERIT, 0);

// prepare to create the process
... blah blah blah other stuff unrelated to handles ...
startupInfo.hStdInput = hHelperReadPipe;
startupInfo.hStdOutput = hWritePipeTmp;
startupInfo.hStdError = hWritePipeTmp;


// Here is where we close the handles

// Write the input to the helper process (hangs here sometimes)
WriteFile(hHelperWritePipe, ...);

This is another case of getting so excited about doing something that you forget to do it. (Notice how the comments to that article very quickly descend into a discussion of command line quotation marks.)

Observe that the handles being closed are hRead­Pipe­Tmp and hWrite­Pipe­Tmp, which is a good thing to do, but neither has any effect on the Write­File. The Write­File is writing to hHelper­Write­Pipe and therefore the handle you need to close is hHelper­Read­Pipe. Since that handle is still open in the controlling process, the pipe manager will not break the pipe, because it's waiting for you to read from it.

Comments (5)
  1. Anonymous says:

    Why are they duplicating the handles?  The docs for CreatePipe say that the handle (it should say handles, but I assume that's a typo) which it returns cannot be inherited when NULL is passed for the security attributes.

  2. Anonymous says:

    They are duplicating the handles so as to set the security attributes on the duplicated copy so it can be inherited.

  3. Joshua Ganes says:

    Ahh… process spawning and duplicate pipe handles. Those will throw a monkey wrench in your gears. Actually, the analogy is pretty close. The whole thing locks up if you're not careful about the way you do it. The worst case of this is when you have a rare error case that triggers the lock. The application can run for days before a sudden, seemingly unexplained error grinds it to a halt.

  4. Anonymous says:

    Unless I'm misreading something, they're missing the point even worse than what Adam and Joshua noticed. It looks like the duplicated handles in hWritePipe is never used for anything. It is probably not even closed, meaning that we also get a handle leak going in the other direction.

  5. Anonymous says:

    @Joshua: Huh?  The comment before the calls to DuplicateHandle explicitly says that it wants to *prevent* the handles from being inherited, which is pointless if the handles are already non-inheritable.  Likewise, the subsequent call to SetHandleInformation to disable inheritance on the stdin pipe is also pointless.

    Why not just call CreatePipe twice (once for the stdin pipe, once for the stdout/stderr pipe), CreateProcess, and then CloseHandle on the read end of the stdin pipe and on the write end of the stdout/stderr pipe?

    [You're asking how come code that misses the point misses the point worse? -Raymond]

Comments are closed.