Files
PowerToys/src/common/UnitTests-CommonUtils/TwoWayPipeMessageIPC.Tests.cpp
Gordon Lam ed7595f3a7 Harden IPC pipe ownership and shutdown lifecycle (#48902)
## Summary of the Pull Request

The two-way named-pipe IPC server (`TwoWayPipeMessageIPC`, shared by the
runner, Settings, and Quick Access host) created every pipe instance
without `FILE_FLAG_FIRST_PIPE_INSTANCE`. If a pipe with the same name
already existed — for example a leftover instance from a previous run or
another process — `CreateNamedPipe` would quietly create an *additional*
instance and share the name instead of owning it.

This makes `start_named_pipe_server` create the **first** instance with
`FILE_FLAG_FIRST_PIPE_INSTANCE`, so `CreateNamedPipe` fails fast on a
name collision and the server is the authoritative owner of its pipe
name.

## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
(N/A — no user-facing strings)
- [x] **Dev docs:** Added/updated (N/A)
- [x] **New binaries:** Added on the required places (N/A — no new
binaries)

## Detailed Description of the Pull Request / Additional comments

- The flag is applied **only** to the first instance. Subsequent
instances continue to omit it, so the existing
`PIPE_UNLIMITED_INSTANCES` behavior is fully preserved.
- The change is contained to a single function in
`src/common/interop/two_way_pipe_message_ipc.cpp`. Public signatures and
the `PowerToys.Interop` ABI are unchanged, so the runner, Settings, and
Quick Access host all benefit without any code changes on their side.

## Validation Steps Performed

- The existing `Common.Interop.UnitTests` `TestSend` exercises the
modified first-instance code path (`Start()` →
`start_named_pipe_server`) and continues to pass — a full IPC round-trip
still works.
- Verified the updated `CreateNamedPipe` open-mode logic compiles
cleanly.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 49797c8c-784d-47e6-bc0f-53464eecec4b
2026-08-09 20:56:12 +08:00

1243 lines
56 KiB
C++

#include "pch.h"
#include <interop/two_way_pipe_message_ipc.h>
#include <aclapi.h>
#include "..\..\modules\Workspaces\WorkspacesLib\IPCHelper.h"
#include <memory>
#include <system_error>
#include <thread>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace UnitTestsCommonUtils
{
namespace
{
constexpr DWORD PipeClientAccess = FILE_READ_DATA |
FILE_READ_ATTRIBUTES |
READ_CONTROL |
FILE_WRITE_DATA |
FILE_WRITE_ATTRIBUTES |
SYNCHRONIZE;
std::wstring UniquePipeName()
{
static LONG counter = 0;
return L"\\\\.\\pipe\\pt_ipc_test_" +
std::to_wstring(GetCurrentProcessId()) + L"_" +
std::to_wstring(GetTickCount64()) + L"_" +
std::to_wstring(InterlockedIncrement(&counter));
}
std::wstring CurrentExePath()
{
wchar_t path[MAX_PATH * 2]{};
GetModuleFileNameW(nullptr, path, ARRAYSIZE(path));
return path;
}
std::wstring DirectoryOf(const std::wstring& path)
{
const auto separator = path.find_last_of(L"\\/");
return separator == std::wstring::npos ? path : path.substr(0, separator);
}
std::wstring BaseNameOf(const std::wstring& path)
{
const auto separator = path.find_last_of(L"\\/");
return separator == std::wstring::npos ? path : path.substr(separator + 1);
}
interop_auth::CallerPolicy SelfCallerPolicy()
{
const std::wstring executable = CurrentExePath();
interop_auth::CallerPolicy policy;
policy.enabled = true;
policy.expectedDirectory = DirectoryOf(executable);
policy.allowedBasenames = { BaseNameOf(executable) };
policy.requireMicrosoftSignature = false;
return policy;
}
bool WriteTestMessage(HANDLE pipe)
{
constexpr wchar_t message[] = L"test";
DWORD bytes_written = 0;
return WriteFile(pipe,
message,
(ARRAYSIZE(message) - 1) * sizeof(wchar_t),
&bytes_written,
nullptr) == TRUE;
}
void AssertRogueServerCannotImpersonateClient(HANDLE server)
{
if (!ImpersonateNamedPipeClient(server))
{
return;
}
HANDLE token = nullptr;
Assert::IsTrue(OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &token) == TRUE,
L"rogue server impersonated the client but could not inspect its token");
SECURITY_IMPERSONATION_LEVEL level{};
DWORD level_size = 0;
Assert::IsTrue(GetTokenInformation(token, TokenImpersonationLevel, &level, sizeof(level), &level_size) == TRUE);
CloseHandle(token);
RevertToSelf();
Assert::AreEqual(static_cast<int>(SecurityIdentification), static_cast<int>(level),
L"the rogue server received an impersonation-capable client token");
}
HANDLE CreateRogueServer(const std::wstring& pipe_name)
{
return CreateNamedPipeW(pipe_name.c_str(),
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1,
4096,
4096,
0,
nullptr);
}
HANDLE ConnectPipeClient(const std::wstring& pipe_name)
{
constexpr DWORD timeout_ms = 2'000;
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
do
{
HANDLE client = CreateFileW(pipe_name.c_str(),
PipeClientAccess,
0,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (client != INVALID_HANDLE_VALUE)
{
return client;
}
const DWORD error = GetLastError();
if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PIPE_BUSY)
{
return INVALID_HANDLE_VALUE;
}
WaitNamedPipeW(pipe_name.c_str(), 50);
} while (GetTickCount64() < deadline);
SetLastError(ERROR_SEM_TIMEOUT);
return INVALID_HANDLE_VALUE;
}
struct RestrictedClientToken
{
HANDLE token = nullptr;
~RestrictedClientToken()
{
if (token)
{
CloseHandle(token);
}
}
bool Create()
{
HANDLE process_token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &process_token))
{
return false;
}
DWORD user_size = 0;
GetTokenInformation(process_token, TokenUser, nullptr, 0, &user_size);
std::vector<BYTE> user_buffer(user_size);
if (!GetTokenInformation(process_token, TokenUser, user_buffer.data(), user_size, &user_size))
{
CloseHandle(process_token);
return false;
}
auto* user = reinterpret_cast<TOKEN_USER*>(user_buffer.data());
SID_AND_ATTRIBUTES disabled_sid{ user->User.Sid, 0 };
HANDLE restricted_primary_token = nullptr;
const BOOL restricted = CreateRestrictedToken(process_token,
0,
1,
&disabled_sid,
0,
nullptr,
0,
nullptr,
&restricted_primary_token);
CloseHandle(process_token);
if (!restricted)
{
return false;
}
const BOOL duplicated = DuplicateTokenEx(restricted_primary_token,
TOKEN_QUERY | TOKEN_IMPERSONATE,
nullptr,
SecurityImpersonation,
TokenImpersonation,
&token);
CloseHandle(restricted_primary_token);
return duplicated == TRUE;
}
};
struct NormalSameUserClientToken
{
HANDLE token = nullptr;
~NormalSameUserClientToken()
{
if (token)
{
CloseHandle(token);
}
}
bool Create()
{
HANDLE process_token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &process_token))
{
return false;
}
BYTE administrators_sid[SECURITY_MAX_SID_SIZE]{};
DWORD administrators_sid_size = ARRAYSIZE(administrators_sid);
if (!CreateWellKnownSid(WinBuiltinAdministratorsSid,
nullptr,
administrators_sid,
&administrators_sid_size))
{
CloseHandle(process_token);
return false;
}
DWORD groups_size = 0;
GetTokenInformation(process_token, TokenGroups, nullptr, 0, &groups_size);
std::vector<BYTE> groups_buffer(groups_size);
if (!GetTokenInformation(process_token, TokenGroups, groups_buffer.data(), groups_size, &groups_size))
{
CloseHandle(process_token);
return false;
}
const auto* groups = reinterpret_cast<const TOKEN_GROUPS*>(groups_buffer.data());
SID_AND_ATTRIBUTES disabled_administrators_sid{};
DWORD disable_count = 0;
for (DWORD index = 0; index < groups->GroupCount; ++index)
{
if (EqualSid(groups->Groups[index].Sid, administrators_sid))
{
disabled_administrators_sid.Sid = groups->Groups[index].Sid;
disable_count = 1;
break;
}
}
HANDLE restricted_primary_token = nullptr;
const BOOL restricted = CreateRestrictedToken(process_token,
0,
disable_count,
disable_count ? &disabled_administrators_sid : nullptr,
0,
nullptr,
0,
nullptr,
&restricted_primary_token);
CloseHandle(process_token);
if (!restricted)
{
return false;
}
const BOOL duplicated = DuplicateTokenEx(restricted_primary_token,
TOKEN_QUERY | TOKEN_IMPERSONATE,
nullptr,
SecurityImpersonation,
TokenImpersonation,
&token);
CloseHandle(restricted_primary_token);
return duplicated == TRUE;
}
};
struct ScopedImpersonation
{
explicit ScopedImpersonation(HANDLE token) :
active(ImpersonateLoggedOnUser(token) == TRUE)
{
}
~ScopedImpersonation()
{
if (active)
{
RevertToSelf();
}
}
bool active = false;
};
std::mutex fault_injection_test_mutex;
struct FaultInjectionReset
{
std::unique_lock<std::mutex> lock{ fault_injection_test_mutex };
FaultInjectionReset()
{
two_way_pipe_message_ipc_test::ResetFaultInjection();
}
~FaultInjectionReset()
{
two_way_pipe_message_ipc_test::ResetFaultInjection();
}
};
bool LogonSidPipeAceAllowsInstanceCreation(HANDLE pipe,
HANDLE token,
bool& allows_client_access,
DWORD& matching_access_mask,
DWORD& error)
{
allows_client_access = false;
matching_access_mask = 0;
DWORD groups_size = 0;
GetTokenInformation(token, TokenGroups, nullptr, 0, &groups_size);
std::vector<BYTE> groups_buffer(groups_size);
if (!GetTokenInformation(token, TokenGroups, groups_buffer.data(), groups_size, &groups_size))
{
error = GetLastError();
return false;
}
const auto* groups = reinterpret_cast<const TOKEN_GROUPS*>(groups_buffer.data());
PSID logon_sid = nullptr;
for (DWORD index = 0; index < groups->GroupCount; ++index)
{
if ((groups->Groups[index].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID)
{
logon_sid = groups->Groups[index].Sid;
break;
}
}
if (!logon_sid)
{
error = ERROR_NOT_FOUND;
return false;
}
PSECURITY_DESCRIPTOR security_descriptor = nullptr;
PACL dacl = nullptr;
const DWORD security_result = GetSecurityInfo(pipe,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
nullptr,
nullptr,
&dacl,
nullptr,
&security_descriptor);
if (security_result != ERROR_SUCCESS)
{
error = security_result;
return false;
}
bool allows_creation = false;
ACL_SIZE_INFORMATION acl_info{};
if (!GetAclInformation(dacl, &acl_info, sizeof(acl_info), AclSizeInformation))
{
error = GetLastError();
LocalFree(security_descriptor);
return false;
}
for (DWORD index = 0; index < acl_info.AceCount; ++index)
{
void* ace = nullptr;
if (!GetAce(dacl, index, &ace))
{
error = GetLastError();
LocalFree(security_descriptor);
return false;
}
auto* allowed_ace = static_cast<ACCESS_ALLOWED_ACE*>(ace);
if (allowed_ace->Header.AceType != ACCESS_ALLOWED_ACE_TYPE ||
!EqualSid(logon_sid, reinterpret_cast<PSID>(&allowed_ace->SidStart)))
{
continue;
}
const DWORD access_mask = allowed_ace->Mask;
matching_access_mask |= access_mask;
allows_creation |= (access_mask & (GENERIC_WRITE | FILE_CREATE_PIPE_INSTANCE)) != 0;
allows_client_access |= (access_mask & PipeClientAccess) == PipeClientAccess;
}
LocalFree(security_descriptor);
error = ERROR_SUCCESS;
return allows_creation;
}
struct OccupiedPipe
{
std::wstring name = UniquePipeName();
HANDLE server = INVALID_HANDLE_VALUE;
HANDLE client = INVALID_HANDLE_VALUE;
~OccupiedPipe()
{
if (client != INVALID_HANDLE_VALUE)
{
CloseHandle(client);
}
if (server != INVALID_HANDLE_VALUE)
{
CloseHandle(server);
}
}
bool Create()
{
server = CreateNamedPipeW(name.c_str(),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
4096,
4096,
0,
nullptr);
if (server == INVALID_HANDLE_VALUE)
{
return false;
}
std::thread connectThread([&]() {
client = CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
});
const BOOL connected = ConnectNamedPipe(server, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
connectThread.join();
return connected && client != INVALID_HANDLE_VALUE;
}
};
struct NonReadingPipePeer
{
std::wstring name = UniquePipeName();
HANDLE server = INVALID_HANDLE_VALUE;
HANDLE connected = CreateEventW(nullptr, TRUE, FALSE, nullptr);
std::thread accept_thread;
~NonReadingPipePeer()
{
if (server != INVALID_HANDLE_VALUE)
{
DisconnectNamedPipe(server);
CloseHandle(server);
}
if (accept_thread.joinable())
{
accept_thread.join();
}
if (connected)
{
CloseHandle(connected);
}
}
bool Start()
{
server = CreateNamedPipeW(name.c_str(),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1,
1,
1,
0,
nullptr);
if (server == INVALID_HANDLE_VALUE)
{
return false;
}
accept_thread = std::thread([this]() {
const BOOL accepted = ConnectNamedPipe(server, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (accepted)
{
SetEvent(connected);
}
});
return true;
}
};
struct BlockedRejectedConnection
{
HANDLE client = INVALID_HANDLE_VALUE;
HANDLE handler_entered = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE allow_handler_to_finish = CreateEventW(nullptr, TRUE, FALSE, nullptr);
~BlockedRejectedConnection()
{
if (client != INVALID_HANDLE_VALUE)
{
CloseHandle(client);
}
if (handler_entered)
{
CloseHandle(handler_entered);
}
if (allow_handler_to_finish)
{
CloseHandle(allow_handler_to_finish);
}
}
bool Start(TwoWayPipeMessageIPC& server, const std::wstring& input_pipe_name)
{
interop_auth::CallerPolicy policy;
policy.enabled = true;
policy.expectedDirectory = L"Z:\\not-the-test-host";
policy.allowedBasenames = { L"not-the-test-host.exe" };
policy.requireMicrosoftSignature = false;
policy.logReject = [this](const interop_auth::AuthResult&) {
SetEvent(handler_entered);
WaitForSingleObject(allow_handler_to_finish, 10'000);
};
server.start(nullptr, policy);
client = ConnectPipeClient(input_pipe_name);
return client != INVALID_HANDLE_VALUE &&
WaitForSingleObject(handler_entered, 2'000) == WAIT_OBJECT_0;
}
void AllowHandlerToFinish()
{
SetEvent(allow_handler_to_finish);
}
};
}
TEST_CLASS(TwoWayPipeMessageIPCTests)
{
public:
TEST_METHOD(ServerDoesNotJoinAnExistingPipeName)
{
OccupiedPipe occupiedPipe;
Assert::IsTrue(occupiedPipe.Create(), L"failed to occupy the pipe name");
TwoWayPipeMessageIPC server(occupiedPipe.name, UniquePipeName(), nullptr);
server.start(nullptr);
// The existing instance is busy. A server that wrongly creates a second instance makes
// WaitNamedPipe succeed; FILE_FLAG_FIRST_PIPE_INSTANCE must instead make its first
// CreateNamedPipe call fail and leave no available instance.
const BOOL available = WaitNamedPipeW(occupiedPipe.name.c_str(), 2000);
server.end();
Assert::IsFalse(available, L"the server must not join an existing pipe name");
}
TEST_METHOD(CommonOutboundPipeClientUsesIdentificationQos)
{
const std::wstring rogue_pipe_name = UniquePipeName();
HANDLE rogue_server = CreateRogueServer(rogue_pipe_name);
Assert::IsTrue(rogue_server != INVALID_HANDLE_VALUE, L"failed to create the rogue common IPC server");
TwoWayPipeMessageIPC client(UniquePipeName(), rogue_pipe_name, nullptr);
client.start(nullptr);
client.send(L"message");
const BOOL connected = ConnectNamedPipe(rogue_server, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
Assert::IsTrue(connected == TRUE, L"the common IPC client did not connect to the rogue server");
AssertRogueServerCannotImpersonateClient(rogue_server);
wchar_t message[16]{};
DWORD bytes_read = 0;
ReadFile(rogue_server, message, sizeof(message), &bytes_read, nullptr);
client.end();
DisconnectNamedPipe(rogue_server);
CloseHandle(rogue_server);
}
TEST_METHOD(WorkspacesLauncherArrangerClientUsesIdentificationQos)
{
const std::wstring& pipe_name = IPCHelperStrings::LauncherArrangerPipeName;
HANDLE rogue_server = CreateRogueServer(pipe_name);
Assert::IsTrue(rogue_server != INVALID_HANDLE_VALUE,
L"failed to claim the static LauncherArranger pipe name for the rogue server");
HANDLE client = INVALID_HANDLE_VALUE;
std::thread connect_thread([&]() {
client = CreateFileW(pipe_name.c_str(),
PipeClientAccess,
0,
nullptr,
OPEN_EXISTING,
two_way_pipe_message_ipc::ClientOpenFlags,
nullptr);
});
const BOOL connected = ConnectNamedPipe(rogue_server, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
connect_thread.join();
Assert::IsTrue(connected == TRUE && client != INVALID_HANDLE_VALUE,
L"the LauncherArranger client could not connect to the rogue server");
AssertRogueServerCannotImpersonateClient(rogue_server);
CloseHandle(client);
DisconnectNamedPipe(rogue_server);
CloseHandle(rogue_server);
}
TEST_METHOD(RestrictedClientCanConnectButCannotCreateAnotherServerInstance)
{
HANDLE token = nullptr;
Assert::IsTrue(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token) == TRUE,
L"failed to open the current process token");
RestrictedClientToken restricted_client;
Assert::IsTrue(restricted_client.Create(), L"failed to create the restricted same-logon client token");
const std::wstring input_pipe_name = UniquePipeName();
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), nullptr);
server.start(token);
{
ScopedImpersonation impersonation(restricted_client.token);
Assert::IsTrue(impersonation.active, L"failed to impersonate the restricted client token");
HANDLE client = ConnectPipeClient(input_pipe_name);
const DWORD connect_error = GetLastError();
Assert::IsTrue(client != INVALID_HANDLE_VALUE,
(L"the explicitly-permitted client access must connect; error=" +
std::to_wstring(connect_error))
.c_str());
// A later CreateNamedPipe call is authorized by the first instance's DACL. Verify
// that the ACE for this same-logon client contains every requested client right
// but excludes FILE_CREATE_PIPE_INSTANCE (also included by GENERIC_WRITE).
DWORD acl_error = ERROR_SUCCESS;
bool acl_allows_client_access = false;
DWORD matching_access_mask = 0;
bool can_create_later_instance = false;
const ULONGLONG acl_deadline = GetTickCount64() + 2'000;
do
{
can_create_later_instance = LogonSidPipeAceAllowsInstanceCreation(client,
token,
acl_allows_client_access,
matching_access_mask,
acl_error);
if (acl_error != ERROR_SUCCESS || acl_allows_client_access)
{
break;
}
Sleep(10);
} while (GetTickCount64() < acl_deadline);
CloseHandle(client);
Assert::IsTrue(acl_allows_client_access,
(L"the same-logon client ACE must contain the explicit client access rights; mask=" +
std::to_wstring(matching_access_mask))
.c_str());
Assert::IsFalse(can_create_later_instance,
L"a same-logon client must not create a later pipe instance");
Assert::AreEqual(static_cast<DWORD>(ERROR_SUCCESS), acl_error);
}
server.end();
CloseHandle(token);
}
TEST_METHOD(NormalSameUserCannotModifyProtectedDaclOrCreateAnotherServerInstance)
{
HANDLE token = nullptr;
Assert::IsTrue(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token) == TRUE);
const std::wstring input_pipe_name = UniquePipeName();
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), nullptr);
server.start(token);
CloseHandle(token);
NormalSameUserClientToken normal_client;
Assert::IsTrue(normal_client.Create(), L"failed to create the normal same-user client token");
{
ScopedImpersonation impersonation(normal_client.token);
Assert::IsTrue(impersonation.active, L"failed to impersonate the normal same-user client token");
HANDLE client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(client != INVALID_HANDLE_VALUE, L"the normal client could not connect to the protected pipe");
PSECURITY_DESCRIPTOR security_descriptor = nullptr;
PSID owner = nullptr;
PACL dacl = nullptr;
Assert::AreEqual(static_cast<DWORD>(ERROR_SUCCESS),
GetSecurityInfo(client,
SE_KERNEL_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&owner,
nullptr,
&dacl,
nullptr,
&security_descriptor));
BYTE administrators_sid[SECURITY_MAX_SID_SIZE]{};
DWORD administrators_sid_size = ARRAYSIZE(administrators_sid);
Assert::IsTrue(CreateWellKnownSid(WinBuiltinAdministratorsSid,
nullptr,
administrators_sid,
&administrators_sid_size) == TRUE);
Assert::IsTrue(EqualSid(owner, administrators_sid) == TRUE,
L"the pipe owner must not be the normal client user");
const DWORD set_dacl_error = SetSecurityInfo(client,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
nullptr,
nullptr,
dacl,
nullptr);
SetLastError(ERROR_SUCCESS);
HANDLE rogue_server = CreateNamedPipeW(input_pipe_name.c_str(),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
4096,
4096,
0,
nullptr);
const DWORD create_instance_error = GetLastError();
if (rogue_server != INVALID_HANDLE_VALUE)
{
CloseHandle(rogue_server);
}
LocalFree(security_descriptor);
CloseHandle(client);
Assert::AreEqual(static_cast<DWORD>(ERROR_ACCESS_DENIED), set_dacl_error);
Assert::IsTrue(rogue_server == INVALID_HANDLE_VALUE,
L"the normal same-user client created a later server instance");
Assert::AreEqual(static_cast<DWORD>(ERROR_ACCESS_DENIED), create_instance_error);
}
server.end();
}
TEST_METHOD(RejectedClientRapidCloseNeverReleasesPipeName)
{
HANDLE token = nullptr;
Assert::IsTrue(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token) == TRUE);
const std::wstring input_pipe_name = UniquePipeName();
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), nullptr);
interop_auth::CallerPolicy reject_policy;
reject_policy.enabled = true;
reject_policy.expectedDirectory = L"Z:\\not-the-test-host";
reject_policy.allowedBasenames = { L"not-the-test-host.exe" };
reject_policy.requireMicrosoftSignature = false;
server.start(token, reject_policy);
CloseHandle(token);
NormalSameUserClientToken normal_client;
Assert::IsTrue(normal_client.Create(), L"failed to create the normal same-user client token");
{
ScopedImpersonation impersonation(normal_client.token);
Assert::IsTrue(impersonation.active, L"failed to impersonate the normal same-user client token");
HANDLE client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(client != INVALID_HANDLE_VALUE, L"the rejected client could not connect");
CloseHandle(client);
for (int attempt = 0; attempt < 100; ++attempt)
{
SetLastError(ERROR_SUCCESS);
HANDLE rogue_server = CreateNamedPipeW(input_pipe_name.c_str(),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
4096,
4096,
0,
nullptr);
const DWORD create_error = GetLastError();
if (rogue_server != INVALID_HANDLE_VALUE)
{
CloseHandle(rogue_server);
}
Assert::IsTrue(rogue_server == INVALID_HANDLE_VALUE,
L"the pipe name was released while a rejected client closed");
Assert::AreEqual(static_cast<DWORD>(ERROR_ACCESS_DENIED), create_error);
Sleep(1);
}
}
server.end();
}
TEST_METHOD(ReplacementListenerIsReservedBeforeRejectedHandlerStarts)
{
FaultInjectionReset reset;
HANDLE before_replacement = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE allow_replacement = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE handler_rejected = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(before_replacement);
Assert::IsNotNull(allow_replacement);
Assert::IsNotNull(handler_rejected);
two_way_pipe_message_ipc_test::SetBeforeReplacementListenerEvents(before_replacement, allow_replacement);
HANDLE token = nullptr;
Assert::IsTrue(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token) == TRUE);
const std::wstring input_pipe_name = UniquePipeName();
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), nullptr);
interop_auth::CallerPolicy reject_policy;
reject_policy.enabled = true;
reject_policy.expectedDirectory = L"Z:\\not-the-test-host";
reject_policy.allowedBasenames = { L"not-the-test-host.exe" };
reject_policy.requireMicrosoftSignature = false;
reject_policy.logReject = [handler_rejected](const interop_auth::AuthResult&) {
SetEvent(handler_rejected);
};
server.start(token, reject_policy);
CloseHandle(token);
NormalSameUserClientToken normal_client;
Assert::IsTrue(normal_client.Create(), L"failed to create the normal same-user client token");
{
ScopedImpersonation impersonation(normal_client.token);
Assert::IsTrue(impersonation.active, L"failed to impersonate the normal same-user client token");
HANDLE client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(client != INVALID_HANDLE_VALUE, L"the rejected client could not connect");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(before_replacement, 2'000),
L"the server did not begin reserving a replacement listener");
Assert::AreEqual(static_cast<DWORD>(WAIT_TIMEOUT), WaitForSingleObject(handler_rejected, 0),
L"the rejected handler started before its replacement listener was reserved");
SetLastError(ERROR_SUCCESS);
HANDLE rogue_server = CreateNamedPipeW(input_pipe_name.c_str(),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
4096,
4096,
0,
nullptr);
const DWORD create_error = GetLastError();
if (rogue_server != INVALID_HANDLE_VALUE)
{
CloseHandle(rogue_server);
}
Assert::IsTrue(rogue_server == INVALID_HANDLE_VALUE,
L"the pipe name was released before the replacement listener existed");
Assert::AreEqual(static_cast<DWORD>(ERROR_ACCESS_DENIED), create_error);
CloseHandle(client);
}
SetEvent(allow_replacement);
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(handler_rejected, 2'000),
L"the rejected handler did not run after the replacement was created");
server.end();
two_way_pipe_message_ipc_test::SetBeforeReplacementListenerEvents(nullptr, nullptr);
CloseHandle(before_replacement);
CloseHandle(allow_replacement);
CloseHandle(handler_rejected);
}
TEST_METHOD(OwnedSecurityTokenSupportsReplacementAfterCallerClosesIt)
{
HANDLE caller_token = nullptr;
Assert::IsTrue(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &caller_token) == TRUE);
const std::wstring input_pipe_name = UniquePipeName();
HANDLE first_client_dispatched = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE two_clients_dispatched = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(first_client_dispatched);
Assert::IsNotNull(two_clients_dispatched);
std::atomic<int> dispatch_count = 0;
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), [&](const std::wstring&) {
const int count = ++dispatch_count;
if (count == 1)
{
SetEvent(first_client_dispatched);
}
else if (count == 2)
{
SetEvent(two_clients_dispatched);
}
});
server.start(caller_token, SelfCallerPolicy());
CloseHandle(caller_token);
HANDLE first_client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(first_client != INVALID_HANDLE_VALUE, L"the first client could not connect");
Assert::IsTrue(WriteTestMessage(first_client), L"the first client could not write");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(first_client_dispatched, 2'000),
L"the first client was not authenticated and dispatched");
CloseHandle(first_client);
HANDLE second_client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(second_client != INVALID_HANDLE_VALUE,
L"the replacement listener did not survive the caller token closing");
Assert::IsTrue(WriteTestMessage(second_client), L"the second client could not write");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(two_clients_dispatched, 2'000),
L"the replacement listener did not authenticate the second client");
CloseHandle(second_client);
server.end();
CloseHandle(first_client_dispatched);
CloseHandle(two_clients_dispatched);
}
TEST_METHOD(ShutdownClosesReplacementReservedDuringHandoff)
{
FaultInjectionReset reset;
HANDLE after_replacement = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE allow_handoff = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE shutdown_finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(after_replacement);
Assert::IsNotNull(allow_handoff);
Assert::IsNotNull(shutdown_finished);
two_way_pipe_message_ipc_test::SetAfterReplacementListenerEvents(after_replacement, allow_handoff);
const std::wstring input_pipe_name = UniquePipeName();
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), nullptr);
server.start(nullptr);
HANDLE client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(client != INVALID_HANDLE_VALUE, L"the handoff client could not connect");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(after_replacement, 2'000),
L"the replacement listener was not created");
std::thread shutdown_thread([&]() {
server.end();
SetEvent(shutdown_finished);
});
Assert::AreEqual(static_cast<DWORD>(WAIT_TIMEOUT), WaitForSingleObject(shutdown_finished, 200),
L"shutdown unexpectedly completed before the handoff race was released");
SetEvent(allow_handoff);
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(shutdown_finished, 5'000),
L"shutdown did not close the reserved replacement listener");
shutdown_thread.join();
CloseHandle(client);
HANDLE probe = CreateNamedPipeW(input_pipe_name.c_str(),
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1,
4096,
4096,
0,
nullptr);
Assert::IsTrue(probe != INVALID_HANDLE_VALUE,
L"shutdown leaked a replacement listener reservation");
CloseHandle(probe);
two_way_pipe_message_ipc_test::SetAfterReplacementListenerEvents(nullptr, nullptr);
CloseHandle(after_replacement);
CloseHandle(allow_handoff);
CloseHandle(shutdown_finished);
}
TEST_METHOD(HandlerThreadStartFailureTransfersAndClosesPipeOnce)
{
FaultInjectionReset reset;
HANDLE handler_start_attempted = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(handler_start_attempted);
two_way_pipe_message_ipc_test::FailHandlerThreadStartAfter(0);
two_way_pipe_message_ipc_test::SetHandlerThreadStartAttemptEvent(handler_start_attempted);
const std::wstring input_pipe_name = UniquePipeName();
HANDLE dispatched = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(dispatched);
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), [dispatched](const std::wstring&) {
SetEvent(dispatched);
});
server.start(nullptr);
HANDLE first_client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(first_client != INVALID_HANDLE_VALUE, L"the first client could not connect");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(handler_start_attempted, 2'000),
L"the injected handler-start failure was not consumed for the first client");
CloseHandle(first_client);
HANDLE second_client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(second_client != INVALID_HANDLE_VALUE,
L"the listener did not remain usable after handler thread creation failed");
Assert::IsTrue(WriteTestMessage(second_client), L"the second client could not write");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(dispatched, 2'000),
L"the replacement listener did not process the second client");
CloseHandle(second_client);
server.end();
two_way_pipe_message_ipc_test::SetHandlerThreadStartAttemptEvent(nullptr);
CloseHandle(handler_start_attempted);
CloseHandle(dispatched);
}
TEST_METHOD(StartFailureAfterFirstThreadCleansUp)
{
FaultInjectionReset reset;
auto server = std::make_unique<TwoWayPipeMessageIPC>(UniquePipeName(), UniquePipeName(), nullptr);
two_way_pipe_message_ipc_test::FailThreadStartAfter(1);
bool threw = false;
try
{
server->start(nullptr);
}
catch (const std::system_error&)
{
threw = true;
}
Assert::IsTrue(threw, L"the injected second thread creation failure was not observed");
server->end();
server.reset();
}
TEST_METHOD(StartFailureAfterSecondThreadCleansUp)
{
FaultInjectionReset reset;
auto server = std::make_unique<TwoWayPipeMessageIPC>(UniquePipeName(), UniquePipeName(), nullptr);
two_way_pipe_message_ipc_test::FailThreadStartAfter(2);
bool threw = false;
try
{
server->start(nullptr);
}
catch (const std::system_error&)
{
threw = true;
}
Assert::IsTrue(threw, L"the injected third thread creation failure was not observed");
server->end();
server.reset();
}
TEST_METHOD(EndWaitsForActiveConnectionHandler)
{
const std::wstring input_pipe_name = UniquePipeName();
TwoWayPipeMessageIPC server(input_pipe_name, UniquePipeName(), nullptr);
BlockedRejectedConnection connection;
Assert::IsTrue(connection.Start(server, input_pipe_name),
L"the test connection did not enter its handler");
HANDLE end_finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(end_finished);
std::thread shutdown_thread([&]() {
server.end();
SetEvent(end_finished);
});
Assert::AreEqual(static_cast<DWORD>(WAIT_TIMEOUT), WaitForSingleObject(end_finished, 200),
L"end must wait for the active handler before returning");
connection.AllowHandlerToFinish();
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(end_finished, 5'000),
L"end did not finish after the active handler completed");
shutdown_thread.join();
CloseHandle(end_finished);
}
TEST_METHOD(DestructorWaitsForActiveConnectionHandler)
{
const std::wstring input_pipe_name = UniquePipeName();
auto server = std::make_unique<TwoWayPipeMessageIPC>(input_pipe_name, UniquePipeName(), nullptr);
BlockedRejectedConnection connection;
Assert::IsTrue(connection.Start(*server, input_pipe_name),
L"the test connection did not enter its handler");
HANDLE destructor_finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(destructor_finished);
std::thread destroyer([&]() {
server.reset();
SetEvent(destructor_finished);
});
Assert::AreEqual(static_cast<DWORD>(WAIT_TIMEOUT), WaitForSingleObject(destructor_finished, 200),
L"destruction must wait for the active handler before freeing IPC state");
connection.AllowHandlerToFinish();
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(destructor_finished, 5'000),
L"destruction did not finish after the active handler completed");
destroyer.join();
CloseHandle(destructor_finished);
}
TEST_METHOD(DestructorJoinsHandlerAfterCompletion)
{
FaultInjectionReset reset;
HANDLE handler_completed = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE allow_handler_return = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE destructor_finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE handler_rejected = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(handler_completed);
Assert::IsNotNull(allow_handler_return);
Assert::IsNotNull(destructor_finished);
Assert::IsNotNull(handler_rejected);
two_way_pipe_message_ipc_test::SetHandlerCompletionEvents(handler_completed, allow_handler_return);
const std::wstring input_pipe_name = UniquePipeName();
auto server = std::make_unique<TwoWayPipeMessageIPC>(input_pipe_name, UniquePipeName(), nullptr);
interop_auth::CallerPolicy reject_policy;
reject_policy.enabled = true;
reject_policy.expectedDirectory = L"Z:\\not-the-test-host";
reject_policy.allowedBasenames = { L"not-the-test-host.exe" };
reject_policy.requireMicrosoftSignature = false;
reject_policy.logReject = [handler_rejected](const interop_auth::AuthResult&) {
SetEvent(handler_rejected);
};
server->start(nullptr, reject_policy);
HANDLE client = ConnectPipeClient(input_pipe_name);
Assert::IsTrue(client != INVALID_HANDLE_VALUE, L"the rejected client could not connect");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(handler_rejected, 2'000),
L"the handler did not reject the test client");
CloseHandle(client);
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(handler_completed, 2'000),
L"the handler did not reach its completion point");
std::thread destroyer([&]() {
server.reset();
SetEvent(destructor_finished);
});
Assert::AreEqual(static_cast<DWORD>(WAIT_TIMEOUT), WaitForSingleObject(destructor_finished, 200),
L"destruction returned before the completed handler thread was joined");
SetEvent(allow_handler_return);
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(destructor_finished, 5'000));
destroyer.join();
two_way_pipe_message_ipc_test::SetHandlerCompletionEvents(nullptr, nullptr);
CloseHandle(handler_completed);
CloseHandle(allow_handler_return);
CloseHandle(destructor_finished);
CloseHandle(handler_rejected);
}
TEST_METHOD(DestructorCancelsBlockedConnectionRead)
{
const std::wstring input_pipe_name = UniquePipeName();
auto server = std::make_unique<TwoWayPipeMessageIPC>(input_pipe_name, UniquePipeName(), nullptr);
HANDLE server_token = nullptr;
Assert::IsTrue(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &server_token) == TRUE);
server->start(server_token);
CloseHandle(server_token);
RestrictedClientToken restricted_client;
Assert::IsTrue(restricted_client.Create(), L"failed to create the restricted same-logon client token");
HANDLE client = INVALID_HANDLE_VALUE;
{
ScopedImpersonation impersonation(restricted_client.token);
Assert::IsTrue(impersonation.active, L"failed to impersonate the restricted client token");
client = ConnectPipeClient(input_pipe_name);
}
const DWORD connect_error = GetLastError();
Assert::IsTrue(client != INVALID_HANDLE_VALUE,
(L"failed to connect the client that blocks in ReadFile; error=" +
std::to_wstring(connect_error))
.c_str());
// The next listener is created only after the accepted connection has been registered
// for lifetime tracking, so destruction must cancel that handler's blocked read.
Assert::IsTrue(WaitNamedPipeW(input_pipe_name.c_str(), 2'000) == TRUE,
L"the server did not create the next listening instance");
HANDLE destructor_finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(destructor_finished);
std::thread destroyer([&]() {
server.reset();
SetEvent(destructor_finished);
});
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(destructor_finished, 5'000),
L"destruction did not cancel and join the handler blocked in ReadFile");
destroyer.join();
CloseHandle(destructor_finished);
CloseHandle(client);
}
TEST_METHOD(EndInterruptsBusyOutputPipeWait)
{
FaultInjectionReset reset;
OccupiedPipe busy_output_pipe;
Assert::IsTrue(busy_output_pipe.Create(), L"failed to create the busy output pipe");
HANDLE wait_entered = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(wait_entered);
two_way_pipe_message_ipc_test::SetWaitNamedPipeEnteredEvent(wait_entered);
TwoWayPipeMessageIPC server(UniquePipeName(), busy_output_pipe.name, nullptr);
server.start(nullptr);
server.send(L"message");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(wait_entered, 2'000),
L"the output worker did not enter WaitNamedPipe");
const auto start = std::chrono::steady_clock::now();
server.end();
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
two_way_pipe_message_ipc_test::SetWaitNamedPipeEnteredEvent(nullptr);
CloseHandle(wait_entered);
Assert::IsTrue(elapsed.count() < 1'000,
L"end waited too long for an unavailable output pipe");
}
TEST_METHOD(DestructorCancelsPendingOutputWrite)
{
FaultInjectionReset reset;
NonReadingPipePeer peer;
Assert::IsTrue(peer.Start(), L"failed to create the non-reading output peer");
HANDLE write_pending = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE destructor_finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Assert::IsNotNull(write_pending);
Assert::IsNotNull(destructor_finished);
two_way_pipe_message_ipc_test::SetOutputWritePendingEvent(write_pending);
auto server = std::make_unique<TwoWayPipeMessageIPC>(UniquePipeName(), peer.name, nullptr);
server->start(nullptr);
server->send(std::wstring(512 * 1024, L'x'));
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(write_pending, 5'000),
L"the output write did not become pending against the non-reading peer");
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(peer.connected, 5'000),
L"the output peer did not accept the connection");
peer.accept_thread.join();
const auto start = std::chrono::steady_clock::now();
std::thread destroyer([&]() {
server.reset();
SetEvent(destructor_finished);
});
Assert::AreEqual(static_cast<DWORD>(WAIT_OBJECT_0), WaitForSingleObject(destructor_finished, 1'000),
L"destruction did not cancel the pending output write");
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
destroyer.join();
two_way_pipe_message_ipc_test::SetOutputWritePendingEvent(nullptr);
CloseHandle(write_pending);
CloseHandle(destructor_finished);
Assert::IsTrue(elapsed.count() < 1'000,
L"destruction waited too long for the non-reading output peer");
}
};
}