【问题标题】:FreeConsole behaviour on Windows 8Windows 8 上的 FreeConsole 行为
【发布时间】:2012-10-01 15:29:07
【问题描述】:

在 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 上运行它,有一个消息框。
  • reported this to Microsoft 考虑到它的安全隐患。它几乎在我们的应用程序中造成了一个极其危险的错误。我仍然想知道是否有任何解释或评论。
  • 您向 Microsoft 报告此问题的链接现在已损坏 - 您收到 Microsoft 的任何回复了吗?
  • 好问题,我现在在他们网站上的任何地方都找不到。不仅链接被破坏,而且它不再列在我的 Microsoft Connect 仪表板中的“您提交的反馈”下。他们只是删除了我的错误报告吗!?我认为他们的反馈是“无效的,您依赖于未定义/未记录的行为”。我的回答是,“你在开玩笑,怎么可能没有记录 FreeConsole 和 stdout 之间的交互”。

标签: visual-studio-2005 windows-8 msvcrt


【解决方案1】:

问题是由于以前的 Windows 8 标准(未重定向)控制台句柄(由 GetStdHandle 返回的)实际上是伪句柄,其值不与其他内核对象句柄相交,因此在该伪句柄被“关闭”后写入该伪句柄FreeConsole 总是失败。 在 Win8 MS 里面改变了一些东西,所以 GetStdHandle 返回正常的内核对象句柄,它引用控制台子系统驱动程序对象(实际上该驱动程序也只出现在 Win8 中)。所以 FreeConsole 关闭了那个句柄。最有趣的是 CRT 在启动时执行 GetStdHandle 并将返回值保存在内部某处,并在任何地方使用称为访问 std::in/out/err 的 C 函数。由于 FreeConsole 关闭了该句柄,它不再是一个特殊的伪句柄值 - 相同的句柄值可以被任何其他打开的内核对象句柄重用,如果在这种情况下它不是文件、管道或套接字,你会很幸运你的调试输出会去那里:)

【讨论】:

  • 确实,真正可怕的是 FreeConsole 关闭了 NUL 的句柄,这在以前是可以的。也就是说,过去的情况是,除非您发送 NUL,否则 FreeConsole 会使 stdio 处于不可恢复的状态,但现在即使这样也行不通。我希望他们能从安全 POV 中修复它!
【解决方案2】:

在不同Windows版本上拆解FreeConsole的代码后,我找出了问题的原因。

FreeConsole 是一个非常简单的功能!我确实为您关闭了大量句柄,即使它不“拥有”这些句柄(例如,stdio 函数拥有的句柄)。

而且,Windows 7 和 8 中的行为有所不同,并在 10 中再次更改。

这是解决问题时的两难选择:

  • 一旦 stdio 拥有控制台设备的句柄,没有记录的方法可以让它放弃该句柄,而无需调用 CloseHandle。您可以调用close(1)freopen(stdout) 或任何您喜欢的方法,但如果有一个指向控制台的打开文件描述符,如果您想将stdout 切换到新的NUL 句柄,则会在其上调用CloseHandle 在 FreeConsole 之后。
  • 另一方面,由于 Windows 10 也无法避免 FreeConsole 也调用 CloseHandle。
  • Visual Studio 的调试器和应用程序验证程序将应用标记为在无效 HANDLE 上调用 CloseHandle。而且,他们是对的,确实不好。
  • 因此,如果您在调用 FreeConsole 之前尝试“修复”stdio,那么 FreeConsole 将执行无效的 CloseHandle(使用其缓存的句柄,并且无法告诉它句柄已消失 - FreeConsole 不再检查 @987654323 @)。而且,如果您先调用 FreeConsole,则无法修复 stdio 对象而不导致它们对 CloseHandle 进行无效调用。

通过消除,我得出的结论是,如果公共函数不起作用,唯一的解决方案是使用未记录的函数。

// 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?
  • 哦,我看到你在最后一个要点中提到了这一点。
  • 总而言之,这似乎是 CRT stdio 实现中的一个错误,它不应该借用标准句柄(可以随时被 OS API 关闭),但应该调用 DuplicateHandle 来获得它自己的句柄存储在 CRT FILE 对象中,并且 CRT 可以完全控制其生命周期。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多