From 1d2ffe9109da12fff4a7f890159ca677b3f85f41 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 17 Jul 2024 03:02:07 +0200 Subject: [PATCH] Add helper functions for pipes (#17566) Split off from #17510: * `HandleWantsOverlappedIo` can be used to check if a handle requires overlapped IO. This is important, as `ReadFile` and `WriteFile` are documented to not work correctly if an overlapped handle is used without overlapped IO and vice versa. In my tests with pipes, this appears to be true. * `CreatePipe` creates a synchronous, unidirectional pipe. * `CreateOverlappedPipe` does what it says on the tin, while allowing you to specify the direction of the pipe (in, out, duplex). * `GetOverlappedResultSameThread` is largely the same as `GetOverlappedResult`, but adds back a neat optimization from the time before Windows 7. I thought it was neat. --- .github/actions/spelling/expect/expect.txt | 1 + src/types/inc/utils.hpp | 10 + src/types/utils.cpp | 208 ++++++++++++++++++++- 3 files changed, 215 insertions(+), 4 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 6495be1be4..bc19bfc8d4 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1205,6 +1205,7 @@ nouicompat nounihan NOYIELD NOZORDER +NPFS nrcs NSTATUS ntapi diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index 477229c51f..ff5ac5129d 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -15,6 +15,12 @@ Author(s): namespace Microsoft::Console::Utils { + struct Pipe + { + wil::unique_hfile server; + wil::unique_hfile client; + }; + // Function Description: // - Returns -1, 0 or +1 to indicate the sign of the passed-in value. template @@ -24,6 +30,10 @@ namespace Microsoft::Console::Utils } bool IsValidHandle(const HANDLE handle) noexcept; + bool HandleWantsOverlappedIo(HANDLE handle) noexcept; + Pipe CreatePipe(DWORD bufferSize); + Pipe CreateOverlappedPipe(DWORD openMode, DWORD bufferSize); + HRESULT GetOverlappedResultSameThread(const OVERLAPPED* overlapped, DWORD* bytesTransferred) noexcept; // Function Description: // - Clamps a long in between `min` and `SHRT_MAX` diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 53cc3d9505..ed112bc1c1 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -4,13 +4,11 @@ #include "precomp.h" #include "inc/utils.hpp" -#include +#include +#include #include "inc/colorTable.hpp" -#include -#include - using namespace Microsoft::Console; // Routine Description: @@ -631,6 +629,208 @@ bool Utils::IsValidHandle(const HANDLE handle) noexcept return handle != nullptr && handle != INVALID_HANDLE_VALUE; } +#define FileModeInformation (FILE_INFORMATION_CLASS)16 + +#define FILE_PIPE_BYTE_STREAM_TYPE 0x00000000 +#define FILE_PIPE_BYTE_STREAM_MODE 0x00000000 +#define FILE_PIPE_QUEUE_OPERATION 0x00000000 + +typedef struct _FILE_MODE_INFORMATION +{ + ULONG Mode; +} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION; + +extern "C" NTSTATUS NTAPI NtQueryInformationFile( + HANDLE FileHandle, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass); + +extern "C" NTSTATUS NTAPI NtCreateNamedPipeFile( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, + ULONG CreateDisposition, + ULONG CreateOptions, + ULONG NamedPipeType, + ULONG ReadMode, + ULONG CompletionMode, + ULONG MaximumInstances, + ULONG InboundQuota, + ULONG OutboundQuota, + PLARGE_INTEGER DefaultTimeout); + +bool Utils::HandleWantsOverlappedIo(HANDLE handle) noexcept +{ + IO_STATUS_BLOCK statusBlock; + FILE_MODE_INFORMATION modeInfo; + const auto status = NtQueryInformationFile(handle, &statusBlock, &modeInfo, sizeof(modeInfo), FileModeInformation); + return status == 0 && WI_AreAllFlagsClear(modeInfo.Mode, FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT); +} + +// Creates an anonymous pipe. Behaves like PIPE_ACCESS_INBOUND, +// meaning the .server is for reading and the .client is for writing. +Utils::Pipe Utils::CreatePipe(DWORD bufferSize) +{ + wil::unique_hfile rx, tx; + THROW_IF_WIN32_BOOL_FALSE(::CreatePipe(rx.addressof(), tx.addressof(), nullptr, bufferSize)); + return { std::move(rx), std::move(tx) }; +} + +// Creates an overlapped anonymous pipe. openMode should be either: +// * PIPE_ACCESS_INBOUND +// * PIPE_ACCESS_OUTBOUND +// * PIPE_ACCESS_DUPLEX +// +// I know, I know. MSDN infamously says +// > Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. +// but that's a lie. The only reason they're not supported is because the Win32 +// API doesn't have a parameter where you could pass FILE_FLAG_OVERLAPPED! +// So, we'll simply use the underlying NT APIs instead. +// +// Most code on the internet suggests creating named pipes with a random name, +// but usually conveniently forgets to mention that named pipes require strict ACLs. +// https://stackoverflow.com/q/60645 for instance contains a lot of poor advice. +// Anonymous pipes also cannot be discovered via NtQueryDirectoryFile inside the NPFS driver, +// whereas running a tool like Sysinternals' PipeList will return all those semi-named pipes. +// +// The code below contains comments to create unidirectional pipes. +Utils::Pipe Utils::CreateOverlappedPipe(DWORD openMode, DWORD bufferSize) +{ + LARGE_INTEGER timeout = { .QuadPart = -10'0000'0000 }; // 1 second + UNICODE_STRING emptyPath{}; + IO_STATUS_BLOCK statusBlock; + OBJECT_ATTRIBUTES objectAttributes{ + .Length = sizeof(OBJECT_ATTRIBUTES), + .ObjectName = &emptyPath, + .Attributes = OBJ_CASE_INSENSITIVE, + }; + DWORD serverDesiredAccess = 0; + DWORD clientDesiredAccess = 0; + DWORD serverShareAccess = 0; + DWORD clientShareAccess = 0; + + switch (openMode) + { + case PIPE_ACCESS_INBOUND: + serverDesiredAccess = SYNCHRONIZE | GENERIC_READ | FILE_WRITE_ATTRIBUTES; + clientDesiredAccess = SYNCHRONIZE | GENERIC_WRITE | FILE_READ_ATTRIBUTES; + serverShareAccess = FILE_SHARE_WRITE; + clientShareAccess = FILE_SHARE_READ; + break; + case PIPE_ACCESS_OUTBOUND: + serverDesiredAccess = SYNCHRONIZE | GENERIC_WRITE | FILE_READ_ATTRIBUTES; + clientDesiredAccess = SYNCHRONIZE | GENERIC_READ | FILE_WRITE_ATTRIBUTES; + serverShareAccess = FILE_SHARE_READ; + clientShareAccess = FILE_SHARE_WRITE; + break; + case PIPE_ACCESS_DUPLEX: + serverDesiredAccess = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE; + clientDesiredAccess = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE; + serverShareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE; + clientShareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE; + break; + default: + THROW_HR(E_UNEXPECTED); + } + + // Cache a handle to the pipe driver. + static const auto pipeDirectory = []() { + UNICODE_STRING path = RTL_CONSTANT_STRING(L"\\Device\\NamedPipe\\"); + + OBJECT_ATTRIBUTES objectAttributes{ + .Length = sizeof(OBJECT_ATTRIBUTES), + .ObjectName = &path, + }; + + wil::unique_hfile dir; + IO_STATUS_BLOCK statusBlock; + THROW_IF_NTSTATUS_FAILED(NtCreateFile( + /* FileHandle */ dir.addressof(), + /* DesiredAccess */ SYNCHRONIZE | GENERIC_READ, + /* ObjectAttributes */ &objectAttributes, + /* IoStatusBlock */ &statusBlock, + /* AllocationSize */ nullptr, + /* FileAttributes */ 0, + /* ShareAccess */ FILE_SHARE_READ | FILE_SHARE_WRITE, + /* CreateDisposition */ FILE_OPEN, + /* CreateOptions */ FILE_SYNCHRONOUS_IO_NONALERT, + /* EaBuffer */ nullptr, + /* EaLength */ 0)); + + return dir; + }(); + + wil::unique_hfile server; + objectAttributes.RootDirectory = pipeDirectory.get(); + THROW_IF_NTSTATUS_FAILED(NtCreateNamedPipeFile( + /* FileHandle */ server.addressof(), + /* DesiredAccess */ serverDesiredAccess, + /* ObjectAttributes */ &objectAttributes, + /* IoStatusBlock */ &statusBlock, + /* ShareAccess */ serverShareAccess, + /* CreateDisposition */ FILE_CREATE, + /* CreateOptions */ 0, // would be FILE_SYNCHRONOUS_IO_NONALERT for a synchronous pipe + /* NamedPipeType */ FILE_PIPE_BYTE_STREAM_TYPE, + /* ReadMode */ FILE_PIPE_BYTE_STREAM_MODE, + /* CompletionMode */ FILE_PIPE_QUEUE_OPERATION, // would be FILE_PIPE_COMPLETE_OPERATION for PIPE_NOWAIT + /* MaximumInstances */ 1, + /* InboundQuota */ bufferSize, + /* OutboundQuota */ bufferSize, + /* DefaultTimeout */ &timeout)); + + wil::unique_hfile client; + objectAttributes.RootDirectory = server.get(); + THROW_IF_NTSTATUS_FAILED(NtCreateFile( + /* FileHandle */ client.addressof(), + /* DesiredAccess */ clientDesiredAccess, + /* ObjectAttributes */ &objectAttributes, + /* IoStatusBlock */ &statusBlock, + /* AllocationSize */ nullptr, + /* FileAttributes */ 0, + /* ShareAccess */ clientShareAccess, + /* CreateDisposition */ FILE_OPEN, + /* CreateOptions */ FILE_NON_DIRECTORY_FILE, // would include FILE_SYNCHRONOUS_IO_NONALERT for a synchronous pipe + /* EaBuffer */ nullptr, + /* EaLength */ 0)); + + return { std::move(server), std::move(client) }; +} + +// GetOverlappedResult() for professionals! Only for single-threaded use. +// +// GetOverlappedResult() used to have a neat optimization where it would only call WaitForSingleObject() if the state was STATUS_PENDING. +// That got removed in Windows 7, because people kept starting a read/write on one thread and called GetOverlappedResult() on another. +// When the OS sets Internal from STATUS_PENDING to 0 (= done) and then flags the hEvent, that doesn't happen atomically. +// This results in a race condition if a OVERLAPPED is used across threads. +HRESULT Utils::GetOverlappedResultSameThread(const OVERLAPPED* overlapped, DWORD* bytesTransferred) noexcept +{ + assert(overlapped != nullptr); + assert(overlapped->hEvent != nullptr); + assert(bytesTransferred != nullptr); + + __assume(overlapped != nullptr); + __assume(overlapped->hEvent != nullptr); + __assume(bytesTransferred != nullptr); + + if (overlapped->Internal == STATUS_PENDING) + { + if (WaitForSingleObjectEx(overlapped->hEvent, INFINITE, FALSE) != WAIT_OBJECT_0) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + } + + // Assuming no multi-threading as per the function contract and + // now that we ensured that hEvent is set (= read/write done), + // we can safely read whatever want because nothing will set these concurrently. + *bytesTransferred = gsl::narrow_cast(overlapped->InternalHigh); + return HRESULT_FROM_NT(overlapped->Internal); +} + // Function Description: // - Generate a Version 5 UUID (specified in RFC4122 4.3) // v5 UUIDs are stable given the same namespace and "name".