From 652af318dbb32c4f3d38d89ae7bee93d4c373f92 Mon Sep 17 00:00:00 2001 From: stasoid Date: Fri, 31 Jan 2025 10:57:34 +0500 Subject: [PATCH] LibIPC: Port to Windows The Linux IPC uses SCM_RIGHTS to transfer fds to another process (see TransportSocket::transfer, which calls LocalSocket::send_message). File descriptors are handled separately from regular data. On Windows handles are embedded in regular data. They are duplicated in the sender process. Socket handles need special code both on sender side (because they require using WSADuplicateSocket instead of DuplicateHandle, see TransportSocketWindows::duplicate_handles) and on receiver side (because they require WSASocket, see FileWindows.cpp). TransportSocketWindows::ReadResult::fds vector is always empty, it is kept the same as Linux version to avoid OS #ifdefs in Connection.h/.cpp and Web::HTML::MessagePort::read_from_transport. Separate handling of fds permeates all IPC code, it doesn't make sense to #ifdef out all this code on Windows. In other words, the Linux code is more generic - it handles both regular data and fds. On Windows, we need only the regular data portion of it, and we just use that. Duplicating handles on Windows requires pid of target (receiver) process (see TransportSocketWindows::m_peer_pid). This pid is received during special TransportSocketWindows initialization, which is performed only on Windows. It is handled in a separate PR #3179. Note: ChatGPT and [stackoverflow](https://stackoverflow.com/questions/25429887/getting-pid-of-peer-socket-on-windows) suggest using GetExtendedTcpTable/GetTcpTable2 to get peer pid, but this doesn't work because [MIB_TCPROW2::dwOwningPid](https://learn.microsoft.com/en-us/windows/win32/api/tcpmib/ns-tcpmib-mib_tcprow2) is "The PID of the process that issued a context bind for this TCP connection.", so for both ends it will return the pid of the process that called socketpair. Co-Authored-By: Andrew Kaster --- Libraries/LibIPC/CMakeLists.txt | 11 +- Libraries/LibIPC/Connection.h | 2 +- Libraries/LibIPC/Decoder.cpp | 12 -- Libraries/LibIPC/File.cpp | 36 ++++ Libraries/LibIPC/File.h | 11 +- Libraries/LibIPC/FileWindows.cpp | 41 +++++ Libraries/LibIPC/HandleType.h | 18 ++ Libraries/LibIPC/Message.h | 9 +- Libraries/LibIPC/MessageWindows.cpp | 74 ++++++++ Libraries/LibIPC/Transport.h | 4 +- Libraries/LibIPC/TransportSocketWindows.cpp | 177 ++++++++++++++++++++ Libraries/LibIPC/TransportSocketWindows.h | 50 ++++++ 12 files changed, 416 insertions(+), 29 deletions(-) create mode 100644 Libraries/LibIPC/File.cpp create mode 100644 Libraries/LibIPC/FileWindows.cpp create mode 100644 Libraries/LibIPC/HandleType.h create mode 100644 Libraries/LibIPC/MessageWindows.cpp create mode 100644 Libraries/LibIPC/TransportSocketWindows.cpp create mode 100644 Libraries/LibIPC/TransportSocketWindows.h diff --git a/Libraries/LibIPC/CMakeLists.txt b/Libraries/LibIPC/CMakeLists.txt index 114d0c482b..9383718df8 100644 --- a/Libraries/LibIPC/CMakeLists.txt +++ b/Libraries/LibIPC/CMakeLists.txt @@ -2,11 +2,18 @@ set(SOURCES Connection.cpp Decoder.cpp Encoder.cpp - Message.cpp ) if (UNIX) - list(APPEND SOURCES TransportSocket.cpp) + list(APPEND SOURCES + File.cpp + Message.cpp + TransportSocket.cpp) +else() + list(APPEND SOURCES + FileWindows.cpp + MessageWindows.cpp + TransportSocketWindows.cpp) endif() serenity_lib(LibIPC ipc) diff --git a/Libraries/LibIPC/Connection.h b/Libraries/LibIPC/Connection.h index 229227c7c1..9bbf65b0c3 100644 --- a/Libraries/LibIPC/Connection.h +++ b/Libraries/LibIPC/Connection.h @@ -57,7 +57,7 @@ protected: RefPtr m_responsiveness_timer; Vector> m_unprocessed_messages; - Queue m_unprocessed_fds; + Queue m_unprocessed_fds; // unused on Windows ByteBuffer m_unprocessed_bytes; u32 m_local_endpoint_magic { 0 }; diff --git a/Libraries/LibIPC/Decoder.cpp b/Libraries/LibIPC/Decoder.cpp index 022e52661c..5334859783 100644 --- a/Libraries/LibIPC/Decoder.cpp +++ b/Libraries/LibIPC/Decoder.cpp @@ -14,7 +14,6 @@ #include #include #include -#include namespace IPC { @@ -120,17 +119,6 @@ ErrorOr decode(Decoder& decoder) return URL::Host { move(value) }; } -template<> -ErrorOr decode(Decoder& decoder) -{ - auto file = TRY(decoder.files().try_dequeue()); - auto fd = file.fd(); - - auto fd_flags = TRY(Core::System::fcntl(fd, F_GETFD)); - TRY(Core::System::fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC)); - return file; -} - template<> ErrorOr decode(Decoder&) { diff --git a/Libraries/LibIPC/File.cpp b/Libraries/LibIPC/File.cpp new file mode 100644 index 0000000000..148a9c7d84 --- /dev/null +++ b/Libraries/LibIPC/File.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020, Sergey Bugaev + * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace IPC { + +// FIXME: IPC::Files transferred over the wire are always set O_CLOEXEC during decoding. +// Perhaps we should add an option to IPC::File to allow the receiver to decide whether to +// make it O_CLOEXEC or not. Or an attribute in the .ipc file? +ErrorOr File::clear_close_on_exec() +{ + auto fd_flags = TRY(Core::System::fcntl(m_fd, F_GETFD)); + fd_flags &= ~FD_CLOEXEC; + TRY(Core::System::fcntl(m_fd, F_SETFD, fd_flags)); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + auto file = TRY(decoder.files().try_dequeue()); + auto fd = file.fd(); + + auto fd_flags = TRY(Core::System::fcntl(fd, F_GETFD)); + TRY(Core::System::fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC)); + return file; +} + +} diff --git a/Libraries/LibIPC/File.h b/Libraries/LibIPC/File.h index 5172f53a4d..77c5f3d86b 100644 --- a/Libraries/LibIPC/File.h +++ b/Libraries/LibIPC/File.h @@ -63,16 +63,7 @@ public: return exchange(m_fd, -1); } - // FIXME: IPC::Files transferred over the wire are always set O_CLOEXEC during decoding. - // Perhaps we should add an option to IPC::File to allow the receiver to decide whether to - // make it O_CLOEXEC or not. Or an attribute in the .ipc file? - ErrorOr clear_close_on_exec() - { - auto fd_flags = TRY(Core::System::fcntl(m_fd, F_GETFD)); - fd_flags &= ~FD_CLOEXEC; - TRY(Core::System::fcntl(m_fd, F_SETFD, fd_flags)); - return {}; - } + ErrorOr clear_close_on_exec(); private: explicit File(int fd) diff --git a/Libraries/LibIPC/FileWindows.cpp b/Libraries/LibIPC/FileWindows.cpp new file mode 100644 index 0000000000..2999e1f184 --- /dev/null +++ b/Libraries/LibIPC/FileWindows.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, stasoid + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +#include + +namespace IPC { + +ErrorOr File::clear_close_on_exec() +{ + if (!SetHandleInformation(to_handle(m_fd), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) + return Error::from_windows_error(); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + auto handle_type = TRY(decoder.decode()); + int handle = -1; + if (handle_type == HandleType::Generic) { + TRY(decoder.decode_into(handle)); + } else if (handle_type == HandleType::Socket) { + WSAPROTOCOL_INFO pi = {}; + TRY(decoder.decode_into({ reinterpret_cast(&pi), sizeof(pi) })); + handle = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, &pi, 0, WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT); + if (handle == -1) + return Error::from_windows_error(); + } else { + return Error::from_string_literal("Invalid handle type"); + } + return File::adopt_fd(handle); +} + +} diff --git a/Libraries/LibIPC/HandleType.h b/Libraries/LibIPC/HandleType.h new file mode 100644 index 0000000000..047d938fbe --- /dev/null +++ b/Libraries/LibIPC/HandleType.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025, stasoid + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace IPC { + +enum class HandleType : u8 { + Generic, + Socket +}; + +} diff --git a/Libraries/LibIPC/Message.h b/Libraries/LibIPC/Message.h index 413cb03dde..c0b9b39291 100644 --- a/Libraries/LibIPC/Message.h +++ b/Libraries/LibIPC/Message.h @@ -12,8 +12,8 @@ #include #include #include +#include #include -#include namespace IPC { @@ -27,7 +27,7 @@ public: ~AutoCloseFileDescriptor() { if (m_fd != -1) - close(m_fd); + (void)Core::System::close(m_fd); } int value() const { return m_fd; } @@ -45,11 +45,14 @@ public: ErrorOr append_file_descriptor(int fd); - ErrorOr transfer_message(Transport& socket); + ErrorOr transfer_message(Transport& transport); private: Vector m_data; Vector, 1> m_fds; +#ifdef AK_OS_WINDOWS + Vector m_handle_offsets; +#endif }; enum class ErrorCode : u32 { diff --git a/Libraries/LibIPC/MessageWindows.cpp b/Libraries/LibIPC/MessageWindows.cpp new file mode 100644 index 0000000000..e15126c2a9 --- /dev/null +++ b/Libraries/LibIPC/MessageWindows.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024, Tim Flynn + * Copyright (c) 2025, stasoid + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +#include + +namespace IPC { + +using MessageSizeType = u32; + +MessageBuffer::MessageBuffer() +{ + m_data.resize(sizeof(MessageSizeType)); +} + +ErrorOr MessageBuffer::extend_data_capacity(size_t capacity) +{ + TRY(m_data.try_ensure_capacity(m_data.size() + capacity)); + return {}; +} + +ErrorOr MessageBuffer::append_data(u8 const* values, size_t count) +{ + TRY(m_data.try_append(values, count)); + return {}; +} + +ErrorOr MessageBuffer::append_file_descriptor(int handle) +{ + TRY(m_fds.try_append(adopt_ref(*new AutoCloseFileDescriptor(handle)))); + TRY(m_handle_offsets.try_append(m_data.size())); + + if (Core::System::is_socket(handle)) { + auto type = HandleType::Socket; + TRY(m_data.try_append(to_underlying(type))); + + // The handle will be duplicated and WSAPROTOCOL_INFO will be filled later in TransportSocketWindows::transfer. + // It can't be duplicated here because it requires peer process pid, which only TransportSocketWindows knows about. + WSAPROTOCOL_INFO pi = {}; + static_assert(sizeof(pi) >= sizeof(int)); + ByteReader::store(reinterpret_cast(&pi), handle); + TRY(m_data.try_append(reinterpret_cast(&pi), sizeof(pi))); + } else { + auto type = HandleType::Generic; + TRY(m_data.try_append(to_underlying(type))); + // The handle will be overwritten by a duplicate handle later in TransportSocketWindows::transfer (for the same reason). + TRY(m_data.try_append(reinterpret_cast(&handle), sizeof(handle))); + } + return {}; +} + +ErrorOr MessageBuffer::transfer_message(Transport& transport) +{ + Checked checked_message_size { m_data.size() }; + checked_message_size -= sizeof(MessageSizeType); + + if (checked_message_size.has_overflow()) + return Error::from_string_literal("Message is too large for IPC encoding"); + + MessageSizeType const message_size = checked_message_size.value(); + m_data.span().overwrite(0, reinterpret_cast(&message_size), sizeof(message_size)); + + TRY(transport.transfer(m_data.span(), m_handle_offsets)); + return {}; +} + +} diff --git a/Libraries/LibIPC/Transport.h b/Libraries/LibIPC/Transport.h index fdc063fc05..9862d41849 100644 --- a/Libraries/LibIPC/Transport.h +++ b/Libraries/LibIPC/Transport.h @@ -10,6 +10,8 @@ #if !defined(AK_OS_WINDOWS) # include +#else +# include #endif namespace IPC { @@ -18,7 +20,7 @@ namespace IPC { // Unix Domain Sockets using Transport = TransportSocket; #else -# error "LibIPC Transport has not been ported to this platform" +using Transport = TransportSocketWindows; #endif } diff --git a/Libraries/LibIPC/TransportSocketWindows.cpp b/Libraries/LibIPC/TransportSocketWindows.cpp new file mode 100644 index 0000000000..e2c16a6dc2 --- /dev/null +++ b/Libraries/LibIPC/TransportSocketWindows.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * Copyright (c) 2025, stasoid + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +#include + +namespace IPC { + +TransportSocketWindows::TransportSocketWindows(NonnullOwnPtr socket) + : m_socket(move(socket)) +{ +} + +void TransportSocketWindows::set_peer_pid(int pid) +{ + m_peer_pid = pid; +} + +void TransportSocketWindows::set_up_read_hook(Function hook) +{ + VERIFY(m_socket->is_open()); + m_socket->on_ready_to_read = move(hook); +} + +bool TransportSocketWindows::is_open() const +{ + return m_socket->is_open(); +} + +void TransportSocketWindows::close() +{ + m_socket->close(); +} + +void TransportSocketWindows::wait_until_readable() +{ + auto readable = MUST(m_socket->can_read_without_blocking(-1)); + VERIFY(readable); +} + +ErrorOr TransportSocketWindows::duplicate_handles(Bytes bytes, Vector const& handle_offsets) +{ + if (handle_offsets.is_empty()) + return {}; + + if (m_peer_pid == -1) + return Error::from_string_literal("Transport is not initialized"); + + HANDLE peer_process_handle = OpenProcess(PROCESS_DUP_HANDLE, FALSE, m_peer_pid); + if (!peer_process_handle) + return Error::from_windows_error(); + ScopeGuard guard = [&] { CloseHandle(peer_process_handle); }; + + for (auto offset : handle_offsets) { + + auto span = bytes.slice(offset); + if (span.size() < sizeof(HandleType)) + return Error::from_string_literal("Not enough bytes"); + + UnderlyingType raw_type {}; + ByteReader::load(span.data(), raw_type); + auto type = static_cast(raw_type); + if (type != HandleType::Generic && type != HandleType::Socket) + return Error::from_string_literal("Invalid handle type"); + span = span.slice(sizeof(HandleType)); + + if (type == HandleType::Socket) { + if (span.size() < sizeof(WSAPROTOCOL_INFO)) + return Error::from_string_literal("Not enough bytes for socket handle"); + + // We stashed the bytes of this process's version of the handle at the offset location + int handle = -1; + ByteReader::load(span.data(), handle); + + auto* pi = reinterpret_cast(span.data()); + if (WSADuplicateSocket(handle, m_peer_pid, pi)) + return Error::from_windows_error(); + } else { + if (span.size() < sizeof(int)) + return Error::from_string_literal("Not enough bytes for generic handle"); + + int handle = -1; + ByteReader::load(span.data(), handle); + + HANDLE new_handle = INVALID_HANDLE_VALUE; + if (!DuplicateHandle(GetCurrentProcess(), to_handle(handle), peer_process_handle, &new_handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) + return Error::from_windows_error(); + + ByteReader::store(span.data(), to_fd(new_handle)); + } + } + + return {}; +} + +ErrorOr TransportSocketWindows::transfer(Bytes bytes_to_write, Vector const& handle_offsets) +{ + TRY(duplicate_handles(bytes_to_write, handle_offsets)); + + while (!bytes_to_write.is_empty()) { + + ErrorOr maybe_nwritten = m_socket->write_some(bytes_to_write); + + if (maybe_nwritten.is_error()) { + auto error = maybe_nwritten.release_error(); + if (error.code() != EWOULDBLOCK) + return error; + + struct pollfd pollfd = { + .fd = static_cast(m_socket->fd().value()), + .events = POLLOUT, + .revents = 0 + }; + + auto result = WSAPoll(&pollfd, 1, -1); + if (result == 1) + continue; + if (result == SOCKET_ERROR) + return Error::from_windows_error(); + VERIFY_NOT_REACHED(); + } + + bytes_to_write = bytes_to_write.slice(maybe_nwritten.value()); + } + return {}; +} + +TransportSocketWindows::ReadResult TransportSocketWindows::read_as_much_as_possible_without_blocking(Function schedule_shutdown) +{ + ReadResult result; + + while (is_open()) { + + u8 buffer[4096]; + auto maybe_bytes_read = m_socket->read_without_waiting({ buffer, sizeof(buffer) }); + + if (maybe_bytes_read.is_error()) { + auto error = maybe_bytes_read.release_error(); + if (error.code() == EWOULDBLOCK) + break; + if (error.code() == ECONNRESET) { + schedule_shutdown(); + break; + } + VERIFY_NOT_REACHED(); + } + + auto bytes_read = maybe_bytes_read.release_value(); + if (bytes_read.is_empty()) { + schedule_shutdown(); + break; + } + + result.bytes.append(bytes_read.data(), bytes_read.size()); + } + + return result; +} + +ErrorOr TransportSocketWindows::release_underlying_transport_for_transfer() +{ + return m_socket->release_fd(); +} + +ErrorOr TransportSocketWindows::clone_for_transfer() +{ + return IPC::File::clone_fd(m_socket->fd().value()); +} + +} diff --git a/Libraries/LibIPC/TransportSocketWindows.h b/Libraries/LibIPC/TransportSocketWindows.h new file mode 100644 index 0000000000..3c4102b759 --- /dev/null +++ b/Libraries/LibIPC/TransportSocketWindows.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * Copyright (c) 2025, stasoid + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace IPC { + +class TransportSocketWindows { + AK_MAKE_NONCOPYABLE(TransportSocketWindows); + AK_MAKE_DEFAULT_MOVABLE(TransportSocketWindows); + +public: + explicit TransportSocketWindows(NonnullOwnPtr socket); + + void set_peer_pid(int pid); + void set_up_read_hook(Function); + bool is_open() const; + void close(); + + void wait_until_readable(); + + ErrorOr transfer(Bytes, Vector const& handle_offsets); + + struct [[nodiscard]] ReadResult { + Vector bytes; + Vector fds; // always empty, present to avoid OS #ifdefs in Connection.cpp + }; + ReadResult read_as_much_as_possible_without_blocking(Function schedule_shutdown); + + // Obnoxious name to make it clear that this is a dangerous operation. + ErrorOr release_underlying_transport_for_transfer(); + + ErrorOr clone_for_transfer(); + +private: + ErrorOr duplicate_handles(Bytes, Vector const& handle_offsets); + +private: + NonnullOwnPtr m_socket; + int m_peer_pid = -1; +}; + +}