// // Windows versions tested // // Vista Enterprise SP2 32-bit // - ver reports [Version 6.0.6002] // - kernel32.dll product/file versions are 6.0.6002.19381 // // Windows 7 Ultimate SP1 32-bit // - ver reports [Version 6.1.7601] // - conhost.exe product/file versions are 6.1.7601.18847 // - kernel32.dll product/file versions are 6.1.7601.18847 // // Windows Server 2008 R2 Datacenter SP1 64-bit // - ver reports [Version 6.1.7601] // - conhost.exe product/file versions are 6.1.7601.23153 // - kernel32.dll product/file versions are 6.1.7601.23153 // // Windows 8 Enterprise 32-bit // - ver reports [Version 6.2.9200] // - conhost.exe product/file versions are 6.2.9200.16578 // - kernel32.dll product/file versions are 6.2.9200.16859 // // // Specific version details on working Server 2008 R2: // // dwMajorVersion = 6 // dwMinorVersion = 1 // dwBuildNumber = 7601 // dwPlatformId = 2 // szCSDVersion = Service Pack 1 // wServicePackMajor = 1 // wServicePackMinor = 0 // wSuiteMask = 0x190 // wProductType = 0x3 // // Specific version details on broken Win7: // // dwMajorVersion = 6 // dwMinorVersion = 1 // dwBuildNumber = 7601 // dwPlatformId = 2 // szCSDVersion = Service Pack 1 // wServicePackMajor = 1 // wServicePackMinor = 0 // wSuiteMask = 0x100 // wProductType = 0x1 // #include #include #include #include "TestUtil.cc" const char *g_prefix = ""; static void dumpHandles() { trace("%sSTDIN=0x%I64x STDOUT=0x%I64x STDERR=0x%I64x", g_prefix, (long long)GetStdHandle(STD_INPUT_HANDLE), (long long)GetStdHandle(STD_OUTPUT_HANDLE), (long long)GetStdHandle(STD_ERROR_HANDLE)); } static const char *successOrFail(BOOL ret) { return ret ? "ok" : "FAILED"; } static void startChildInSameConsole(const wchar_t *args, BOOL bInheritHandles=FALSE) { wchar_t program[1024]; wchar_t cmdline[1024]; GetModuleFileNameW(NULL, program, 1024); swprintf(cmdline, L"\"%ls\" %ls", program, args); STARTUPINFOW sui; PROCESS_INFORMATION pi; memset(&sui, 0, sizeof(sui)); memset(&pi, 0, sizeof(pi)); sui.cb = sizeof(sui); CreateProcessW(program, cmdline, NULL, NULL, /*bInheritHandles=*/bInheritHandles, /*dwCreationFlags=*/0, NULL, NULL, &sui, &pi); } static void closeHandle(HANDLE h) { trace("%sClosing handle 0x%I64x...", g_prefix, (long long)h); trace("%sClosing handle 0x%I64x... %s", g_prefix, (long long)h, successOrFail(CloseHandle(h))); } static HANDLE createBuffer() { // If sa isn't provided, the handle defaults to not-inheritable. SECURITY_ATTRIBUTES sa = {0}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; trace("%sCreating a new buffer...", g_prefix); HANDLE conout = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, CONSOLE_TEXTMODE_BUFFER, NULL); trace("%sCreating a new buffer... 0x%I64x", g_prefix, (long long)conout); return conout; } static HANDLE openConout() { // If sa isn't provided, the handle defaults to not-inheritable. SECURITY_ATTRIBUTES sa = {0}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; trace("%sOpening CONOUT...", g_prefix); HANDLE conout = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL); trace("%sOpening CONOUT... 0x%I64x", g_prefix, (long long)conout); return conout; } static void setConsoleActiveScreenBuffer(HANDLE conout) { trace("%sSetConsoleActiveScreenBuffer(0x%I64x) called...", g_prefix, (long long)conout); trace("%sSetConsoleActiveScreenBuffer(0x%I64x) called... %s", g_prefix, (long long)conout, successOrFail(SetConsoleActiveScreenBuffer(conout))); } static void writeTest(HANDLE conout, const char *msg) { char writeData[256]; sprintf(writeData, "%s%s\n", g_prefix, msg); trace("%sWriting to 0x%I64x: '%s'...", g_prefix, (long long)conout, msg); DWORD actual = 0; BOOL ret = WriteConsoleA(conout, writeData, strlen(writeData), &actual, NULL); trace("%sWriting to 0x%I64x: '%s'... %s", g_prefix, (long long)conout, msg, successOrFail(ret && actual == strlen(writeData))); } static void writeTest(const char *msg) { writeTest(GetStdHandle(STD_OUTPUT_HANDLE), msg); } /////////////////////////////////////////////////////////////////////////////// // TEST 1 -- create new buffer, activate it, and close the handle. The console // automatically switches the screen buffer back to the original. // // This test passes everywhere. // static void test1(int argc, char *argv[]) { if (!strcmp(argv[1], "1")) { startChildProcess(L"1:child"); return; } HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); writeTest(origBuffer, "<-- origBuffer -->"); HANDLE newBuffer = createBuffer(); writeTest(newBuffer, "<-- newBuffer -->"); setConsoleActiveScreenBuffer(newBuffer); Sleep(2000); writeTest(origBuffer, "TEST PASSED!"); // Closing the handle w/o switching the active screen buffer automatically // switches the console back to the original buffer. closeHandle(newBuffer); while (true) { Sleep(1000); } } /////////////////////////////////////////////////////////////////////////////// // TEST 2 -- Test program that creates and activates newBuffer, starts a child // process, then closes its newBuffer handle. newBuffer remains activated, // because the child keeps it active. (Also see TEST D.) // static void test2(int argc, char *argv[]) { if (!strcmp(argv[1], "2")) { startChildProcess(L"2:parent"); return; } if (!strcmp(argv[1], "2:parent")) { g_prefix = "parent: "; dumpHandles(); HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); writeTest(origBuffer, "<-- origBuffer -->"); HANDLE newBuffer = createBuffer(); writeTest(newBuffer, "<-- newBuffer -->"); setConsoleActiveScreenBuffer(newBuffer); Sleep(1000); writeTest(newBuffer, "bInheritHandles=FALSE:"); startChildInSameConsole(L"2:child", FALSE); Sleep(1000); writeTest(newBuffer, "bInheritHandles=TRUE:"); startChildInSameConsole(L"2:child", TRUE); Sleep(1000); trace("parent:----"); // Close the new buffer. The active screen buffer doesn't automatically // switch back to origBuffer, because the child process has a handle open // to the original buffer. closeHandle(newBuffer); Sleep(600 * 1000); return; } if (!strcmp(argv[1], "2:child")) { g_prefix = "child: "; dumpHandles(); // The child's output isn't visible, because it's still writing to // origBuffer. trace("child:----"); writeTest("writing to STDOUT"); // Handle inheritability is curious. The console handles this program // creates are inheritable, but CreateProcess is called with both // bInheritHandles=TRUE and bInheritHandles=FALSE. // // Vista and Windows 7: bInheritHandles has no effect. The child and // parent processes have the same STDIN/STDOUT/STDERR handles: // 0x3, 0x7, and 0xB. The parent has a 0xF handle for newBuffer. // The child can only write to 0x7, 0xB, and 0xF. Only the writes to // 0xF are visible (i.e. they touch newBuffer). // // Windows 8 or Windows 10 (legacy or non-legacy): the lowest 2 bits of // the HANDLE to WriteConsole seem to be ignored. The new process' // console handles always refer to the buffer that was active when they // started, but the values of the handles depend upon bInheritHandles. // With bInheritHandles=TRUE, the child has the same // STDIN/STDOUT/STDERR/newBuffer handles as the parent, and the three // output handles all work, though their output is all visible. With // bInheritHandles=FALSE, the child has different STDIN/STDOUT/STDERR // handles, and only the new STDOUT/STDERR handles work. // for (unsigned int i = 0x1; i <= 0xB0; ++i) { char msg[256]; sprintf(msg, "Write to handle 0x%x", i); HANDLE h = reinterpret_cast(i); writeTest(h, msg); } Sleep(600 * 1000); return; } } /////////////////////////////////////////////////////////////////////////////// // TEST A -- demonstrate an apparent Windows bug with screen buffers // // Steps: // - The parent starts a child process. // - The child process creates and activates newBuffer // - The parent opens CONOUT$ and writes to it. // - The parent closes CONOUT$. // - At this point, broken Windows reactivates origBuffer. // - The child writes to newBuffer again. // - The child activates origBuffer again, then closes newBuffer. // // Test passes if the message "TEST PASSED!" is visible. // Test commonly fails if conhost.exe crashes. // // Results: // - Windows 7 Ultimate SP1 32-bit: conhost.exe crashes // - Windows Server 2008 R2 Datacenter SP1 64-bit: PASS // - Windows 8 Enterprise 32-bit: PASS // - Windows 10 64-bit (legacy and non-legacy): PASS // static void testA_parentWork() { // Open an extra CONOUT$ handle so that the HANDLE values in parent and // child don't collide. I think it's OK if they collide, but since we're // trying to track down a Windows bug, it's best to avoid unnecessary // complication. HANDLE dummy = openConout(); Sleep(3000); // Step 2: Open CONOUT$ in the parent. This opens the active buffer, which // was just created in the child. It's handle 0x13. Write to it. HANDLE newBuffer = openConout(); writeTest(newBuffer, "step2: writing to newBuffer"); Sleep(3000); // Step 3: Close handle 0x13. With Windows 7, the console switches back to // origBuffer, and (unless I'm missing something) it shouldn't. closeHandle(newBuffer); } static void testA_childWork() { HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); // // Step 1: Create the new screen buffer in the child process and make it // active. (Typically, it's handle 0x0F.) // HANDLE newBuffer = createBuffer(); setConsoleActiveScreenBuffer(newBuffer); writeTest(newBuffer, "<-- newBuffer -->"); Sleep(9000); trace("child:----"); // Step 4: write to the newBuffer again. writeTest(newBuffer, "TEST PASSED!"); // // Step 5: Switch back to the original screen buffer and close the new // buffer. The switch call succeeds, but the CloseHandle call freezes for // several seconds, because conhost.exe crashes. // Sleep(3000); setConsoleActiveScreenBuffer(origBuffer); writeTest(origBuffer, "writing to origBuffer"); closeHandle(newBuffer); // The console HWND is NULL. trace("child: console HWND=0x%I64x", (long long)GetConsoleWindow()); // At this point, the console window has closed, but the parent/child // processes are still running. Calling AllocConsole would fail, but // calling FreeConsole followed by AllocConsole would both succeed, and a // new console would appear. } static void testA(int argc, char *argv[]) { if (!strcmp(argv[1], "A")) { startChildProcess(L"A:parent"); return; } if (!strcmp(argv[1], "A:parent")) { g_prefix = "parent: "; trace("parent:----"); dumpHandles(); writeTest("<-- origBuffer -->"); startChildInSameConsole(L"A:child"); testA_parentWork(); Sleep(120000); return; } if (!strcmp(argv[1], "A:child")) { g_prefix = "child: "; dumpHandles(); testA_childWork(); Sleep(120000); return; } } /////////////////////////////////////////////////////////////////////////////// // TEST B -- invert TEST A -- also crashes conhost on Windows 7 // // Test passes if the message "TEST PASSED!" is visible. // Test commonly fails if conhost.exe crashes. // // Results: // - Windows 7 Ultimate SP1 32-bit: conhost.exe crashes // - Windows Server 2008 R2 Datacenter SP1 64-bit: PASS // - Windows 8 Enterprise 32-bit: PASS // - Windows 10 64-bit (legacy and non-legacy): PASS // static void testB(int argc, char *argv[]) { if (!strcmp(argv[1], "B")) { startChildProcess(L"B:parent"); return; } if (!strcmp(argv[1], "B:parent")) { g_prefix = "parent: "; startChildInSameConsole(L"B:child"); writeTest("<-- origBuffer -->"); HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); // // Step 1: Create the new buffer and make it active. // trace("%s----", g_prefix); HANDLE newBuffer = createBuffer(); setConsoleActiveScreenBuffer(newBuffer); writeTest(newBuffer, "<-- newBuffer -->"); // // Step 4: Attempt to write again to the new buffer. // Sleep(9000); trace("%s----", g_prefix); writeTest(newBuffer, "TEST PASSED!"); // // Step 5: Switch back to the original buffer. // Sleep(3000); trace("%s----", g_prefix); setConsoleActiveScreenBuffer(origBuffer); closeHandle(newBuffer); writeTest(origBuffer, "writing to the initial buffer"); Sleep(60000); return; } if (!strcmp(argv[1], "B:child")) { g_prefix = "child: "; Sleep(3000); trace("%s----", g_prefix); // // Step 2: Open the newly active buffer and write to it. // HANDLE newBuffer = openConout(); writeTest(newBuffer, "writing to newBuffer"); // // Step 3: Close the newly active buffer. // Sleep(3000); closeHandle(newBuffer); Sleep(60000); return; } } /////////////////////////////////////////////////////////////////////////////// // TEST C -- Interleaving open/close of console handles also seems to break on // Windows 7. // // Test: // - child creates and activates newBuf1 // - parent opens newBuf1 // - child creates and activates newBuf2 // - parent opens newBuf2, then closes newBuf1 // - child switches back to newBuf1 // * At this point, the console starts malfunctioning. // - parent and child close newBuf2 // - child closes newBuf1 // // Test passes if the message "TEST PASSED!" is visible. // Test commonly fails if conhost.exe crashes. // // Results: // - Windows 7 Ultimate SP1 32-bit: conhost.exe crashes // - Windows Server 2008 R2 Datacenter SP1 64-bit: PASS // - Windows 8 Enterprise 32-bit: PASS // - Windows 10 64-bit (legacy and non-legacy): PASS // static void testC(int argc, char *argv[]) { if (!strcmp(argv[1], "C")) { startChildProcess(L"C:parent"); return; } if (!strcmp(argv[1], "C:parent")) { startChildInSameConsole(L"C:child"); writeTest("<-- origBuffer -->"); g_prefix = "parent: "; // At time=4, open newBuffer1. Sleep(4000); trace("%s---- t=4", g_prefix); const HANDLE newBuffer1 = openConout(); // At time=8, open newBuffer2, and close newBuffer1. Sleep(4000); trace("%s---- t=8", g_prefix); const HANDLE newBuffer2 = openConout(); closeHandle(newBuffer1); // At time=25, cleanup of newBuffer2. Sleep(17000); trace("%s---- t=25", g_prefix); closeHandle(newBuffer2); Sleep(240000); return; } if (!strcmp(argv[1], "C:child")) { g_prefix = "child: "; // At time=2, create newBuffer1 and activate it. Sleep(2000); trace("%s---- t=2", g_prefix); const HANDLE newBuffer1 = createBuffer(); setConsoleActiveScreenBuffer(newBuffer1); writeTest(newBuffer1, "<-- newBuffer1 -->"); // At time=6, create newBuffer2 and activate it. Sleep(4000); trace("%s---- t=6", g_prefix); const HANDLE newBuffer2 = createBuffer(); setConsoleActiveScreenBuffer(newBuffer2); writeTest(newBuffer2, "<-- newBuffer2 -->"); // At time=10, attempt to switch back to newBuffer1. The parent process // has opened and closed its handle to newBuffer1, so does it still exist? Sleep(4000); trace("%s---- t=10", g_prefix); setConsoleActiveScreenBuffer(newBuffer1); writeTest(newBuffer1, "write to newBuffer1: TEST PASSED!"); // At time=25, cleanup of newBuffer2. Sleep(15000); trace("%s---- t=25", g_prefix); closeHandle(newBuffer2); // At time=35, cleanup of newBuffer1. The console should switch to the // initial buffer again. Sleep(10000); trace("%s---- t=35", g_prefix); closeHandle(newBuffer1); Sleep(240000); return; } } /////////////////////////////////////////////////////////////////////////////// // TEST D -- parent creates a new buffer, child launches, writes, // closes it output handle, then parent writes again. (Also see TEST 2.) // // On success, this will appear: // // parent: <-- newBuffer --> // child: writing to newBuffer // parent: TEST PASSED! // // If this appears, it indicates that the child's closing its output handle did // not destroy newBuffer. // // Results: // - Windows 7 Ultimate SP1 32-bit: PASS // - Windows 8 Enterprise 32-bit: PASS // - Windows 10 64-bit (legacy and non-legacy): PASS // static void testD(int argc, char *argv[]) { if (!strcmp(argv[1], "D")) { startChildProcess(L"D:parent"); return; } if (!strcmp(argv[1], "D:parent")) { g_prefix = "parent: "; HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); writeTest(origBuffer, "<-- origBuffer -->"); HANDLE newBuffer = createBuffer(); writeTest(newBuffer, "<-- newBuffer -->"); setConsoleActiveScreenBuffer(newBuffer); // At t=2, start a child process, explicitly forcing it to use // newBuffer for its standard handles. These calls are apparently // redundant on Windows 8 and up. Sleep(2000); trace("parent:----"); trace("parent: starting child process"); SetStdHandle(STD_OUTPUT_HANDLE, newBuffer); SetStdHandle(STD_ERROR_HANDLE, newBuffer); startChildInSameConsole(L"D:child"); SetStdHandle(STD_OUTPUT_HANDLE, origBuffer); SetStdHandle(STD_ERROR_HANDLE, origBuffer); // At t=6, write again to newBuffer. Sleep(4000); trace("parent:----"); writeTest(newBuffer, "TEST PASSED!"); // At t=8, close the newBuffer. In earlier versions of windows // (including Server 2008 R2), the console then switches back to // origBuffer. As of Windows 8, it doesn't, because somehow the child // process is keeping the console on newBuffer, even though the child // process closed its STDIN/STDOUT/STDERR handles. Killing the child // process by hand after the test finishes *does* force the console // back to origBuffer. Sleep(2000); closeHandle(newBuffer); Sleep(120000); return; } if (!strcmp(argv[1], "D:child")) { g_prefix = "child: "; // At t=2, the child starts. trace("child:----"); dumpHandles(); writeTest("writing to newBuffer"); // At t=4, the child explicitly closes its handle. Sleep(2000); trace("child:----"); if (GetStdHandle(STD_ERROR_HANDLE) != GetStdHandle(STD_OUTPUT_HANDLE)) { closeHandle(GetStdHandle(STD_ERROR_HANDLE)); } closeHandle(GetStdHandle(STD_OUTPUT_HANDLE)); closeHandle(GetStdHandle(STD_INPUT_HANDLE)); Sleep(120000); return; } } int main(int argc, char *argv[]) { if (argc == 1) { printf("USAGE: %s testnum\n", argv[0]); return 0; } if (argv[1][0] == '1') { test1(argc, argv); } else if (argv[1][0] == '2') { test2(argc, argv); } else if (argv[1][0] == 'A') { testA(argc, argv); } else if (argv[1][0] == 'B') { testB(argc, argv); } else if (argv[1][0] == 'C') { testC(argc, argv); } else if (argv[1][0] == 'D') { testD(argc, argv); } return 0; }