Поведение FreeConsole в Windows 8

В Windows 8 у нас есть проблема с FreeConsole. Кажется, что закрывает дескрипторы stdio, не закрывая файловые потоки.

Это может быть проблема Windows 8, а может быть, я просто не понимаю (совершенно абсурдного) способа, которым работает подсистема приложения консоли / графического интерфейса Windows.

Что происходит?

Минимальный пример ниже. Протестировано с компиляторами: 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);
}

person Nicholas Wilson    schedule 01.10.2012    source источник
comment
Примечание. Если вы запустите этот код в Windows 7, MessageBox не будет. Запустите его в Windows 8, есть окно сообщения.   -  person Nicholas Wilson    schedule 05.10.2012
comment
Я сообщил об этом в Microsoft с учетом последствий для безопасности. Это чуть не вызвало чрезвычайно опасную ошибку в нашем приложении. Я все еще хотел бы знать, есть ли какие-либо объяснения или комментарии, которые есть у SO.   -  person Nicholas Wilson    schedule 19.12.2012
comment
ваша ссылка, сообщающая об этом в Microsoft, сейчас не работает - получили ли вы какой-либо ответ от Microsoft?   -  person Dan Blanchard    schedule 14.12.2017
comment
Хороший вопрос, сейчас я нигде не могу найти его на их сайте. Мало того, что ссылка не работает, она больше не указана на моей панели инструментов Microsoft Connect под отзывами, которые вы отправили. Они только что удалили мой отчет об ошибке !? Я думаю, что их отзыв был недействительным, вы полагаетесь на неопределенное / недокументированное поведение. На что я ответил, вы шутите, как может случиться так, что взаимодействие между FreeConsole и stdout не задокументировано.   -  person Nicholas Wilson    schedule 15.12.2017


Ответы (2)


проблема, вызванная тем, что предыдущие стандартные (не перенаправленные) консольные дескрипторы Windows 8 (которые возвращались GetStdHandle) были фактически псевдодескрипторами, значения которых не пересекаются с другими дескрипторами объектов ядра, поэтому запись в этот псевдодескриптор после того, как он был `` закрыт '' FreeConsole, всегда терпит неудачу . В Win8 MS что-то изменила внутри, поэтому GetStdHandle возвращает нормальный дескриптор объекта ядра, который относится к объекту драйвера консольной подсистемы (на самом деле этот драйвер также появился только в Win8). Итак, FreeConsole закрывает этот дескриптор. Самое забавное, что CRT выполняет GetStdHandle при запуске и сохраняет возвращаемое значение где-то внутри и использует везде, где используются вызываемые функции C, которые обращаются к std :: in / out / err. Поскольку FreeConsole закрыла этот дескриптор, и это больше не является специальным значением псевдодескриптора - одно и то же значение дескриптора может быть повторно использовано любым другим открытым дескриптором объекта ядра, и вам повезет, если это не будет файл, канал или сокет, потому что в этом случае все туда пойдет ваш отладочный выход :)

person test4sb    schedule 30.05.2013
comment
В самом деле, действительно ужасно то, что FreeConsole закрывает дескрипторы до NUL, что раньше было нормально. То есть раньше FreeConsole оставляла stdio в невосстановимом состоянии, если вы не отправили его NUL, но теперь даже это не работает. Я надеюсь, что с точки зрения безопасности они это исправят! - person Nicholas Wilson; 31.05.2013

После дизассемблирования кода FreeConsole в разных версиях Windows я выяснил причину проблемы.

FreeConsole - на удивление непонятная функция! Я действительно закрываю для вас множество дескрипторов, даже если он не «владеет» этими дескрипторами (например, HANDLE, принадлежащие функциям stdio).

И поведение отличается в Windows 7 и 8 и снова изменилось в 10.

Придумывая решение, возникает дилемма:

  • Как только stdio имеет HANDLE для консольного устройства, нет документированного способа заставить его отказаться от этого дескриптора без вызова CloseHandle. Вы можете вызвать close(1) или freopen(stdout) или что угодно, но если есть дескриптор открытого файла, который ссылается на консоль, на нем будет вызываться CloseHandle, если вы хотите переключить stdout на новый дескриптор NUL после FreeConsole.
  • С другой стороны, начиная с Windows 10, также невозможно избежать вызова CloseHandle FreeConsole.
  • Отладчик Visual Studio и средство проверки приложений помечают приложение для вызова CloseHandle для недопустимой HANDLE. И они правы, это действительно нехорошо.
  • Итак, если вы попытаетесь «исправить» stdio перед вызовом FreeConsole, FreeConsole выполнит недопустимый CloseHandle (используя свой кэшированный дескриптор, и нет никакого способа сообщить ему, что дескриптор пропал - FreeConsole больше не проверяет GetStdHandle(STD_OUTPUT_HANDLE)). И, если вы сначала вызовете 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);
}
person Nicholas Wilson    schedule 15.12.2017
comment
Интересно, если вы сначала сделаете freopen (который, как вы заметили, вызывает ::CloseHandle(), оставляя болтающиеся дескрипторы), недостаточно ли вызвать ::SetStdHandle(), чтобы остановить ::FreeConsole() от попытки закрыть болтающуюся (и, возможно, повторно используемую) РУЧКУ? - person Ben Voigt; 26.01.2021
comment
О, я вижу, вы затронули это в своем последнем пункте. - person Ben Voigt; 26.01.2021
comment
В целом это кажется ошибкой в ​​реализации CRT stdio, которая не должна заимствовать стандартный дескриптор (который может быть закрыт в любое время API ОС), но должна вызывать DuplicateHandle, чтобы получить свои собственные дескрипторы для хранения в CRT FILE объектах, и для ЭЛТ полностью контролирует срок службы. - person Ben Voigt; 26.01.2021