Files
mirkobrombin 4b89a48e1e fix: bind completion ports late and honor pipe read modes
Assisted-by: Claude Code:claude-opus-5-5
AI-Scope: named pipe completion port lookup, read mode state and tests
2026-11-01 21:00:40 +00:00

923 lines
33 KiB
C++

#include "pipe.h"
#include "child_process.h"
#include "host_names.h"
#include "kernel_context.h"
#include "security_attributes.h"
#include "text.h"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace raven {
namespace {
constexpr uint32_t error_access_denied = 5, error_invalid_parameter = 87, error_file_not_found = 2, error_broken_pipe = 109, error_no_data = 232,
error_more_data = 234, error_pipe_busy = 231, error_pipe_connected = 535, error_pipe_listening = 536, error_not_connected = 233, error_io_pending = 997,
error_aborted = 995, error_io_incomplete = 996, error_not_found = 1168, error_invalid_function = 1, error_not_enough_memory = 8;
constexpr uint32_t status_pending = 0x103;
constexpr uint32_t file_flag_first_pipe_instance = 0x00080000U, pipe_type_message = 4;
constexpr uint64_t invalid_handle = UINT64_MAX;
// The pipes of one prefix share a namespace of abstract socket names, so processes that run in the same prefix find each other's pipes.
std::string pipe_address(const std::u16string &name)
{
auto lower = to_utf8(name);
for (auto &character : lower)
character = char(std::tolower(static_cast<unsigned char>(character)));
return "raven.pipe." + prefix_tag() + "." + hex_text(name_hash(lower));
}
socklen_t abstract_address(const std::string &address, sockaddr_un &storage)
{
std::memset(&storage, 0, sizeof(storage));
storage.sun_family = AF_UNIX;
std::memcpy(storage.sun_path + 1, address.data(), address.size());
return socklen_t(offsetof(sockaddr_un, sun_path) + 1 + address.size());
}
// OVERLAPPED holds NTSTATUS values; the errors the pipe calls report map to the statuses Windows uses for them.
uint32_t status_of(uint32_t error)
{
switch (error) {
case 0: return 0;
case error_broken_pipe: return 0xc000014bU;
case error_aborted: return 0xc0000120U;
case error_not_connected: return 0xc00000b0U;
case error_no_data: return 0xc00000b1U;
case error_pipe_listening: return 0xc00000b3U;
case error_more_data: return 0x80000005U;
default: return 0xc0070000U | (error & 0xffff);
}
}
uint32_t error_of_status(uint64_t status)
{
switch (uint32_t(status)) {
case 0: return 0;
case 0xc000014bU: return error_broken_pipe;
case 0xc0000120U: return error_aborted;
case 0xc00000b0U: return error_not_connected;
case 0xc00000b1U: return error_no_data;
case 0xc00000b3U: return error_pipe_listening;
case 0x80000005U: return error_more_data;
default:
if ((uint32_t(status) & 0xffff0000U) == 0xc0070000U)
return uint32_t(status) & 0xffff;
// A word that is no status holds a Windows error already, as the socket calls leave it.
return uint32_t(status);
}
}
void put_word(NativeProcess &process, uint64_t at, uint64_t value, size_t width)
{
process.write_memory(at, std::span<const uint8_t>(reinterpret_cast<const uint8_t *>(&value), width));
}
uint64_t get_word(NativeProcess &process, uint64_t at, size_t width)
{
const auto raw = process.read_memory(at, width);
uint64_t value = 0;
std::memcpy(&value, raw.data(), width);
return value;
}
// OVERLAPPED: the status and the byte count come first, the event handle after the two offset words.
// A native call names its event apart from the IO_STATUS_BLOCK it passes as the overlapped words: that event stands in for the one an
// OVERLAPPED would hold.
void finish_io(KernelContext &context, const std::shared_ptr<CompletionPort> &port, uint64_t key, uint64_t overlapped, size_t width, uint32_t error, uint64_t bytes,
bool report = true, uint64_t event = UINT64_MAX)
{
put_word(context.process, overlapped, status_of(error), width);
put_word(context.process, overlapped + width, bytes, width);
const uint64_t handle = event != UINT64_MAX ? event : get_word(context.process, overlapped + 2 * width + 8, width);
if (handle & ~uint64_t(1))
context.objects.set_event(handle & ~uint64_t(1), true);
// A handle with its low bit set asks for no completion packet.
if (report && port && !(handle & 1))
port->post({bytes, key, overlapped, error});
}
// One try at a transfer. 0: finished, with the error and the byte count; 1: it has to wait.
struct Transfer {
bool reading = false;
uint64_t buffer = 0;
uint32_t size = 0;
std::vector<uint8_t> outgoing;
size_t sent = 0;
};
int attempt(KernelContext &context, PipeObject &pipe, Transfer &transfer, uint32_t &error, uint64_t &bytes)
{
error = 0;
bytes = 0;
if (pipe.descriptor < 0) {
error = pipe.server ? error_pipe_listening : error_not_connected;
return 0;
}
if (transfer.reading) {
if (pipe.message_mode) {
std::vector<uint8_t> message = std::move(pipe.carry);
pipe.carry.clear();
if (message.empty()) {
message.resize(1 << 16);
const ssize_t count = ::recv(pipe.descriptor, message.data(), message.size(), MSG_DONTWAIT);
if (count < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
return 1;
error = error_broken_pipe;
return 0;
}
if (count == 0) {
error = error_broken_pipe;
return 0;
}
message.resize(size_t(count));
}
const size_t part = std::min<size_t>(message.size(), transfer.size);
if (part)
context.process.write_memory(transfer.buffer, std::span<const uint8_t>(message.data(), part));
bytes = part;
if (part < message.size()) {
pipe.carry.assign(message.begin() + ptrdiff_t(part), message.end());
// Only a handle that reads messages hears that one did not fit: in byte mode the rest is simply the next bytes.
if (pipe.read_messages)
error = error_more_data;
}
return 0;
}
std::vector<uint8_t> data(std::min<size_t>(transfer.size, 1 << 20));
const ssize_t count = ::recv(pipe.descriptor, data.data(), data.size(), MSG_DONTWAIT);
if (count < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
return 1;
error = error_broken_pipe;
return 0;
}
if (count == 0 && !data.empty()) {
error = error_broken_pipe;
return 0;
}
if (count > 0)
context.process.write_memory(transfer.buffer, std::span<const uint8_t>(data.data(), size_t(count)));
bytes = uint64_t(count);
return 0;
}
while (transfer.sent < transfer.outgoing.size()) {
const ssize_t count = ::send(pipe.descriptor, transfer.outgoing.data() + transfer.sent, transfer.outgoing.size() - transfer.sent, MSG_DONTWAIT | MSG_NOSIGNAL);
if (count < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
return 1;
error = error_no_data;
bytes = transfer.sent;
return 0;
}
transfer.sent += size_t(count);
if (pipe.message_mode)
break;
}
bytes = transfer.sent;
return 0;
}
// Waits until the descriptor can take or give data; other guest threads run meanwhile.
void wait_for(KernelContext &context, const PipeObject &pipe, bool reading)
{
HostWait waiting;
context.threads.block([&pipe, reading] {
if (pipe.descriptor < 0)
return true;
pollfd entry{pipe.descriptor, short(reading ? POLLIN : POLLOUT), 0};
return ::poll(&entry, 1, 0) != 0;
}, std::nullopt);
}
// A transfer for the handlers that read or write on their own: it waits for the pipe and ends with the error and the bytes.
uint32_t blocking(KernelContext &context, PipeObject &pipe, Transfer &transfer, uint64_t &bytes)
{
for (;;) {
uint32_t error = 0;
if (attempt(context, pipe, transfer, error, bytes) == 0)
return error;
wait_for(context, pipe, transfer.reading);
}
}
// The transfer behind ReadFile and WriteFile for overlapped handles: it runs at once when it can and finishes later otherwise.
// How a transfer on an overlapped pipe started: finished at once, with an error or none and its count, or pending until the reactor
// finishes it into the overlapped words.
struct Started {
bool pending = false;
uint32_t error = 0;
uint64_t bytes = 0;
};
Started start_overlapped(KernelContext &context, size_t width, const std::shared_ptr<PipeObject> &pipe, Transfer transfer, uint64_t overlapped,
uint64_t event = UINT64_MAX)
{
put_word(context.process, overlapped, status_pending, width);
put_word(context.process, overlapped + width, 0, width);
const bool native = event != UINT64_MAX;
// Starting a transfer clears its event, so the event tells of this transfer and not of an earlier one.
if (const uint64_t handle = native ? event : get_word(context.process, overlapped + 2 * width + 8, width); handle & ~uint64_t(1))
context.objects.set_event(handle & ~uint64_t(1), false);
uint32_t error = 0;
uint64_t bytes = 0;
if (attempt(context, *pipe, transfer, error, bytes) == 0) {
// A failure found at the start is reported by the call; only a transfer that ran is queued as completed.
if (error && error != error_more_data) {
put_word(context.process, overlapped, status_of(error), width);
return {false, error, 0};
}
finish_io(context, pipe->port, pipe->key, overlapped, width, error, bytes, !native && !pipe->skip_port_on_success, event);
return {false, error, bytes};
}
auto io = std::make_shared<PendingIo>();
io->pipe = pipe;
io->overlapped = overlapped;
context.pipes->pending.push_back(io);
auto shared = std::make_shared<Transfer>(std::move(transfer));
const auto port = pipe->port;
const auto key = pipe->key;
auto &state = *context.pipes;
context.reactor->add([&context, &state, io, shared, port, key, width, overlapped, native, event]() {
const auto pipe = io->pipe.lock();
uint32_t error = 0;
uint64_t bytes = 0;
bool finished = false;
if (io->cancelled || !pipe) {
error = error_aborted;
finished = true;
} else {
if (ensure_pipe_connected(*pipe) || pipe->connected)
finished = attempt(context, *pipe, *shared, error, bytes) == 0;
}
if (!finished)
return false;
state.pending.erase(std::remove(state.pending.begin(), state.pending.end(), io), state.pending.end());
finish_io(context, pipe ? pipe->port : port, pipe ? pipe->key : key, overlapped, width, error, bytes, !native, event);
return true;
}, pipe->descriptor, short(shared->reading ? POLLIN : POLLOUT));
return {true, 0, 0};
}
std::shared_ptr<PipeObject> make_client(int descriptor, uint32_t access, uint32_t flags, int type, const std::u16string &name)
{
auto pipe = std::make_shared<PipeObject>(descriptor, access, flags);
pipe->connected = true;
// A client starts in byte read mode whatever the pipe's type, until SetNamedPipeHandleState says otherwise.
pipe->message_mode = type == SOCK_SEQPACKET;
pipe->name = name;
return pipe;
}
// The pipe's socket to a server, or the Windows error that Connect would have reported.
int connect_to(const std::u16string &name, int &type, uint32_t &error)
{
sockaddr_un storage;
const auto length = abstract_address(pipe_address(name), storage);
for (const int candidate : {SOCK_STREAM, SOCK_SEQPACKET}) {
const int descriptor = ::socket(AF_UNIX, candidate | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (descriptor < 0) {
error = error_not_enough_memory;
return -1;
}
if (::connect(descriptor, reinterpret_cast<sockaddr *>(&storage), length) == 0) {
type = candidate;
return descriptor;
}
const int cause = errno;
::close(descriptor);
if (cause == EAGAIN) {
error = error_pipe_busy;
return -1;
}
// A listener of the other socket type refuses like a name nobody serves, so the second type is tried too.
}
error = error_file_not_found;
return -1;
}
// Whether any process of the prefix serves the pipe; the abstract names of the host sockets are listed in /proc/net/unix.
bool pipe_exists(KernelContext &context, const std::u16string &name)
{
const auto address = pipe_address(name);
if (const auto found = context.pipes->listeners.find(address); found != context.pipes->listeners.end() && !found->second.expired())
return true;
std::ifstream list("/proc/net/unix");
std::string line;
const auto wanted = "@" + address;
while (std::getline(list, line))
if (line.size() >= wanted.size() && line.compare(line.size() - wanted.size(), wanted.size(), wanted) == 0)
return true;
return false;
}
}
void complete_file_io(KernelContext &context, FileObject &file, uint64_t overlapped, size_t width, uint32_t error, uint64_t bytes)
{
finish_io(context, file.port, file.key, overlapped, width, error, bytes, !(file.skip_port_on_success && !error));
}
PipeObject::~PipeObject()
{
if (listener && listener->instances)
--listener->instances;
}
std::optional<std::u16string> pipe_name_of(std::u16string_view path)
{
for (const std::u16string_view prefix : {u"\\\\.\\pipe\\", u"\\\\?\\pipe\\", u"\\??\\pipe\\"}) {
if (path.size() > prefix.size() && equal_fold(path.substr(0, prefix.size()), prefix))
return std::u16string(path.substr(prefix.size()));
}
return std::nullopt;
}
bool ensure_pipe_connected(PipeObject &pipe)
{
if (pipe.connected)
return true;
if (!pipe.server || !pipe.listener)
return false;
const int descriptor = ::accept4(pipe.listener->descriptor, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (descriptor < 0)
return false;
pipe.descriptor = descriptor;
pipe.connected = true;
return true;
}
bool open_pipe_client(KernelContext &context, ApiCall &call, std::u16string_view path, uint32_t access, uint32_t flags)
{
const auto name = pipe_name_of(path);
if (!name)
return false;
call.ret(invalid_handle);
int type = SOCK_STREAM;
uint32_t error = 0;
const int descriptor = connect_to(*name, type, error);
if (descriptor < 0) {
call.set_last_error(error);
return true;
}
auto pipe = make_client(descriptor, access, flags, type, *name);
pipe->inherit = inheritable_handle(call, optional_argument(call, 3));
const auto made = context.objects.add_object(pipe);
if (made.status) {
call.set_last_error(error_not_enough_memory);
return true;
}
if (pipe->inherit)
context.objects.set_inherit(made.value, true);
call.set_last_error(0);
call.ret(made.value);
return true;
}
bool transfer_pipe(KernelContext &context, ApiCall &call, bool reading)
{
const auto pipe = context.objects.object_as<PipeObject>(call[0]);
if (!pipe)
return false;
if (reading ? !pipe->readable() : !pipe->writable()) {
call.fail(error_access_denied);
return true;
}
ensure_pipe_connected(*pipe);
if (!pipe->connected) {
call.fail(pipe->server ? error_pipe_listening : error_not_connected);
return true;
}
const uint64_t counted = call[3], overlapped = call[4];
const bool asynchronous = overlapped && pipe->overlapped();
if (!asynchronous && !counted) {
call.fail(error_invalid_parameter);
return true;
}
Transfer transfer;
transfer.reading = reading;
transfer.buffer = call[1];
transfer.size = call.u32(2);
if (!reading)
transfer.outgoing = call.read(call[1], transfer.size);
if (asynchronous) {
const auto started = start_overlapped(context, call.pointer_width(), pipe, std::move(transfer), overlapped);
if (started.pending) {
call.fail(error_io_pending);
return true;
}
if (counted && (!started.error || started.error == error_more_data))
call.store<uint32_t>(counted, uint32_t(started.bytes));
if (started.error)
call.fail(started.error);
else
call.ret(1);
return true;
}
if (counted)
call.store<uint32_t>(counted, 0);
// A write of nothing ends at once; a read of nothing waits for data like any other read.
uint64_t bytes = 0;
const uint32_t error = blocking(context, *pipe, transfer, bytes);
if (counted)
call.store<uint32_t>(counted, uint32_t(bytes));
if (error) {
call.fail(error);
return true;
}
call.ret(1);
return true;
}
bool native_pipe_transfer(KernelContext &context, ApiCall &call, bool reading)
{
const auto pipe = context.objects.object_as<PipeObject>(call[0]);
if (!pipe)
return false;
const size_t width = call.pointer_width();
const uint64_t event = call[1], status_block = call[4];
if (!status_block) {
call.ret(0xc000000dU);
return true;
}
if (reading ? !pipe->readable() : !pipe->writable()) {
call.ret(0xc0000022U);
return true;
}
ensure_pipe_connected(*pipe);
if (!pipe->connected) {
call.ret(status_of(pipe->server ? error_pipe_listening : error_not_connected));
return true;
}
Transfer transfer;
transfer.reading = reading;
transfer.buffer = call[5];
transfer.size = call.u32(6);
if (!reading)
transfer.outgoing = call.read(call[5], transfer.size);
if (event)
context.objects.set_event(event, false);
if (pipe->overlapped()) {
const auto started = start_overlapped(context, width, pipe, std::move(transfer), status_block, event);
call.ret(started.pending ? status_pending : status_of(started.error));
return true;
}
uint64_t bytes = 0;
const uint32_t error = blocking(context, *pipe, transfer, bytes);
put_word(context.process, status_block, status_of(error), width);
put_word(context.process, status_block + width, bytes, width);
if (event)
context.objects.set_event(event, true);
call.ret(status_of(error));
return true;
}
void register_pipe_api(ApiRegistry &registry, KernelContext &context)
{
auto &objects = context.objects;
const auto pipe_of = [&objects](uint64_t handle) { return objects.object_as<PipeObject>(handle); };
registry.add("CreateNamedPipeW", 8, [&context, &objects](ApiCall &call) {
const auto name = pipe_name_of(call.wide(call[0], 260));
const uint32_t mode = call.u32(1), type = call.u32(2), limit = call.u32(3);
call.ret(invalid_handle);
if (!name || name->empty() || !(mode & 3) || (limit == 0 || (limit > 255 && limit != UINT32_MAX))) {
call.set_last_error(error_invalid_parameter);
return;
}
const bool message = type & pipe_type_message;
const auto address = pipe_address(*name);
std::shared_ptr<PipeListener> listener;
if (const auto found = context.pipes->listeners.find(address); found != context.pipes->listeners.end())
listener = found->second.lock();
if (listener) {
// More instances of a pipe the process already serves.
if (mode & file_flag_first_pipe_instance) {
call.set_last_error(error_access_denied);
return;
}
if (listener->instances >= listener->maximum || (listener->type == SOCK_SEQPACKET) != message) {
call.set_last_error(error_pipe_busy);
return;
}
} else {
listener = std::make_shared<PipeListener>();
listener->type = message ? SOCK_SEQPACKET : SOCK_STREAM;
listener->maximum = limit == UINT32_MAX ? 255 : limit;
listener->address = address;
listener->descriptor = ::socket(AF_UNIX, listener->type | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
sockaddr_un storage;
const auto length = abstract_address(address, storage);
if (listener->descriptor < 0 || ::bind(listener->descriptor, reinterpret_cast<sockaddr *>(&storage), length) || ::listen(listener->descriptor, 128)) {
// A pipe of that name that another process serves has a first instance already.
call.set_last_error(errno == EADDRINUSE ? ((mode & file_flag_first_pipe_instance) ? error_access_denied : error_pipe_busy) : error_not_enough_memory);
return;
}
context.pipes->listeners[address] = listener;
}
const uint32_t access = ((mode & 1) ? 0x80000000U : 0) | ((mode & 2) ? 0x40000000U : 0);
auto pipe = std::make_shared<PipeObject>(-1, access, mode & 0x40000000U);
pipe->server = true;
pipe->listener = listener;
pipe->message_mode = message;
pipe->read_messages = message && (type & 2);
pipe->name = *name;
pipe->inherit = inheritable_handle(call, call[7]);
++listener->instances;
const auto made = objects.add_object(pipe);
if (made.status) {
call.set_last_error(error_not_enough_memory);
return;
}
if (pipe->inherit)
objects.set_inherit(made.value, true);
call.set_last_error(0);
call.ret(made.value);
});
registry.add("ConnectNamedPipe", 2, [&context, pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe || !pipe->server) {
call.fail(pipe ? error_invalid_function : 6);
return;
}
const size_t width = call.pointer_width();
const uint64_t overlapped = call[1];
const bool asynchronous = overlapped && pipe->overlapped();
if (pipe->connected || ensure_pipe_connected(*pipe)) {
// A client that came first is reported as a connection that exists; the event tells an overlapped caller all the same.
if (asynchronous) {
put_word(context.process, overlapped, 0, width);
put_word(context.process, overlapped + width, 0, width);
if (const uint64_t handle = get_word(context.process, overlapped + 2 * width + 8, width); handle & ~uint64_t(1))
context.objects.set_event(handle & ~uint64_t(1), true);
}
call.fail(error_pipe_connected);
return;
}
if (!asynchronous) {
// A handle without overlapped I/O waits for a client.
HostWait waiting;
context.threads.block([pipe] { return ensure_pipe_connected(*pipe); }, std::nullopt);
call.ret(1);
return;
}
put_word(context.process, overlapped, status_pending, width);
put_word(context.process, overlapped + width, 0, width);
auto io = std::make_shared<PendingIo>();
io->pipe = pipe;
io->overlapped = overlapped;
context.pipes->pending.push_back(io);
const auto port = pipe->port;
const auto key = pipe->key;
auto &state = *context.pipes;
context.reactor->add([&context, &state, io, port, key, width, overlapped]() {
const auto pipe = io->pipe.lock();
uint32_t error = 0;
if (io->cancelled || !pipe)
error = error_aborted;
else if (!ensure_pipe_connected(*pipe))
return false;
state.pending.erase(std::remove(state.pending.begin(), state.pending.end(), io), state.pending.end());
// The port is the one bound when the operation ends: a program may bind it after starting the wait.
finish_io(context, pipe ? pipe->port : port, pipe ? pipe->key : key, overlapped, width, error, 0);
return true;
}, pipe->listener ? pipe->listener->descriptor : -1, short(POLLIN));
call.fail(error_io_pending);
});
registry.add("DisconnectNamedPipe", 1, [pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe || !pipe->server) {
call.fail(pipe ? error_invalid_function : 6);
return;
}
if (pipe->descriptor >= 0)
::close(pipe->descriptor);
pipe->descriptor = -1;
pipe->connected = false;
pipe->carry.clear();
call.ret(1);
});
registry.add("WaitNamedPipeW", 2, [&context](ApiCall &call) {
const auto name = pipe_name_of(call.wide(call[0], 260));
if (!name) {
call.fail(error_invalid_parameter);
return;
}
if (!pipe_exists(context, *name)) {
call.fail(error_file_not_found);
return;
}
call.ret(1);
});
registry.add("SetNamedPipeHandleState", 4, [pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe) {
call.fail(6);
return;
}
if (call[1])
pipe->read_messages = pipe->message_mode && (call.load<uint32_t>(call[1]) & 2);
call.ret(1);
});
registry.add("GetNamedPipeInfo", 5, [pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe) {
call.fail(6);
return;
}
if (call[1])
call.store<uint32_t>(call[1], (pipe->server ? 1U : 0U) | (pipe->message_mode ? 4U : 0U));
if (call[2])
call.store<uint32_t>(call[2], 65536);
if (call[3])
call.store<uint32_t>(call[3], 65536);
if (call[4])
call.store<uint32_t>(call[4], pipe->listener ? pipe->listener->maximum : 255);
call.ret(1);
});
registry.add("GetNamedPipeHandleState", 7, [pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe) {
call.fail(6);
return;
}
if (call[1])
call.store<uint32_t>(call[1], pipe->read_messages ? 2 : 0);
if (call[2])
call.store<uint32_t>(call[2], pipe->listener ? pipe->listener->instances : 1);
if (call[3])
call.store<uint32_t>(call[3], 0);
if (call[4])
call.store<uint32_t>(call[4], 0);
call.ret(1);
});
const auto peer_process = [pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe || !call[1]) {
call.fail(pipe ? error_invalid_parameter : 6);
return;
}
ucred credentials{};
socklen_t length = sizeof(credentials);
if (pipe->descriptor < 0 || ::getsockopt(pipe->descriptor, SOL_SOCKET, SO_PEERCRED, &credentials, &length)) {
call.fail(error_not_connected);
return;
}
call.store<uint32_t>(call[1], uint32_t(credentials.pid));
call.ret(1);
};
registry.add("GetNamedPipeClientProcessId", 2, peer_process);
registry.add("GetNamedPipeServerProcessId", 2, peer_process);
// Every client of a Raven pipe runs as the same user, so the server thread takes the identity of its own logon at the impersonation
// level a client grants unless it asks otherwise.
registry.add("ImpersonateNamedPipeClient", 1, [&context, pipe_of](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe || !pipe->server) {
call.fail(6);
return;
}
ensure_pipe_connected(*pipe);
if (!pipe->connected) {
call.fail(1368);
return;
}
auto token = std::make_shared<AccessToken>(*context.process_token);
token->set_type(2);
token->set_impersonation_level(2);
context.threads.current().impersonation = std::move(token);
call.ret(1);
});
// A transaction writes a request and reads the answer in one call.
const auto transact = [&context](ApiCall &call, const std::shared_ptr<PipeObject> &pipe, uint64_t request, uint32_t request_size, uint64_t answer, uint32_t answer_size, uint64_t counted) {
ensure_pipe_connected(*pipe);
if (!pipe->connected) {
call.fail(pipe->server ? error_pipe_listening : error_not_connected);
return;
}
Transfer out;
out.outgoing = call.read(request, request_size);
uint64_t bytes = 0;
if (const uint32_t error = blocking(context, *pipe, out, bytes)) {
call.fail(error);
return;
}
Transfer in;
in.reading = true;
in.buffer = answer;
in.size = answer_size;
const uint32_t error = blocking(context, *pipe, in, bytes);
if (counted)
call.store<uint32_t>(counted, uint32_t(bytes));
if (error) {
call.fail(error);
return;
}
call.ret(1);
};
registry.add("TransactNamedPipe", 7, [pipe_of, transact](ApiCall &call) {
const auto pipe = pipe_of(call[0]);
if (!pipe || call[6]) {
call.fail(pipe ? error_invalid_function : 6);
return;
}
transact(call, pipe, call[1], call.u32(2), call[3], call.u32(4), call[5]);
});
registry.add("CallNamedPipeW", 7, [&context, transact](ApiCall &call) {
const auto name = pipe_name_of(call.wide(call[0], 260));
if (!name) {
call.fail(error_invalid_parameter);
return;
}
int type = SOCK_STREAM;
uint32_t error = 0;
const int descriptor = connect_to(*name, type, error);
if (descriptor < 0) {
call.fail(error);
return;
}
const auto pipe = make_client(descriptor, 0xc0000000U, 0, type, *name);
transact(call, pipe, call[1], call.u32(2), call[3], call.u32(4), call[5]);
});
// The result of an overlapped operation is in the OVERLAPPED structure the program passed to start it.
const auto overlapped_result = [&context](ApiCall &call) {
const uint64_t overlapped = call[1];
if (!overlapped) {
call.fail(error_invalid_parameter);
return;
}
const size_t width = call.pointer_width();
auto status = get_word(context.process, overlapped, width);
if (uint32_t(status) == status_pending) {
if (!call.u32(3)) {
call.fail(error_io_incomplete);
return;
}
HostWait waiting;
context.threads.block([&] {
context.reactor->poll();
return uint32_t(get_word(context.process, overlapped, width)) != status_pending;
}, std::nullopt);
status = get_word(context.process, overlapped, width);
}
if (call[2])
call.store<uint32_t>(call[2], uint32_t(get_word(context.process, overlapped + width, width)));
if (const uint32_t error = error_of_status(status)) {
call.fail(error);
return;
}
call.ret(1);
};
registry.add("GetOverlappedResult", 4, overlapped_result);
registry.add("GetOverlappedResultEx", 5, [&context, overlapped_result](ApiCall &call) {
// The wait of the extended call is bounded; an operation that is still running when it ends reports a timeout.
const uint32_t milliseconds = call.u32(3);
const auto status = get_word(context.process, call[1], call.pointer_width());
if (call[1] && uint32_t(status) == status_pending && milliseconds != UINT32_MAX) {
const auto wake = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds);
HostWait waiting;
context.threads.block([&] {
context.reactor->poll();
return uint32_t(get_word(context.process, call[1], call.pointer_width())) != status_pending;
}, wake);
if (uint32_t(get_word(context.process, call[1], call.pointer_width())) == status_pending) {
call.fail(258);
return;
}
}
overlapped_result(call);
});
// Cancelling ends the operations still waiting on the handle, as aborted; the completions are delivered at once.
const auto cancel = [&context, pipe_of](ApiCall &call, uint64_t handle, uint64_t overlapped, bool required) {
const auto pipe = pipe_of(handle);
if (!pipe) {
call.fail(6);
return;
}
bool found = false;
for (const auto &io : context.pipes->pending)
if (io->pipe.lock() == pipe && (!overlapped || io->overlapped == overlapped)) {
io->cancelled = true;
found = true;
}
context.reactor->poll();
if (!found && required) {
call.fail(error_not_found);
return;
}
call.ret(1);
};
// The flags are a UCHAR: only the low byte of the argument word is the caller's.
registry.add("SetFileCompletionNotificationModes", 2, [&context](ApiCall &call) {
const uint32_t modes = uint8_t(call[1]);
if (!(modes & ~3U) && set_socket_completion_modes(context.objects, call[0], modes)) {
call.ret(1);
return;
}
const auto file = context.objects.object_as<FileObject>(call[0]);
if (!file || (modes & ~3U)) {
call.fail(file ? error_invalid_parameter : 6);
return;
}
file->skip_port_on_success = modes & 1;
file->skip_event_on_success = modes & 2;
call.ret(1);
});
registry.add("CancelIoEx", 2, [cancel](ApiCall &call) { cancel(call, call[0], call[1], true); });
// The native cancels: every transfer the handle has pending, or the one whose IO_STATUS_BLOCK is named; the outcome as an NTSTATUS.
const auto native_cancel = [&context, pipe_of](ApiCall &call, uint64_t handle, uint64_t status_block, uint64_t outcome) {
const auto pipe = pipe_of(handle);
if (!pipe) {
// An AFD endpoint's polls end as cancelled, with their completions.
if (const auto found = afd_cancel(context, handle, status_block)) {
if (outcome) {
put_word(context.process, outcome, 0, call.pointer_width());
put_word(context.process, outcome + call.pointer_width(), 0, call.pointer_width());
}
call.ret(*found || !status_block ? 0 : 0xc0000225U);
return;
}
call.ret(0xc0000008U);
return;
}
bool found = false;
for (const auto &io : context.pipes->pending)
if (io->pipe.lock() == pipe && (!status_block || io->overlapped == status_block)) {
io->cancelled = true;
found = true;
}
context.reactor->poll();
if (outcome) {
put_word(context.process, outcome, 0, call.pointer_width());
put_word(context.process, outcome + call.pointer_width(), 0, call.pointer_width());
}
call.ret(found || !status_block ? 0 : 0xc0000225U);
};
registry.add("NtCancelIoFile", 2, [native_cancel](ApiCall &call) { native_cancel(call, call[0], 0, call[1]); });
registry.add("NtCancelIoFileEx", 3, [native_cancel](ApiCall &call) { native_cancel(call, call[0], call[1], call[2]); });
// FSCTL_PIPE_LISTEN, ConnectNamedPipe as the native call makes it: the event and the IO_STATUS_BLOCK report a client that arrives.
registry.add("NtFsControlFile", 10, [&context, pipe_of](ApiCall &call) {
constexpr uint32_t fsctl_pipe_listen = 0x110008, status_invalid_device_request = 0xc0000010U, status_pipe_connected = 0xc00000b2U;
const auto pipe = pipe_of(call[0]);
const size_t width = call.pointer_width();
const uint64_t event = call[1], status_block = call[4];
if (!pipe || !pipe->server || call.u32(5) != fsctl_pipe_listen || !status_block) {
call.ret(status_invalid_device_request);
return;
}
if (event)
context.objects.set_event(event, false);
if (pipe->connected || ensure_pipe_connected(*pipe)) {
put_word(context.process, status_block, status_pipe_connected, width);
put_word(context.process, status_block + width, 0, width);
call.ret(status_pipe_connected);
return;
}
if (!pipe->overlapped()) {
HostWait waiting;
context.threads.block([pipe] { return ensure_pipe_connected(*pipe); }, std::nullopt);
put_word(context.process, status_block, 0, width);
put_word(context.process, status_block + width, 0, width);
if (event)
context.objects.set_event(event, true);
call.ret(0);
return;
}
put_word(context.process, status_block, status_pending, width);
put_word(context.process, status_block + width, 0, width);
auto io = std::make_shared<PendingIo>();
io->pipe = pipe;
io->overlapped = status_block;
context.pipes->pending.push_back(io);
auto &state = *context.pipes;
context.reactor->add([&context, &state, io, width, status_block, event]() {
const auto pipe = io->pipe.lock();
uint32_t error = 0;
if (io->cancelled || !pipe)
error = error_aborted;
else if (!ensure_pipe_connected(*pipe))
return false;
state.pending.erase(std::remove(state.pending.begin(), state.pending.end(), io), state.pending.end());
finish_io(context, nullptr, 0, status_block, width, error, 0, false, event);
return true;
}, pipe->listener ? pipe->listener->descriptor : -1, short(POLLIN));
call.ret(status_pending);
});
registry.add("CancelIo", 1, [cancel](ApiCall &call) { cancel(call, call[0], 0, false); });
}
}