3

I am working on my library which needs to capture and process the standard output (and err) of a child process as it runs. The problem arises when ReadFile is used to read the output, it does not return once the process ends (gets killed or exits).

It looks like ReadFile is not able to detect that the other end of the pipe (the write handle) is closed. According to the documentation it should return FALSE and set the last error to ERROR_BROKEN_PIPE:

If an anonymous pipe is being used and the write handle has been closed, when ReadFile attempts to read using the pipe's corresponding read handle, the function returns FALSE and GetLastError returns ERROR_BROKEN_PIPE.

Here is my code, I have stripped out the irrelevant bits: (NOTE: I have updated the allium_start to follow the suggested changes, I am keeping the original for reference, please use the newer function code to find flaws)

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
    // Prepare startup info with appropriate information
    SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
    instance->startup_info.dwFlags = STARTF_USESTDHANDLES;

    SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};

    HANDLE pipes[2];
    if (output_pipes == NULL) {
        CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
        output_pipes = pipes;
    }
    instance->startup_info.hStdOutput = output_pipes[1];
    instance->startup_info.hStdError = output_pipes[1];
    instance->stdout_pipe = output_pipes[0]; // Stored for internal reference

    // Create the process
    bool success = CreateProcessA(
        NULL,
        cmd,
        NULL,
        NULL,
        config ? true : false,
        0,
        NULL,
        NULL,
        &instance->startup_info,
        SecureZeroMemory(&instance->process, sizeof instance->process)
    );

    // Return on failure
    if (!success) return false;
}

char *allium_read_stdout_line(struct TorInstance *instance) {
    char *buffer = instance->buffer.data;

    // Process the input
    unsigned int read_len = 0;
    while (true) {
        // Read data
        unsigned long bytes_read;
        if (ReadFile(instance->stdout_pipe, buffer, 1, &bytes_read, NULL) == false || bytes_read == 0) return NULL;

        // Check if we have reached end of line
        if (buffer[0] == '\n') break;

        // Proceed to the next character
        ++buffer; ++read_len;
    }

    // Terminate the new line with null character and return
    // Special handling for Windows, terminate at CR if present
    buffer[read_len >= 2 && buffer[-1] == '\r' ? -1 : 0] = '\0';

    return instance->buffer.data;
}

The allium_start creates the pipe for output redirection (it uses the same pipe for both stdout and stderr to get merged streams) and then creates the child process. The other allium_read_stdout_line function is responsible for reading the output from the pipe and returning it when it encounters a new line.

The issue occurs at the ReadFile function call, it never returns if there is nothing to read after the process exits, from my understanding all the handles of a process are closed by Windows when it ends, so it looks like ReadFile is not able to detect the fact that the pipe (write handle) at the other end has been closed.

How do I fix this? I have been searching for a solution but I have found none so far, one potential option is to use multi-threading and put ReadFile in a separate thread so that it doesn't block the whole program, by using that method I can check if the process still exists periodically while I wait for the reading to finish... or kill/stop the thread if the process is gone.

I do prefer fixing the issue instead of opting for a workaround, but I am open to any other solutions to make it work. Thanks in advance!


Edit: After reading @RemyLebeau's answer and @RbMm's comments in that answer, it is pretty clear that my understand of how handle inheritance works is fundamentally flawed. So I incorporated their suggestions (SetHandleInformation to disable inheritance of read handle and closing it after creating the child process) into my allium_start function:

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
    // Prepare startup info with appropriate information
    SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
    instance->startup_info.dwFlags = STARTF_USESTDHANDLES;

    SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};

    HANDLE pipes[2];
    if (output_pipes == NULL) {
        CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
        output_pipes = pipes;
    }
    SetHandleInformation(output_pipes[0], HANDLE_FLAG_INHERIT, 0);
    instance->startup_info.hStdOutput = output_pipes[1];
    instance->startup_info.hStdError = output_pipes[1];
    instance->stdout_pipe = output_pipes[0]; // Stored for internal reference

    // Create the process
    bool success = CreateProcessA(
        NULL,
        cmd,
        NULL,
        NULL,
        config ? true : false,
        0,
        NULL,
        NULL,
        &instance->startup_info,
        SecureZeroMemory(&instance->process, sizeof instance->process)
    );

    // Close the write end of our stdout handle
    CloseHandle(output_pipes[1]);

    // Return on failure
    if (!success) return false;
}

(The below text was originally here before edit 2)

