Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper functions for pipes #17566

Merged
merged 2 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,7 @@ nouicompat
nounihan
NOYIELD
NOZORDER
NPFS
nrcs
NSTATUS
ntapi
Expand Down
10 changes: 10 additions & 0 deletions src/types/inc/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename T>
Expand All @@ -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`
Expand Down
208 changes: 204 additions & 4 deletions src/types/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
#include "precomp.h"
#include "inc/utils.hpp"

#include <propsys.h>
#include <til/string.h>
#include <wil/token_helpers.h>

#include "inc/colorTable.hpp"

#include <wil/token_helpers.h>
#include <til/string.h>

using namespace Microsoft::Console;

// Routine Description:
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you point to official docs explaining why we can do this safely and above-board?

Copy link
Member Author

@lhecker lhecker Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean why we can safely create a pipe without a name, now and in the future? Outside of the function description, there are no official docs about how NPFS works, both publicly and internally. It basically works "as designed". I believe it's safe to rely on it for 2 reasons:

  • NPFS last changed in Windows Vista
  • There are no filter driver events for the creation of anonymous pipes. The only way to know about them is by subscribing to FO_NAMED_PIPE and testing for the name length. Anonymous pipes have an empty name. This effectively makes it a public contract.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine with me!

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal - is this documented anywhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep: https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-overlapped
It does say "its behavior may change" but I think that's a minor concern for us, given that we're effectively a system component. It also hasn't changed since "Windows NT 1.0" (I honestly don't know but basically since forever). The reason it's unlikely to change is because the two Internal fields overlap with NT's IO_STATUS_BLOCK struct, whose 2 fields are publicly documented and don't mention this "its behavior may change".

{
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<DWORD>(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".
Expand Down
Loading