返回

解决Windows cmd.exe I/O重定向管道死锁问题

windows

解决Windows cmd.exe I/O重定向管道死锁问题

当使用管道重定向Windows cmd.exe的输入输出时,一个常见的问题是程序在读取stdout管道时发生死锁。当cmd.exe完成写入并且没有更多数据可用时,ReadFile调用会无限期阻塞,导致程序无法继续执行。本文将探讨此问题的原因,并提供几种解决方案。

问题分析

死锁发生的根本原因是程序在stdout管道上执行同步读取操作,而cmd.exe在没有数据可写时并不会主动关闭管道或发出结束信号。因此,ReadFile会一直等待,直到有新的数据写入管道,而cmd.exe已经结束写入,从而造成死锁。

另外,代码示例中试图通过在命令末尾附加结束标志来解决问题,虽然可行,但不够优雅,且引入了额外的复杂性,比如需要处理标志的生成和识别,安全性较差,并且容易被恶意输入利用,存在被篡改的风险,不建议作为首选方案。

解决方案

以下提供几种解决cmd.exe I/O重定向管道死锁问题的方案,并辅以代码示例和操作步骤。

1. 使用异步I/O

使用异步I/O可以避免程序在ReadFile调用时阻塞。通过使用ReadFileEx函数和重叠I/O,程序可以在后台等待数据,而不会阻塞主线程。当有数据可用时,系统会通过完成例程或事件对象通知程序。

代码示例:

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

// ... (Pipe creation code remains the same as in the original example) ...

// 完成例程
VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered,
                                    LPOVERLAPPED lpOverlapped) {
  if (dwErrorCode == ERROR_SUCCESS) {
    char* buffer = (char*)lpOverlapped->hEvent; // Retrieve buffer from overlapped structure
    buffer[dwNumberOfBytesTransfered] = 0;  // Null-terminate the received data
    printf("[*] ReadFile output: %s\n", buffer);

    // Reset the overlapped structure and initiate another read if needed
    memset(lpOverlapped, 0, sizeof(OVERLAPPED));
  } else {
    fprintf(stderr, "[!] ReadFileEx failed with error code: %d\n", dwErrorCode);
  }
}

int main() {
  // ... (Pipe creation code remains the same as in the original example) ...

  OVERLAPPED overlapped = {0};

  // Use a manual-reset event object for signaling
  overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 
  if(overlapped.hEvent == NULL) {
      fprintf(stderr, "[!] CreateEvent failed: %d\n", GetLastError());
      return 1;
  }

  char buffer[1024] = "echo HelloWorld!\n";
  DWORD writtenBytes = 0;
  BOOL success = TRUE;

  // Input a command to the write end of the stdin pipe
  if (!WriteFile(StdinWrite, buffer, strlen(buffer), &writtenBytes, NULL)) {
    fprintf(stderr, "[!] WriteFile failed with error code: %d\n", GetLastError());
      return 1;
  }
  strcpy(buffer, "dir\n");
   if (!WriteFile(StdinWrite, buffer, strlen(buffer), &writtenBytes, NULL)) {
    fprintf(stderr, "[!] WriteFile failed with error code: %d\n", GetLastError());
      return 1;
  }

  // Create a buffer for the ReadFile operation to be passed to completion routine.
  char readBuffer[1024];

  while (TRUE) {
      overlapped.hEvent = readBuffer;
      success = ReadFileEx(StdoutRead, readBuffer, sizeof(readBuffer), &overlapped, FileIOCompletionRoutine);

      if (!success) {
          if (GetLastError() != ERROR_IO_PENDING) {
             fprintf(stderr, "[!] ReadFileEx failed with error: %d\n", GetLastError());
             break;
          }
      }

     SleepEx(INFINITE, TRUE); // Wait for completion or timeout

  }

    CloseHandle(StdoutWrite);
    CloseHandle(StdinRead);
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);
    CloseHandle(overlapped.hEvent);
  return 0;
}