But sadly it still doesn't work :(

Edit 2 (after accepting answer): It does work! See my last comment on the accepted answer.

TheDcoder
  • 509
  • 1
  • 7
  • 23

2 Answers2

5

You are not managing your pipes correctly, or more specifically, you are not controlling the inheritance of your pipe handles. DO NOT let the child process inherit the reading handle of your pipe (output_pipes[0]), otherwise the pipe will not break correctly when the child process ends.

Read MSDN for more details:

Creating a Child Process with Redirected Input and Output

The case of the redirected standard handles that won’t close even though the child process has exited

Use SetHandleInformation() or PROC_THREAD_ATTRIBUTE_LIST to prevent CreateProcess() from passing output_pipes[0] to the child process as an inheritable handle. The child process does not need access to that handle, so there is no need to pass it over the process boundary anyway. It only needs access to the writing handle of your pipe (output_pipes[1]).

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 1
    Thank you for the answer, I did come across that article about creating a child process with redirected i/o, I was aware of `SetHandleInformation` and even tried it, but no avail... I even tried again now to make sure. I am not sure how the child can inherit the read handle if I am only passing it the write handle. Either way, not working :-/ – TheDcoder Jan 29 '19 at 08:43
  • 1
    I have just finished reading the other article, I am not familiar or have seen `PROC_THREAD_ATTRIBUTE_LIST` before... The article talks about a weird scenario where there 2 threads executing two different processes, but somehow the handle gets swapped (if I understood correctly). None of this should effect my program since it is fully single-threaded. – TheDcoder Jan 29 '19 at 08:52
  • 3
    @TheDcoder - you got read error with `ERROR_BROKEN_PIPE` only when the **last** handle of opposite end will be closed. but in your code this never will be because you keep this handle in self process too. you not close any of `pipes[2]` in self process - so never `ERROR_BROKEN_PIPE` will be even when child exit and all it handles will be closed – RbMm Jan 29 '19 at 10:08
  • Yes, it should be added that `output_pipes[1]` must be closed after `CreateProcess()` call and before any `ReadFile()` call. – zett42 Jan 29 '19 at 10:13
  • 3
    really [code from msdn](https://learn.microsoft.com/en-us/windows/desktop/procthread/creating-a-child-process-with-redirected-input-and-output) is very bad and wrong anyway (not close handles which **must** close). i not understand how many years can set as example simple wrong code. and not efficient as solution too – RbMm Jan 29 '19 at 10:20
  • @zett42 - yes, but in this case parent can only read, and child only write. good solution create duplex pipe pair, where both end can read and write. one (client) handle end make inherit, pass to child and close in self process, another create not inherited and asynchronous (for prevent deadlock) – RbMm Jan 29 '19 at 10:26
  • 4
    in concrete [msdn code](https://learn.microsoft.com/en-us/windows/desktop/procthread/creating-a-child-process-with-redirected-input-and-output) the `ReadFile( g_hChildStd_OUT_Rd,` never fail even after child process exit, because we not close `g_hChildStd_OUT_Wr`. so here not close handle not simply temporary (until process exit) resource leak but cause to hung forever. must be how minimum `CloseHandle(g_hChildStd_OUT_Wr); CloseHandle(g_hChildStd_IN_Rd);` added just after `CreateProcess` call. only after this we break `ReadFromPipe` infinite loop. but anyway solution is bad – RbMm Jan 29 '19 at 10:51
  • @RbMm Not sure I understand the reason why I should close the handle, but I did try this too, and it didn't work. To make sure, I tried it again with an immediate `CloseHandle(output_pipes[1])` call after `CreateProcess`, still not working :( – TheDcoder Jan 29 '19 at 12:47
  • @TheDcoder - *reason why I should close the handle* - because you got read error with `ERROR_BROKEN_PIPE` only when the **last** handle of opposite end will be closed. in your code you have 2 handles of this end - one in child, one in your process. if you not close your copy - pipe will be not broken – RbMm Jan 29 '19 at 12:56
  • @TheDcoder *still not working* mean only still many errors in your code – RbMm Jan 29 '19 at 12:56
  • @RbMm Isn't it 2 copies of the same handle rather than 2 copies of 2 handles pointing to the same thing? That is the logic I am currently following... – TheDcoder Jan 29 '19 at 13:20
  • @RbMm I'd be glad to post the whole code as it is an open source project, should I attach it at the end of my question? The code base itself is very simple and small, I only started writing it recently... – TheDcoder Jan 29 '19 at 13:22
  • @TheDcoder you read from `output_pipes[0]`, the opposite end for it is output_pipes[1]. the `ReadFile` from `output_pipes[0]` not return error until **all** handles to object output_pipes[1] will be closed. in current code which you paste you not close `output_pipes[1]`. so you not got `ERROR_BROKEN_PIPE` – RbMm Jan 29 '19 at 13:54
  • @RbMm The code in my question is my original code, I did test again after closing `output_pipe[1]` after I read your comments, I have mentioned the snippet in my previous comment as well, looks like you have missed it :) – TheDcoder Jan 29 '19 at 14:32
  • @TheDcoder "*I am not sure how the child can inherit the read handle if I am only passing it the write handle*" - you are creating a single pipe where *both* handles to it are inheritable, and then you call `CreateProcess()` telling it to pass ALL inheritable handles to the child. The first article uses `SetHandleInformation()` to prevent that, while the second article uses `PROC_THREAD_ATTRIBUTE_LIST` instead (ignore the 2-process example, that is just a use-case to demonstrate a race condition under a different scenario). Two different solutions to solve the same underlying problem. – Remy Lebeau Jan 29 '19 at 16:12
  • @Remy Lebeau "_you call CreateProcess() telling it to pass ALL inheritable handles to the child._" I don't think this is true, I am only passing it the write handle as far as I can see (`hWritePipe` from `CreatePipe`) – TheDcoder Jan 29 '19 at 16:40
  • @TheDcoder you are setting the `bInheritHandles` parameter of `CreateProcess()` to TRUE: "*If this parameter is TRUE, **each inheritable handle in the calling process is inherited by the new process**.*" That happens *regardless* of which handles are in the `STARTUPINFO`. ALL inheritable handles. That is why you need to control WHICH handles you actually want to be inheritable and WHICH you don't, and why there are extra API calls needed to control that behavior in this scenario. [Raymond Chen's](https://blogs.msdn.microsoft.com/oldnewthing/20111216-00/?p=8873) article descusses this in detail – Remy Lebeau Jan 29 '19 at 17:13
  • @RemyLebeau You are right, I did ponder about the implications of setting the security attributes in `CreatePipe` but didn't give it further thought. I guess my understanding of how these handles are inherited is not correct... But still, why does it not work with both of your and @RbMm's suggested fixes? I will edit my question to include the code with the fixes. – TheDcoder Jan 30 '19 at 03:22
  • It turns out that I was being dumb... too dumb infact to realize that I was re-using the same `pipes` array for stdin in my code (which I took out before posting it in the question because it was not relavent). So what was happening is that I was closing the stdin handles instead of the stdout handles after `CreateProcess`. Now my program works perfectly! Thank you very much @RemyLebeau and @RbMm :) – TheDcoder Jan 31 '19 at 04:49
  • the comment from @RbMm from Jan 29, 2019 at 10:51 helped me out! – Wolfgang Roth Dec 22 '22 at 10:40
