// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "winconpty.h" #ifdef __INSIDE_WINDOWS #include #include // You need kernelbasestaging.h to be able to use wil in libraries consumed by kernelbase.dll #include #define RESOURCE_SUPPRESS_STL #define WIL_SUPPORT_BITOPERATION_PASCAL_NAMES #include #else #include "device.h" #include #endif // __INSIDE_WINDOWS #pragma warning(push) #pragma warning(disable : 4273) // inconsistent dll linkage (we are exporting things kernel32 also exports) #pragma warning(disable : 26485) // array-to-pointer decay is virtually impossible to avoid when we can't use STL. // Function Description: // - Returns the path to conhost.exe as a process heap string. static wil::unique_process_heap_string _InboxConsoleHostPath() { wil::unique_process_heap_string systemDirectory; wil::GetSystemDirectoryW(systemDirectory); return wil::str_concat_failfast(L"\\\\?\\", systemDirectory, L"\\conhost.exe"); } // Function Description: // - Returns the path to either conhost.exe or the side-by-side OpenConsole, depending on whether this // module is building with Windows and OpenConsole could be found. // Return Value: // - A pointer to permanent storage containing the path to the console host. static wchar_t* _ConsoleHostPath() { // Use the magic of magic statics to only calculate this once. static wil::unique_process_heap_string consoleHostPath = []() { #if defined(__INSIDE_WINDOWS) return _InboxConsoleHostPath(); #else // Use the STL only if we're not building in Windows. std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; modulePath.replace_filename(L"OpenConsole.exe"); if (!std::filesystem::exists(modulePath)) { return _InboxConsoleHostPath(); } auto modulePathAsString{ modulePath.wstring() }; return wil::make_process_heap_string_nothrow(modulePathAsString.data(), modulePathAsString.size()); #endif // __INSIDE_WINDOWS }(); return consoleHostPath.get(); } static bool _HandleIsValid(HANDLE h) noexcept { return (h != INVALID_HANDLE_VALUE) && (h != nullptr); } HRESULT _CreatePseudoConsole(const HANDLE hToken, const COORD size, const HANDLE hInput, const HANDLE hOutput, const DWORD dwFlags, _Inout_ PseudoConsole* pPty) { if (pPty == nullptr) { return E_INVALIDARG; } if (size.X == 0 || size.Y == 0) { return E_INVALIDARG; } wil::unique_handle serverHandle; RETURN_IF_NTSTATUS_FAILED(CreateServerHandle(serverHandle.addressof(), TRUE)); wil::unique_handle signalPipeConhostSide; wil::unique_handle signalPipeOurSide; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); // Mark inheritable for signal handle when creating. It'll have the same value on the other side. sa.bInheritHandle = FALSE; sa.lpSecurityDescriptor = nullptr; RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeConhostSide.addressof(), signalPipeOurSide.addressof(), &sa, 0)); RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); // GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files const wchar_t* pwszFormat = L"\"%s\" --headless %s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; // This is plenty of space to hold the formatted string wchar_t cmd[MAX_PATH]{}; const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR; const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK; const BOOL bWin32InputMode = (dwFlags & PSEUDOCONSOLE_WIN32_INPUT_MODE) == PSEUDOCONSOLE_WIN32_INPUT_MODE; swprintf_s(cmd, MAX_PATH, pwszFormat, _ConsoleHostPath(), bInheritCursor ? L"--inheritcursor " : L"", bWin32InputMode ? L"--win32input " : L"", bResizeQuirk ? L"--resizeQuirk " : L"", size.X, size.Y, signalPipeConhostSide.get(), serverHandle.get()); STARTUPINFOEXW siEx{ 0 }; siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW); siEx.StartupInfo.hStdInput = hInput; siEx.StartupInfo.hStdOutput = hOutput; siEx.StartupInfo.hStdError = hOutput; siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; // Only pass the handles we actually want the conhost to know about to it: const size_t INHERITED_HANDLES_COUNT = 4; HANDLE inheritedHandles[INHERITED_HANDLES_COUNT]; inheritedHandles[0] = serverHandle.get(); inheritedHandles[1] = hInput; inheritedHandles[2] = hOutput; inheritedHandles[3] = signalPipeConhostSide.get(); // Get the size of the attribute list. We need one attribute, the handle list. SIZE_T listSize = 0; InitializeProcThreadAttributeList(nullptr, 1, 0, &listSize); // I have to use a HeapAlloc here because kernelbase can't link new[] or delete[] PPROC_THREAD_ATTRIBUTE_LIST attrList = static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, listSize)); RETURN_IF_NULL_ALLOC(attrList); auto attrListDelete = wil::scope_exit([&]() noexcept { HeapFree(GetProcessHeap(), 0, attrList); }); siEx.lpAttributeList = attrList; RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &listSize)); // Set cleanup data for ProcThreadAttributeList when successful. auto cleanupProcThreadAttribute = wil::scope_exit([&]() noexcept { DeleteProcThreadAttributeList(siEx.lpAttributeList); }); RETURN_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(siEx.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inheritedHandles, (INHERITED_HANDLES_COUNT * sizeof(HANDLE)), nullptr, nullptr)); wil::unique_process_information pi; { // wow64 disabled filesystem redirection scope #if defined(BUILD_WOW6432) PVOID RedirectionFlag; RETURN_IF_NTSTATUS_FAILED(RtlWow64EnableFsRedirectionEx( WOW64_FILE_SYSTEM_DISABLE_REDIRECT, &RedirectionFlag)); auto resetFsRedirection = wil::scope_exit([&] { RtlWow64EnableFsRedirectionEx(RedirectionFlag, &RedirectionFlag); }); #endif if (hToken == INVALID_HANDLE_VALUE || hToken == nullptr) { // Call create process RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(_ConsoleHostPath(), cmd, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &siEx.StartupInfo, pi.addressof())); } else { // Call create process RETURN_IF_WIN32_BOOL_FALSE(CreateProcessAsUserW(hToken, _ConsoleHostPath(), cmd, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &siEx.StartupInfo, pi.addressof())); } } // Move the process handle out of the PROCESS_INFORMATION into out Pseudoconsole pPty->hConPtyProcess = pi.hProcess; pi.hProcess = nullptr; RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(&pPty->hPtyReference, serverHandle.get(), L"\\Reference", FALSE)); pPty->hSignal = signalPipeOurSide.release(); return S_OK; } // Function Description: // - Resizes the conpty // Arguments: // - hSignal: A signal pipe as returned by CreateConPty. // - size: The new dimensions of the conpty, in characters. // Return Value: // - S_OK if the call succeeded, else an appropriate HRESULT for failing to // write the resize message to the pty. HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size) { if (pPty == nullptr || size.X < 0 || size.Y < 0) { return E_INVALIDARG; } unsigned short signalPacket[3]; signalPacket[0] = PTY_SIGNAL_RESIZE_WINDOW; signalPacket[1] = size.X; signalPacket[2] = size.Y; const BOOL fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); } // Function Description: // - This closes each of the members of a PseudoConsole. It does not free the // data associated with the PseudoConsole. This is helpful for testing, // where we might stack allocate a PseudoConsole (instead of getting a // HPCON via the API). // Arguments: // - pPty: A pointer to a PseudoConsole struct. // Return Value: // - void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty) { if (pPty != nullptr) { // See MSFT:19918626 // First break the signal pipe - this will trigger conhost to tear itself down if (_HandleIsValid(pPty->hSignal)) { CloseHandle(pPty->hSignal); pPty->hSignal = nullptr; } // Then, wait on the conhost process before killing it. // We do this to make sure the conhost finishes flushing any output it // has yet to send before we hard kill it. if (_HandleIsValid(pPty->hConPtyProcess)) { // If the conhost is already dead, then that's fine. Presumably // it's finished flushing it's output already. DWORD dwExit = 0; // If GetExitCodeProcess failed, it's likely conhost is already dead // If so, skip waiting regardless of whatever error // GetExitCodeProcess returned. // We'll just go straight to killing conhost. if (GetExitCodeProcess(pPty->hConPtyProcess, &dwExit) && dwExit == STILL_ACTIVE) { WaitForSingleObject(pPty->hConPtyProcess, INFINITE); } TerminateProcess(pPty->hConPtyProcess, 0); CloseHandle(pPty->hConPtyProcess); pPty->hConPtyProcess = nullptr; } // Then take care of the reference handle. // TODO GH#1810: Closing the reference handle late leaves conhost thinking // that we have an outstanding connected client. if (_HandleIsValid(pPty->hPtyReference)) { CloseHandle(pPty->hPtyReference); pPty->hPtyReference = nullptr; } } } // Function Description: // - This closes each of the members of a PseudoConsole, and HeapFree's the // memory allocated to it. This should be used to cleanup any // PseudoConsoles that were created with CreatePseudoConsole. // Arguments: // - pPty: A pointer to a PseudoConsole struct. // Return Value: // - VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty) { if (pPty != nullptr) { _ClosePseudoConsoleMembers(pPty); HeapFree(GetProcessHeap(), 0, pPty); } } // These functions are defined in the console l1 apiset, which is generated from // the consoleapi.apx file in minkernel\apiset\libs\Console. // Function Description: // Creates a "Pseudo-console" (conpty) with dimensions (in characters) // provided by the `size` parameter. The caller should provide two handles: // - `hInput` is used for writing input to the pty, encoded as UTF-8 and VT sequences. // - `hOutput` is used for reading the output of the pty, encoded as UTF-8 and VT sequences. // Once the call completes, `phPty` will receive a token value to identify this // conpty object. This value should be used in conjunction with the other // Pseudoconsole API's. // `dwFlags` is used to specify optional behavior to the created pseudoconsole. // The flags can be combinations of the following values: // INHERIT_CURSOR: This will cause the created conpty to attempt to inherit the // cursor position of the parent terminal application. This can be useful // for applications like `ssh`, where ssh (currently running in a terminal) // might want to create a pseudoterminal session for an child application // and the child inherit the cursor position of ssh. // The created conpty will immediately emit a "Device Status Request" VT // sequence to hOutput, that should be replied to on hInput in the format // "\x1b[;R", where `` is the row and `` is the column of the // cursor position. // This requires a cooperating terminal application - if a caller does not // reply to this message, the conpty will not process any input until it // does. Most *nix terminals and the Windows Console (after Windows 10 // Anniversary Update) will be able to handle such a message. extern "C" HRESULT WINAPI ConptyCreatePseudoConsole(_In_ COORD size, _In_ HANDLE hInput, _In_ HANDLE hOutput, _In_ DWORD dwFlags, _Out_ HPCON* phPC) { return ConptyCreatePseudoConsoleAsUser(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, phPC); } extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, _In_ COORD size, _In_ HANDLE hInput, _In_ HANDLE hOutput, _In_ DWORD dwFlags, _Out_ HPCON* phPC) { if (phPC == nullptr) { return E_INVALIDARG; } *phPC = nullptr; if ((!_HandleIsValid(hInput)) && (!_HandleIsValid(hOutput))) { return E_INVALIDARG; } PseudoConsole* pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole)); RETURN_IF_NULL_ALLOC(pPty); auto cleanupPty = wil::scope_exit([&]() noexcept { _ClosePseudoConsole(pPty); }); wil::unique_handle duplicatedInput; wil::unique_handle duplicatedOutput; RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), hInput, GetCurrentProcess(), duplicatedInput.addressof(), 0, TRUE, DUPLICATE_SAME_ACCESS)); RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), hOutput, GetCurrentProcess(), duplicatedOutput.addressof(), 0, TRUE, DUPLICATE_SAME_ACCESS)); RETURN_IF_FAILED(_CreatePseudoConsole(hToken, size, duplicatedInput.get(), duplicatedOutput.get(), dwFlags, pPty)); *phPC = (HPCON)pPty; cleanupPty.release(); return S_OK; } // Function Description: // Resizes the given conpty to the specified size, in characters. extern "C" HRESULT WINAPI ConptyResizePseudoConsole(_In_ HPCON hPC, _In_ COORD size) { const PseudoConsole* const pPty = (PseudoConsole*)hPC; HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK; if (SUCCEEDED(hr)) { hr = _ResizePseudoConsole(pPty, size); } return hr; } // Function Description: // Closes the conpty and all associated state. // Client applications attached to the conpty will also behave as though the // console window they were running in was closed. // This can fail if the conhost hosting the pseudoconsole failed to be // terminated, or if the pseudoconsole was already terminated. extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC) { PseudoConsole* const pPty = (PseudoConsole*)hPC; if (pPty != nullptr) { _ClosePseudoConsole(pPty); } } // NOTE: This one is not defined in the Windows headers but is // necessary for our outside recipient in the Terminal // to set up a PTY session in fundamentally the same way as the // Creation functions. Using the same HPCON pack enables // resizing and closing to "just work." // Function Description: // Packs loose handle information for an inbound ConPTY // session into the same HPCON as a created session. extern "C" HRESULT WINAPI ConptyPackPseudoConsole(_In_ HANDLE hProcess, _In_ HANDLE hRef, _In_ HANDLE hSignal, _Out_ HPCON* phPC) { RETURN_HR_IF(E_INVALIDARG, nullptr == phPC); *phPC = nullptr; RETURN_HR_IF(E_INVALIDARG, !_HandleIsValid(hProcess)); RETURN_HR_IF(E_INVALIDARG, !_HandleIsValid(hRef)); RETURN_HR_IF(E_INVALIDARG, !_HandleIsValid(hSignal)); PseudoConsole* pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole)); RETURN_IF_NULL_ALLOC(pPty); pPty->hConPtyProcess = hProcess; pPty->hPtyReference = hRef; pPty->hSignal = hSignal; *phPC = (HPCON)pPty; return S_OK; } #pragma warning(pop)