操作步骤:

  1. 包含必要的头文件windows.hstdio.hstring.h
  2. 创建 OVERLAPPED 结构体, 用于异步I/O操作。
  3. 修改 ReadFile 操作为 ReadFileEx 异步读。
  4. 创建一个完成例程函数FileIOCompletionRoutine,用于处理异步读取完成后的数据。
  5. 使用 SleepEx 函数进入可 alertable 状态等待异步操作的完成信号。
  6. 编译并运行程序。

原理:

异步 I/O 通过 ReadFileEx 函数实现,它立即返回,并在后台执行读取操作。当读取操作完成时,系统会调用 FileIOCompletionRoutine 函数处理数据。主线程通过 SleepEx 函数进入可 alertable 状态,等待异步操作完成。使用 SleepEx 的好处在于,它可以使线程在等待 I/O 完成的同时处理其他任务,如 APC 队列中的回调函数。在这种情况下,APC 队列中的回调函数就是异步 I/O 的完成例程。当异步 I/O 操作完成时,系统会将完成例程添加到 APC 队列,并唤醒线程来执行它。这种机制避免了线程阻塞,提高了程序的并发性和响应能力。

额外安全建议:

  • 限制完成例程的执行时间,防止其阻塞其他异步操作。
  • 检查 ReadFileEx 的返回值和错误码,处理可能的错误情况。

2. 使用匿名管道和 WaitForSingleObject

这种方法结合匿名管道和进程等待,间接判断cmd.exe是否执行完成。首先,创建一个匿名管道并将其设置为cmd.exe的输入,然后创建一个事件对象并在cmd.exe的命令行中添加一个命令,在执行完成后触发该事件对象。程序在WaitForSingleObject等待事件对象被触发,以此来判断cmd.exe何时执行完毕,接着再读取stdout管道。

代码示例:
这里因为代码示例过长,故拆分成两部分,分开演示。首先,创建子进程的代码部分( create_child_process ):

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

// 定义事件名称
#define EVENT_NAME "CmdProcessDoneEvent"

// 用于创建子进程和设置管道的函数
BOOL create_child_process(HANDLE* pStdinWrite, HANDLE* pStdoutRead, PROCESS_INFORMATION* pProcessInfo) {
  SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};
  HANDLE hStdinRead, hStdinWrite, hStdoutRead, hStdoutWrite;

  // 创建输入管道
  if (!CreatePipe(&hStdinRead, &hStdinWrite, &saAttr, 0)) {
    fprintf(stderr, "[!] Stdin pipe creation failed (%d).\n", GetLastError());
    return FALSE;
  }

  // 设置输入管道为不可继承
  if (!SetHandleInformation(hStdinWrite, HANDLE_FLAG_INHERIT, 0)) {
    fprintf(stderr, "[!] Stdin SetHandleInformation failed (%d).\n", GetLastError());
    return FALSE;
  }

  // 创建输出管道
  if (!CreatePipe(&hStdoutRead, &hStdoutWrite, &saAttr, 0)) {
    fprintf(stderr, "[!] Stdout pipe creation failed (%d).\n", GetLastError());
    CloseHandle(hStdinRead);
    CloseHandle(hStdinWrite);
    return FALSE;
  }

  // 设置输出管道为不可继承
  if (!SetHandleInformation(hStdoutRead, HANDLE_FLAG_INHERIT, 0)) {
    fprintf(stderr, "[!] Stdout SetHandleInformation failed (%d).\n", GetLastError());
     CloseHandle(hStdinRead);
     CloseHandle(hStdinWrite);
    CloseHandle(hStdoutRead);
    CloseHandle(hStdoutWrite);
    return FALSE;
  }

    // 创建事件
    HANDLE hEvent = CreateEvent(&saAttr, TRUE, FALSE, EVENT_NAME);
    if (hEvent == NULL)
    {
        fprintf(stderr, "[!] Create event failed (%d).\n", GetLastError());
            CloseHandle(hStdinRead);
            CloseHandle(hStdinWrite);
            CloseHandle(hStdoutRead);
            CloseHandle(hStdoutWrite);

        return FALSE;
    }