1

For anonymous pipelines, the read process and the write process will have the handler of hRead and hWrite, each of process has its own handler(copy after inheritance). So after your child process exit and close the handler in it, anther hWrite still in parent process. We must pay attention to close hRead in the write process, close hWrite in the read process.

I can reproduce this ReadFile issue, and if closing write handler after setting child's hStdOutput and hStdError, the ReadFile will return 0 after the child process exit.

Here is my code sample, Parent.cpp:

#include <windows.h> 
#include <iostream>
#include <stdio.h>

HANDLE childInRead = NULL;
HANDLE W1 = NULL;
HANDLE W2 = NULL;
HANDLE R2 = NULL;

HANDLE R1 = NULL;

#define BUFSIZE 4096

void CreateChildProcess() {
    TCHAR applicationName[] = TEXT("kids.exe");
    PROCESS_INFORMATION pi;
    STARTUPINFO si;
    BOOL success = FALSE;

    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&si, sizeof(STARTUPINFO));

    si.cb = sizeof(STARTUPINFO);
    si.hStdError = W1;
    si.hStdOutput = W1;
    si.hStdInput = R2;
    si.dwFlags |= STARTF_USESTDHANDLES;

    success = CreateProcess(NULL, applicationName, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

    if (!success) {
        printf("Error creating child process \n");
    }
    else {
        printf("Child process successfuly created \n");
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
}

int main()
{
    printf("Parent process running.... \n");

    DWORD dRead, dWritten;
    CHAR chBuf[BUFSIZE] = { 0 };
    BOOL bSuccess = FALSE;

    SECURITY_ATTRIBUTES secAttr;
    secAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    secAttr.bInheritHandle = TRUE;
    secAttr.lpSecurityDescriptor = NULL;

    printf("Creating first pipe \n");

    if (!CreatePipe(&R1, &W1, &secAttr, 0)) {
        printf("\n error creating first pipe \n");
    }
    printf("Creating second pipe \n");

    if (!CreatePipe(&R2, &W2, &secAttr, 0)) {
        printf("\n error creating second pipe \n");
    }

    if (!SetHandleInformation(R1, HANDLE_FLAG_INHERIT, 0)) {
        printf("\n R1 SetHandleInformation \n");
    }
    if (!SetHandleInformation(W2, HANDLE_FLAG_INHERIT, 0)) {
        printf("\n W1 SetHandleInformation \n");
    }

    printf("\n Creating child process..... \n");

    HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
    HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
    CreateChildProcess();
    CloseHandle(W1);
    CloseHandle(R2);
    for (;;) {
        printf("Inside for loop \n");

        //1. read from stdin
        printf("read from stdin:\n");
        bSuccess = ReadFile(hStdIn, chBuf, BUFSIZE, &dRead, NULL);
        if (!bSuccess) {
            printf("error reading \n");
            break;
        }


        //2. write to Pipe2
        printf("write to Pipe2...\n");
        bSuccess = WriteFile(W2, chBuf, 100, &dWritten, NULL);
        if (!bSuccess) {
            printf("error reading \n");
            break;
        }

        //3. read from Pipe1
        printf("read from Pipe1...\n");
        bSuccess = ReadFile(R1, chBuf, BUFSIZE, &dRead, NULL);
        if (!bSuccess)
        {
            printf("error reading :%d \n", GetLastError());
            break;
        }

        //4. write to stdout
        printf("write to stdout:\n");
        bSuccess = WriteFile(hStdOut, chBuf, 100, &dWritten, NULL);
        if (!bSuccess) {
            printf("error reading \n");
            break;
        }
    }
    getchar();
    return 0;
}

Kids.cpp:

#include <windows.h>
#include <stdio.h>

#define BUFSIZE 4096

int main()
{
    DWORD dRead, dWritten;
    CHAR chBuf[BUFSIZE];
    BOOL success = FALSE;
    HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE);
    HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);

    printf("Child process running....");

    if (stdIn == INVALID_HANDLE_VALUE || stdOut == INVALID_HANDLE_VALUE) {
        ExitProcess(1);
    }

    //for (;;) {
        success = ReadFile(stdIn, chBuf, BUFSIZE, &dRead, NULL);
        //if (!success || dRead == 0) break;

        success = WriteFile(stdOut, chBuf, dRead, &dWritten, NULL);
        //if (!success) break;
    //}
    return 0;
}
Drake Wu
  • 6,927
  • 1
  • 7
  • 30
  • "_the read process and the write process will have the handler of hRead and hWrite, each of process has its own handler(copy after inheritance)_" Ah, so the handles are duplicated instead of being just copied by value? If this is true then I understand the reason why I would have to close the ones which I don't use in the parent. By the way, you forgot to close the read handle for stdin (`R2`) as you don't need it in the parent... – TheDcoder Jan 30 '19 at 03:50
  • You've got it. Yes, close now. – Drake Wu Jan 30 '19 at 05:10
  • Thanks for the explanation, so it is similar to how file descriptors are duplicated (off-topic)... unfortunately the issue seems to be something else. – TheDcoder Jan 30 '19 at 06:10
  • Maybe you can try NamePipe, set `FILE_FLAG_OVERLAPPED` flag, and then use `ReadFile` in Overlapped mode. – Drake Wu Jan 30 '19 at 07:19
  • I have considered this option, but I am have read somewhere than `CreatePipe` does not allow named pipes. I will try it as a last resort – TheDcoder Jan 30 '19 at 09:57
  • um... I mean [CreateNamedPipe()](https://learn.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createnamedpipea) – Drake Wu Jan 30 '19 at 10:08
  • Oops, I meant to write `CreateProcess` in my previous comment. You are correct about the function for creating named pipes. – TheDcoder Jan 30 '19 at 13:37