Skip to content

Commit d68c6a5

Browse files
authored
Add helper functions for pipes (microsoft#17566)
Split off from microsoft#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.
1 parent de50310 commit d68c6a5

File tree

3 files changed

+215
-4
lines changed

3 files changed

+215
-4
lines changed

.github/actions/spelling/expect/expect.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,7 @@ nouicompat
12051205
nounihan
12061206
NOYIELD
12071207
NOZORDER
1208+
NPFS
12081209
nrcs
12091210
NSTATUS
12101211
ntapi

src/types/inc/utils.hpp

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ Author(s):
1515

1616
namespace Microsoft::Console::Utils
1717
{
18+
struct Pipe
19+
{
20+
wil::unique_hfile server;
21+
wil::unique_hfile client;
22+
};
23+
1824
// Function Description:
1925
// - Returns -1, 0 or +1 to indicate the sign of the passed-in value.
2026
template<typename T>
@@ -24,6 +30,10 @@ namespace Microsoft::Console::Utils
2430
}
2531

2632
bool IsValidHandle(const HANDLE handle) noexcept;
33+
bool HandleWantsOverlappedIo(HANDLE handle) noexcept;
34+
Pipe CreatePipe(DWORD bufferSize);
35+
Pipe CreateOverlappedPipe(DWORD openMode, DWORD bufferSize);
36+
HRESULT GetOverlappedResultSameThread(const OVERLAPPED* overlapped, DWORD* bytesTransferred) noexcept;
2737

2838
// Function Description:
2939
// - Clamps a long in between `min` and `SHRT_MAX`

src/types/utils.cpp

+204-4
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
#include "precomp.h"
55
#include "inc/utils.hpp"
66

7-
#include <propsys.h>
7+
#include <til/string.h>
8+
#include <wil/token_helpers.h>
89

910
#include "inc/colorTable.hpp"
1011

11-
#include <wil/token_helpers.h>
12-
#include <til/string.h>
13-
1412
using namespace Microsoft::Console;
1513

1614
// Routine Description:
@@ -631,6 +629,208 @@ bool Utils::IsValidHandle(const HANDLE handle) noexcept
631629
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
632630
}
633631

