在Windows 8上FreeConsole的行为表现

7
在Windows 8上,我们遇到了使用FreeConsole的问题。它似乎关闭了stdio句柄,但没有关闭文件流。这可能是一个Windows 8的问题,或者只是我不理解Windows控制台/GUI应用程序子系统的(完全荒谬的)工作方式。下面是一个最简示例,使用VS2005、VS2013和VS2017编译器进行测试,使用静态链接CRT。
#include <windows.h>
#include <io.h>
#include <stdio.h>

static void testHandle(FILE* file) {
  HANDLE h = (HANDLE)_get_osfhandle(fileno(file));
  DWORD flags;
  if (!GetHandleInformation(h, &flags)) {
    MessageBoxA(0, "Bogus handle!!", "TITLE", MB_OK);
  }
}

int main(int argc, char** argv)
{
  freopen("NUL", "wb", stdout); // Demonstrate the issue with NUL
  // Leave stderr as it is, to demonstrate the issue with handles
  // to the console device.

  FreeConsole();

  testHandle(stdout);
  testHandle(stderr);
}

注意:如果您在Windows 7上运行此代码,则没有MessageBox。在Windows 8上运行它,会出现一个消息框。 - Nicholas Wilson
我向微软报告了此问题,因为它具有安全隐患。它几乎导致我们的应用程序出现极其危险的错误。我仍然想知道是否有任何解释或者是 Stack Overflow 对此有何评论。 - Nicholas Wilson
你的报告链接现在已经失效了 - 你收到了微软的任何回复吗? - Dan Blanchard
好问题,我现在在他们的网站上找不到它。链接不仅已经失效,而且在我的Microsoft Connect仪表板中的“您提交的反馈”下已不再列出。他们刚刚删除了我的错误报告吗!?我认为他们的反馈是“无效的,您依赖于未定义/未记录的行为”。我的回应是,“你在开玩笑吧,FreeConsole和stdout之间的交互怎么可能没有文档记录”。 - Nicholas Wilson
2个回答

5
由于之前的Windows 8标准(未重定向)控制台处理程序(由GetStdHandle返回)实际上是伪句柄,其值与其他内核对象句柄不相交,因此在FreeConsole关闭后写入该伪句柄始终会失败。 在Win8中,MS进行了一些更改,因此GetStdHandle返回正常的内核对象句柄,该句柄引用控制台子系统驱动程序对象(实际上,该驱动程序仅出现在Win8中)。 因此,FreeConsole关闭该句柄。 最有趣的事情是CRT在启动时对GetStdHandle进行操作并在某个地方保存返回值,并在使用调用C函数访问std :: in / out / err的位置处使用该值。 由于FreeConsole已关闭该句柄,并且它不再是特殊的伪句柄值-任何其他打开的内核对象句柄都可以重复使用相同的句柄值,如果它不是文件,管道或套接字,则您将很幸运,因为在这种情况下,所有调试输出都将转到那里:)

1
事实上,真正糟糕的是FreeConsole关闭了对NUL的句柄,这曾经是可以接受的。也就是说,以前FreeConsole会使stdio处于无法恢复的状态,除非你发送给它NUL,但现在即使这样也不起作用了。我希望从安全的角度来看,他们能够修复它! - Nicholas Wilson

2
在不同的Windows版本上分解FreeConsole代码后,我找到了问题的原因。
FreeConsole是一个非常粗暴的函数!它确实为您关闭了大量句柄,即使它并没有"拥有"这些句柄(例如由stdio函数拥有的HANDLE)。
而且,在Windows 7和8中的行为不同,并且在10中再次更改。
在想出修复方法时,存在以下困境:
一旦stdio具有控制台设备的HANDLE,就没有记录的方法可以让它放弃该句柄,而不需要调用CloseHandle。您可以调用close(1)或freopen(stdout)或任何您喜欢的内容,但是如果有一个打开的文件描述符引用控制台,则会对其调用CloseHandle,如果您想在FreeConsole之后将stdout切换到新的NUL句柄。
另一方面,自Windows 10以来,也无法避免FreeConsole调用CloseHandle。
Visual Studio的调试器和应用程序验证器标记应用程序调用无效句柄上的CloseHandle。他们是正确的,这真的不好。
因此,如果您尝试在调用FreeConsole之前"修复"stdio,则FreeConsole将执行无效的CloseHandle(使用其缓存的句柄,绝对没有办法告诉它该句柄已经消失了- FreeConsole不再检查GetStdHandle(STD_OUTPUT_HANDLE))。如果您首先调用FreeConsole,则无法在不导致它们执行无效的CloseHandle调用的情况下修复stdio对象。
通过排除,我得出结论,如果公共函数行不通,那么唯一的解决方案是使用未记录的函数。
// The undocumented bit!
extern "C" int __cdecl _free_osfhnd(int const fh);
static HANDLE closeFdButNotHandle(int fd) {
  HANDLE h = (HANDLE)_get_osfhandle(fd);
  _free_osfhnd(fd); // Prevent CloseHandle happening in close()
  close(fd);
  return h;
}

static bool valid(HANDLE h) {
  SetLastError(0);
  return GetFileType(h) != FILE_TYPE_UNKNOWN || GetLastError() == 0;
}

static void openNull(int fd, DWORD flags) {
  int newFd;
  // Yet another Microsoft bug! (I've reported four in this code...)
  // They have confirmed a bug in dup2 in Visual Studio 2013, fixed
  // in Visual Studio 2017.  If dup2 is called with fd == newFd, the
  // CRT lock is corrupted, hence the check here before calling dup2.
  if (!_tsopen_s(&newFd, _T("NUL"), flags, _SH_DENYNO, 0) &&
      fd != newFd)
    dup2(newFd, fd);
  if (fd != newFd) close(newFd);
}

void doFreeConsole() {
  // stderr, stdin are similar - left to the reader.  You probably
  // also want to add code (as we have) to detect when the handle
  // is FILE_TYPE_DISK/FILE_TYPE_PIPE and leave the stdio FILE
  // alone if it's actually pointing to disk/pipe.
  HANDLE stdoutHandle = closeFdButNotHandle(fileno(stdout)); 

  FreeConsole(); // error checking left to the reader

  // If FreeConsole *didn't* close the handle then do so now.
  // Has a race condition, but all of this code does so hey.
  if (valid(stdoutHandle)) CloseHandle(stdoutHandle);

  openNull(stdoutRestore, _O_BINARY | _O_RDONLY);
}

我想知道,在你调用 freopen 之前(正如你指出的那样会调用 ::CloseHandle(),导致处理句柄悬空),是否只调用 ::SetStdHandle() 就足以防止 ::FreeConsole() 尝试关闭这些悬空(可能被重复使用)的 HANDLE? - Ben Voigt
哦,我看到你在最后一条要点中提到了这个。 - Ben Voigt
总的来说,这似乎是CRT stdio实现中的一个错误,它不应该借用标准句柄(可能会被操作系统API随时关闭),而应该调用DuplicateHandle获取自己的句柄并存储在CRT FILE对象中,对其生命周期CRT具有完全控制权。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接