Search code examples
c++node.jswinapiipcchild-process

Windows: passing a file handle to child process as an osfhandle


I'm trying to implement interoperability between a host process written in C++ and a child process which is a Node.js instance on Windows. I do not have control on the child process's IPC API, I have to conform to what Node.js does.

Node.js when used as a server, creates a Windows named pipe object and passes this to the child process as file descriptor 3:

I could confirm by spawning many processes that this fd (also passed in the environment as NODE_CHANNEL_FD) are always equal to 3 when in the child process.

Given the following Node.js program:

const { spawn } = require('child_process');
const child = spawn('child.exe', [], {
   stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
   env: env
});

child.on('message', (message) => {
  let m = JSON.stringify(message);
  console.log(`message from child: ${m}`)
});
child.send({foo: 123, bar: "456"});

Then from a C++ child.exe:

int main(int argc, char **argv)
{
  HANDLE h = _get_osfhandle(3);
  ... 
  WriteFile(h, etc...);
  ...
  ReadFile(h, etc...);
}

gives the expected results, the apps can communicate.

Now, I am trying to do the reverse: from a C++ host, create and pass the right data structures to make sure that the right thing ends up as fd 3 in the child process's fd table. Sadly I am unable to replicate this:

On the parent process, I create my pipe like this:

static constexpr auto PIPENAME = "\\\\?\\pipe\\echo.sock";

OVERLAPPED overlapped = {0};
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof sa;
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;

HANDLE serverPipeHandle = CreateNamedPipeA(PIPENAME,

                                    PIPE_ACCESS_DUPLEX
                                  | FILE_FLAG_OVERLAPPED
                                  | WRITE_DAC
                                  | FILE_FLAG_FIRST_PIPE_INSTANCE,

                                    PIPE_TYPE_BYTE
                                  | PIPE_READMODE_BYTE
                                  | PIPE_WAIT,

                                    PIPE_UNLIMITED_INSTANCES,
                                    65536,
                                    65536,
                                    0,
                                    &sa);

if (serverPipeHandle == INVALID_HANDLE_VALUE) {
    printf("CreateNamedPipe failed, GLE=%lu.\n", GetLastError());
    return 1;
}

HANDLE client_pipe;
client_pipe = CreateFileA(PIPENAME,
                          GENERIC_READ | GENERIC_WRITE | WRITE_DAC,
                          0,
                          &sa,
                          OPEN_EXISTING,
                          FILE_FLAG_OVERLAPPED,
                          NULL);

if (!ConnectNamedPipe(serverPipeHandle, NULL)) {
    if (GetLastError() != ERROR_PIPE_CONNECTED) {
        printf("ConnectNamedPipe failed, GLE=%lu.\n", GetLastError());
    }
}

I can print the unix-like file handles:

auto h = _open_osfhandle((intptr_t) serverPipeHandle, 0);

// Prints: Handle=276 (or sometimes 260 or close numbers) 
fprintf(stderr, "Handle=%llu \n", (std::uintptr_t) serverPipeHandle);
// Always prints: fd=3 
fprintf(stderr, "fd=%d \n", h);

Then I create my process:

STARTUPINFOA si = {0};
ZeroMemory(&si, sizeof(STARTUPINFOA));
si.cb = sizeof(si);

PROCESS_INFORMATION pi = {0};
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

CreateProcess(NULL, app, NULL, NULL,        
              TRUE, // handle inheritance
              CREATE_SUSPENDED, NULL, NULL,
              &si, &pi);

ResumeThread(pi.hThread);

But now, in the child process, fd 3 is not available. What am I missing to make it available?

I tried:

  • Marking the handles explicitly as inherit before creating the process:
SetHandleInformation(serverPipeHandle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
SetHandleInformation(client_pipe, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
  • Calling DuplicateHandle after CreateProcess and before ResumeThread:
HANDLE hFileDup;
DuplicateHandle(GetCurrentProcess(), 
                serverPipeHandle,   
                pi.hProcess,         
                &hFileDup,           
                0,    
                TRUE, 
                DUPLICATE_SAME_ACCESS);
for (int i = -1000; i < 1000; i++) {
  auto hFile = (HANDLE) _get_osfhandle(i);
  if (hFile != INVALID_HANDLE_VALUE) {
    fprintf(stderr, "Trying %d ...", i);
    fprintf(stderr, "success! \n");
  }
}

This prints:

Trying 0 ...success! 
Trying 1 ...success!
Trying 2 ...success!

So only stdin, stdout, stderr but not any other of my handle. (interestingly, these numbers do not seem to match the values of STD_OUTPUT_HANDLE, STD_OUTPUT_HANDLE etc. : https://learn.microsoft.com/en-us/windows/console/getstdhandle).

  • Passing the raw HANDLE value to my child process (e.g. 260 instead of 3). This works, but sadly does not match the node.js api.

Thus I am at loss, what are my options to make sure that my serverPipeHandle correctly ends up as fd=3 in my child process?


Solution

  • I managed to do it through the undocumented API that MSVCRT uses to load the file descriptors:

    #define FOPEN 0x01
    #define FPIPE 0x08
    #define FDEV 0x40
    
    STARTUPINFOA si = {0};
    
    ...
    
    struct {
      int count = 4;
      unsigned char flags[4];
      HANDLE handles[4];
    } fd_buf{.count = 4,
             .flags = {FOPEN | FDEV, FOPEN | FDEV, FOPEN | FDEV, FOPEN | FPIPE},
             .handles = {si.hStdInput, si.hStdOutput, si.hStdError, serverPipeHandle}};
    si.lpReserved2 = (unsigned char *) &fd_buf;
    si.cbReserved2 = sizeof(fd_buf);