632+
#define FileModeInformation (FILE_INFORMATION_CLASS)16
633+
634+
#define FILE_PIPE_BYTE_STREAM_TYPE 0x00000000
635+
#define FILE_PIPE_BYTE_STREAM_MODE 0x00000000
636+
#define FILE_PIPE_QUEUE_OPERATION 0x00000000
637+
638+
typedef struct _FILE_MODE_INFORMATION
639+
{
640+
ULONG Mode;
641+
} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION;
642+
643+
extern "C" NTSTATUS NTAPI NtQueryInformationFile(
644+
HANDLE FileHandle,
645+
PIO_STATUS_BLOCK IoStatusBlock,
646+
PVOID FileInformation,
647+
ULONG Length,
648+
FILE_INFORMATION_CLASS FileInformationClass);
649+
650+
extern "C" NTSTATUS NTAPI NtCreateNamedPipeFile(
651+
PHANDLE FileHandle,
652+
ULONG DesiredAccess,
653+
POBJECT_ATTRIBUTES ObjectAttributes,
654+
PIO_STATUS_BLOCK IoStatusBlock,
655+
ULONG ShareAccess,
656+
ULONG CreateDisposition,
657+
ULONG CreateOptions,
658+
ULONG NamedPipeType,
659+
ULONG ReadMode,
660+
ULONG CompletionMode,
661+
ULONG MaximumInstances,
662+
ULONG InboundQuota,
663+
ULONG OutboundQuota,
664+
PLARGE_INTEGER DefaultTimeout);
665+
666+
bool Utils::HandleWantsOverlappedIo(HANDLE handle) noexcept
667+
{
668+
IO_STATUS_BLOCK statusBlock;
669+
FILE_MODE_INFORMATION modeInfo;
670+
const auto status = NtQueryInformationFile(handle, &statusBlock, &modeInfo, sizeof(modeInfo), FileModeInformation);
671+
return status == 0 && WI_AreAllFlagsClear(modeInfo.Mode, FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT);
672+
}
673+
674+
// Creates an anonymous pipe. Behaves like PIPE_ACCESS_INBOUND,
675+
// meaning the .server is for reading and the .client is for writing.
676+
Utils::Pipe Utils::CreatePipe(DWORD bufferSize)
677+
{
678+
wil::unique_hfile rx, tx;
679+
THROW_IF_WIN32_BOOL_FALSE(::CreatePipe(rx.addressof(), tx.addressof(), nullptr, bufferSize));
680+
return { std::move(rx), std::move(tx) };
681+
}
682+
683+
// Creates an overlapped anonymous pipe. openMode should be either:
684+
// * PIPE_ACCESS_INBOUND
685+
// * PIPE_ACCESS_OUTBOUND
686+
// * PIPE_ACCESS_DUPLEX
687+
//
688+
// I know, I know. MSDN infamously says
689+
// > Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
690+
// but that's a lie. The only reason they're not supported is because the Win32
691+
// API doesn't have a parameter where you could pass FILE_FLAG_OVERLAPPED!
692+
// So, we'll simply use the underlying NT APIs instead.
693+
//
694+
// Most code on the internet suggests creating named pipes with a random name,
695+
// but usually conveniently forgets to mention that named pipes require strict ACLs.
696+
// https://stackoverflow.com/q/60645 for instance contains a lot of poor advice.
697+
// Anonymous pipes also cannot be discovered via NtQueryDirectoryFile inside the NPFS driver,
698+
// whereas running a tool like Sysinternals' PipeList will return all those semi-named pipes.
699+
//
700+
// The code below contains comments to create unidirectional pipes.
701+
Utils::Pipe Utils::CreateOverlappedPipe(DWORD openMode, DWORD bufferSize)
702+
{
703+
LARGE_INTEGER timeout = { .QuadPart = -10'0000'0000 }; // 1 second
704+
UNICODE_STRING emptyPath{};
705+
IO_STATUS_BLOCK statusBlock;
706+
OBJECT_ATTRIBUTES objectAttributes{
707+
.Length = sizeof(OBJECT_ATTRIBUTES),
708+
.ObjectName = &emptyPath,
709+
.Attributes = OBJ_CASE_INSENSITIVE,
710+
};
711+
DWORD serverDesiredAccess = 0;
712+
DWORD clientDesiredAccess = 0;
713+
DWORD serverShareAccess = 0;
714+
DWORD clientShareAccess = 0;
715+
716+
switch (openMode)
717+
{
718+
case PIPE_ACCESS_INBOUND:
719+
serverDesiredAccess = SYNCHRONIZE | GENERIC_READ | FILE_WRITE_ATTRIBUTES;
720+
clientDesiredAccess = SYNCHRONIZE | GENERIC_WRITE | FILE_READ_ATTRIBUTES;
721+
serverShareAccess = FILE_SHARE_WRITE;
722+
clientShareAccess = FILE_SHARE_READ;
723+
break;
724+
case PIPE_ACCESS_OUTBOUND:
725+
serverDesiredAccess = SYNCHRONIZE | GENERIC_WRITE | FILE_READ_ATTRIBUTES;
726+
clientDesiredAccess = SYNCHRONIZE | GENERIC_READ | FILE_WRITE_ATTRIBUTES;
727+
serverShareAccess = FILE_SHARE_READ;
728+
clientShareAccess = FILE_SHARE_WRITE;
729+
break;
730+
case PIPE_ACCESS_DUPLEX:
731+
serverDesiredAccess = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE;
732+
clientDesiredAccess = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE;
733+
serverShareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE;
734+
clientShareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE;
735+
break;
736+
default:
737+
THROW_HR(E_UNEXPECTED);
738+
}
739+
740+
// Cache a handle to the pipe driver.
741+
static const auto pipeDirectory = []() {
742+
UNICODE_STRING path = RTL_CONSTANT_STRING(L"\\Device\\NamedPipe\\");
743+
744+
OBJECT_ATTRIBUTES objectAttributes{
745+
.Length = sizeof(OBJECT_ATTRIBUTES),
746+
.ObjectName = &path,
747+
};
748+
749+
wil::unique_hfile dir;
750+
IO_STATUS_BLOCK statusBlock;
751+
THROW_IF_NTSTATUS_FAILED(NtCreateFile(
752+
/* FileHandle */ dir.addressof(),
753+
/* DesiredAccess */ SYNCHRONIZE | GENERIC_READ,
754+
/* ObjectAttributes */ &objectAttributes,
755+
/* IoStatusBlock */ &statusBlock,
756+
/* AllocationSize */ nullptr,
757+
/* FileAttributes */ 0,
758+
/* ShareAccess */ FILE_SHARE_READ | FILE_SHARE_WRITE,
759+
/* CreateDisposition */ FILE_OPEN,
760+
/* CreateOptions */ FILE_SYNCHRONOUS_IO_NONALERT,
761+
/* EaBuffer */ nullptr,
762+
/* EaLength */ 0));
763+
764+
return dir;
765+
}();
766+
767+
wil::unique_hfile server;
768+
objectAttributes.RootDirectory = pipeDirectory.get();
769+
THROW_IF_NTSTATUS_FAILED(NtCreateNamedPipeFile(
770+
/* FileHandle */ server.addressof(),
771+
/* DesiredAccess */ serverDesiredAccess,
772+
/* ObjectAttributes */ &objectAttributes,
773+
/* IoStatusBlock */ &statusBlock,
774+
/* ShareAccess */ serverShareAccess,
775+
/* CreateDisposition */ FILE_CREATE,
776+
/* CreateOptions */ 0, // would be FILE_SYNCHRONOUS_IO_NONALERT for a synchronous pipe
777+
/* NamedPipeType */ FILE_PIPE_BYTE_STREAM_TYPE,
778+
/* ReadMode */ FILE_PIPE_BYTE_STREAM_MODE,
779+
/* CompletionMode */ FILE_PIPE_QUEUE_OPERATION, // would be FILE_PIPE_COMPLETE_OPERATION for PIPE_NOWAIT
780+
/* MaximumInstances */ 1,
781+
/* InboundQuota */ bufferSize,
782+
/* OutboundQuota */ bufferSize,
783+
/* DefaultTimeout */ &timeout));
784+
785+
wil::unique_hfile client;
786+
objectAttributes.RootDirectory = server.get();
787+
THROW_IF_NTSTATUS_FAILED(NtCreateFile(
788+
/* FileHandle */ client.addressof(),
789+
/* DesiredAccess */ clientDesiredAccess,
790+
/* ObjectAttributes */ &objectAttributes,
791+
/* IoStatusBlock */ &statusBlock,
792+
/* AllocationSize */ nullptr,
793+
/* FileAttributes */ 0,
794+
/* ShareAccess */ clientShareAccess,
795+
/* CreateDisposition */ FILE_OPEN,
796+
/* CreateOptions */ FILE_NON_DIRECTORY_FILE, // would include FILE_SYNCHRONOUS_IO_NONALERT for a synchronous pipe
797+
/* EaBuffer */ nullptr,
798+
/* EaLength */ 0));
799+
800+
return { std::move(server), std::move(client) };
801+
}
802+
803+
// GetOverlappedResult() for professionals! Only for single-threaded use.
804+
//
805+
// GetOverlappedResult() used to have a neat optimization where it would only call WaitForSingleObject() if the state was STATUS_PENDING.
806+
// That got removed in Windows 7, because people kept starting a read/write on one thread and called GetOverlappedResult() on another.
807+
// When the OS sets Internal from STATUS_PENDING to 0 (= done) and then flags the hEvent, that doesn't happen atomically.
808+
// This results in a race condition if a OVERLAPPED is used across threads.
809+
HRESULT Utils::GetOverlappedResultSameThread(const OVERLAPPED* overlapped, DWORD* bytesTransferred) noexcept
810+
{
811+
assert(overlapped != nullptr);
812+
assert(overlapped->hEvent != nullptr);
813+
assert(bytesTransferred != nullptr);
814+
815+
__assume(overlapped != nullptr);
816+
__assume(overlapped->hEvent != nullptr);
817+
__assume(bytesTransferred != nullptr);
818+
819+
if (overlapped->Internal == STATUS_PENDING)
820+
{
821+
if (WaitForSingleObjectEx(overlapped->hEvent, INFINITE, FALSE) != WAIT_OBJECT_0)
822+
{
823+
return HRESULT_FROM_WIN32(GetLastError());
824+
}
825+
}
826+
827+
// Assuming no multi-threading as per the function contract and
828+
// now that we ensured that hEvent is set (= read/write done),
829+
// we can safely read whatever want because nothing will set these concurrently.
830+
*bytesTransferred = gsl::narrow_cast<DWORD>(overlapped->InternalHigh);
831+
return HRESULT_FROM_NT(overlapped->Internal);
832+
}
833+
634834
// Function Description:
635835
// - Generate a Version 5 UUID (specified in RFC4122 4.3)
636836
// v5 UUIDs are stable given the same namespace and "name".

0 commit comments

Comments
 (0)