A MessagePort can be transferred while it already has local queued
state such as incoming messages drained from its transport,
outgoing messages posted before a transport exists, and a pending
shutdown to apply once the port is enabled.
Serialize and restore that state as part of transfer so it moves with
the port instead of being left behind on the old transport.
Also mark transports that are being transferred so shutdown of the old
endpoint during handoff is not reported as peer EOF. That shutdown is
part of moving the transport to the new owner, not peer disconnected.
Co-Authored-By: Alexander Kalenik <kalenik.aliaksandr@gmail.com>
Remove the 5-second send timeout from mach_msg() to align Mach port
transport behavior with the Unix domain socket transport, which blocks
indefinitely on send.
The timeout also made it impossible to attach a debugger to a child
process on macOS: if you didn't attach within the 5-second window, the
send would time out and the connection would be marked as EOF.
Move handle serialization and deserialization entirely into
TransportSocketWindows so that Windows can share the common Message and
File implementations with other platforms.
Move MachPortServer from LibWebView into LibIPC as MachBootstrapListener
and move the Mach message structs from MachMessageTypes.h into LibIPC.
These types are IPC infrastructure, not UI or platform concerns.
Consolidating them in LibIPC keeps the Mach bootstrap handshake
self-contained in a single library and removes LibWebView's dependency
on LibThreading.
Previously, the bootstrap handshake used a two-state machine
(WaitingForPorts / WaitingForReplyPort) to handle a race: the parent
registering transport ports and the child sending a bootstrap request
could arrive in either order, so whichever came first stored its half
and the second completed the handshake.
Eliminate the race by holding a mutex across spawn() and
register_child_transport(). Since the child cannot send a bootstrap
request before it exists, and the lock isn't released until its
transport is registered, handle_bootstrap_request() is guaranteed to
find the entry. This reduces the pending map to a simple pid-to-ports
lookup and collapses the two-variant state into two straightforward
branches: known child, or on-demand (non-child) caller like WebDriver.
Registering multiple Mach port names with the bootstrap server at
runtime is not how macOS expects it to be used — the bootstrap server
is meant for static services, and the only reason we used it originally
was so child processes could reach back to the UI process.
Remove bootstrap_transport_over_socket(), which had both sides register
dynamic names with the bootstrap server and exchange them over a socket.
Instead, WebDriver and BrowserProcess connections now go through
MachPortServer instances directly. When a non-child process contacts a
MachPortServer, the server creates a port pair on demand (detected via
sysctl ppid check) and returns the local half immediately. This keeps
bootstrap server usage limited to the one original case: child processes
looking up their parent's MachPortServer.
WebDriver Session now runs its own MachPortServer per session.
--webdriver-content-path becomes --webdriver-mach-server-name on macOS.
Spare WebContent launches are skipped when a WebDriver session is active
to avoid bootstrap races.
On macOS, use Mach port messaging instead of Unix domain sockets for
all IPC transport. This makes the transport capable of carrying Mach
port rights as message attachments, which is a prerequisite for sending
IOSurface handles over the main IPC channel (currently sent via a
separate out-of-band path). It also avoids the need for the FD
acknowledgement protocol that TransportSocket requires, since Mach port
right transfers are atomic in the kernel.
Three connection establishment patterns:
- Spawned helper processes (WebContent, RequestServer, etc.) use the
existing MachPortServer: the child sends its task port with a reply
port, and the parent responds with a pre-created port pair.
- Socket-bootstrapped connections (WebDriver, BrowserProcess) exchange
Mach port names over the socket, then drop the socket.
- Pre-created pairs for IPC tests and in-message transport transfer.
Attachment on macOS now wraps a MachPort instead of a file descriptor,
converting between the two via fileport_makeport()/fileport_makefd().
The LibIPC socket transport tests are disabled on macOS since they are
socket-specific.
The #pragma once was placed after the #include directives instead of
immediately after the copyright comment, inconsistent with every other
header file
Encode transfer-data attachments as raw IPC attachments instead of first
rewrapping them as IPC::File values.
This is preparatory refactoring for the upcoming Mach-port transport
introduction on macOS, where attachments should remain transport-native
rather than being normalized through file descriptors.
MultiServer was inherited from SerenityOS where it was used in many
places. Now that BrowserProcess is its only consumer, inline the
connection acceptance logic directly into BrowserProcess and remove
the abstraction.
TransportSocket uses its own pipe-based notification mechanism on the IO
thread, making LocalSocket's built-in Core::Notifier redundant. When the
socket reaches EOF, this notifier is disabled from the IO thread. Since
the QSocketNotifier lives on the main thread, the its destruction is
deferred. If the socket is closed before the deferred destruction runs,
Qt detects the invalid socket on the next poll and prints:
QSocketNotifier: Invalid socket 50 and type 'Read', disabling...
Fix this by disabling the redundant socket-level notifier upfront in the
TransportSocket constructor.
Previously, `create_paired()` returned two full Transport objects, and
callers would immediately call `from_transport()` on the remote side to
extract its underlying fd. This wasted resources: the remote
Transport's IO thread, wakeup pipes, and send queue were initialized
only to be torn down without ever sending or receiving a message.
Now `create_paired()` returns `{Transport, TransportHandle}` — the
remote side is born as a lightweight handle containing just the raw fd,
skipping all unnecessary initialization.
Also replace `release_underlying_transport_for_transfer()` (which
returned a raw int fd) with `release_for_transfer()` (which returns a
TransportHandle directly), hiding the socket implementation detail
from callers including MessagePort.
Replace IPC::File / AutoCloseFileDescriptor / MessageFileType in
the IPC message pipeline with a new IPC::Attachment class. This
wraps a file descriptor transferred alongside IPC messages, and
provides a clean extension point for platform-specific transport
mechanisms (e.g., Mach ports on macOS) that will be introduced later.
Replace clone_from_transport() (which dup()s the FD) with
from_transport() (which releases the FD) in the WebWorkerClient
call site. The UI process never uses the WebWorkerClient connection
after spawning — it only passes the transport to WebContent — so
releasing instead of cloning is safe and simpler.
This removes clone_from_transport() from TransportHandle, and
clone_for_transfer() from TransportSocket/TransportSocketWindows,
as they no longer have any callers.
Now that auxiliary service sockets are sent over IPC rather than passed
as command-line arguments, TransportHandle no longer needs to expose raw
file descriptors or manage close-on-exec flags. Remove fd() and
clear_close_on_exec(), and simplify the connect helpers accordingly.
Add IPC::TransportHandle as an abstraction for passing IPC
transports through .ipc messages. This replaces IPC::File at
all sites where a transport (not a generic file) is being
transferred between processes.
TransportHandle provides from_transport(),
clone_from_transport(), and create_transport() methods that
encapsulate the fd-to-socket-to-transport conversion in one
place. This is preparatory work for Mach port support on
macOS -- when that lands, only TransportHandle's internals
need to change while all .ipc definitions and call sites
remain untouched.
Consolidate the repeated socketpair + adopt + configure pattern from
4 call sites into a single Transport::create_paired() factory method.
This fixes inconsistent error handling and socket configuration across
call sites, and prepares for future mach port support on macOS.
We were allocating vectors on the heap and copying the message header's
and payload's bytes to it before passing them on to
`::enqueue_message()`.
Remove these allocations and just pass `ReadonlyBytes` views into the
message header and payload directly. On my machine, this reduces the
time spent on the send-side queuing path by 13% to 42%, depending on the
message size.
While we're processing received messages, we can end up with unprocessed
bytes after the last message. Instead of copying the data into a new
ByteBuffer, just move the bytes inside the existing buffer and resize
it.
This does mean that as long as there are unprocessed bytes after reading
incoming messages, the buffer does not shrink. But as soon as there's
nothing left, we clear this buffer again.
Change post_message(MessageBuffer) to post_message(MessageBuffer&)
to avoid copying the MessageBuffer onto the stack. MessageBuffer
contains a Vector<u8, 1024> with a 1024-byte inline buffer, so
passing by value was adding over 1 KiB to the stack frame of
handle_messages().
This reduces the handle_messages() stack frame from 1328 bytes to
224 bytes, which matters because handle_messages() sits near the
base of the call stack when GC runs its conservative stack scan
in response to IPC requests.
This commit stops using deprecated WSA functions. While the ANSI
versions are most likely not going anywhere, Windows is natively UTF-16
so it has to convert to ANSI internally. All the ANSI functions in
Winsock are marked as deprecated. The macro suppressing the warnings is
no longer defined.
Previously, IPC messages were decoded on the main thread:
1. I/O thread received raw bytes and file descriptors
2. I/O thread stored them in a queue and notified main thread
3. Main thread decoded bytes into Message objects
4. Main thread processed the messages
Now, decoding happens on the I/O thread:
1. I/O thread receives raw bytes and file descriptors
2. I/O thread decodes them using a configurable MessageDecoder
3. I/O thread calls MessageHandler which stores decoded messages
4. I/O thread signals condition variable (for sync waiters)
5. I/O thread wakes main event loop via deferred_invoke()
6. Main thread processes already-decoded messages
This is achieved by:
- Adding MessageDecoder and MessageHandler callbacks to TransportSocket
- Connection template sets up the decoder (tries both endpoints)
- ConnectionBase::initialize_messaging() sets up the handler
- Storing a WeakEventLoopReference to wake the main thread
- Using mutex + condition variable for thread-safe queue access
- Sync message waiting now uses the CV directly instead of polling
The raw message API (read_as_many_messages_as_possible_without_blocking)
is preserved for MessagePort which uses its own decoding logic.
This architecture prepares for future multi-thread dispatch where
different message types could be routed to different handler threads
(e.g., scrolling messages to a dedicated scroll thread).
Previously, when an IPC message failed to parse, we only logged
"Failed to parse IPC message" followed by a hex dump, making it
difficult to diagnose the actual cause.
Now we log the specific error from each endpoint's decode attempt,
making it much easier to identify issues like size limit violations
or invalid field values.
AnonymousBuffer is backed by shared memory, not heap allocation.
The MAX_DECODED_SIZE limit in decode_size() is meant to prevent OOM
from malicious peers claiming huge sizes that would cause heap
allocations, but this doesn't apply to AnonymousBuffer since the
memory is already allocated by the sender.
This fixes decoding of large animated images (e.g. 300 frames at
240x240) where the total bitmap data exceeds 64 MiB.
Also add upfront bounds checking for variant indices before entering
the recursive template, allowing us to convert the template's fallback
error into VERIFY_NOT_REACHED() since the index is now guaranteed to
be valid when we enter the recursive decode.
- VERIFY that IPC::File has a valid fd (>= 0) before encoding
- VERIFY that container sizes fit in u32 before encoding
These catch programming errors where we accidentally try to encode
invalid data. Encoding bugs should crash immediately so we catch them
during development.
Add VERIFY assertions to catch bugs where we accidentally try to send
messages that exceed MAX_MESSAGE_PAYLOAD_SIZE or MAX_MESSAGE_FD_COUNT.
These are programming errors and should crash immediately so we catch
them during development, rather than silently sending invalid data.
This consolidates the message size and FD count limits into a single
header file that can be used by both the encoding and decoding sides
of the IPC layer.
A malicious peer could claim that a string, buffer, or vector has an
extremely large size (up to 4 GiB), causing allocation failures or OOM.
Add MAX_DECODED_SIZE (64 MiB) check in decode_size() to reject
excessively large containers before attempting allocation.
A malicious or misbehaving peer could send data faster than we process
it, causing unbounded memory growth in the unprocessed bytes buffer
and file descriptor queue.
Add MAX_UNPROCESSED_BUFFER_SIZE (128 MiB) and MAX_UNPROCESSED_FDS (512)
limits. When exceeded, the peer is disconnected gracefully rather than
allowing memory exhaustion.
Reject messages from peers that exceed reasonable limits:
- Maximum payload size: 64 MiB
- Maximum file descriptor count: 128
Also use checked arithmetic for message size calculations to prevent
integer overflow attacks.
These limits prevent malicious peers from causing excessive memory
allocation or resource exhaustion.
post_message() returns ErrorOr but was using MUST internally for
transfer_message(). If the transfer fails (e.g., socket error), we
would crash instead of propagating the error to the caller.
The message parsing loop performs arithmetic on payload_size (u32) and
index (size_t). While overflow is unlikely on 64-bit systems, use
Checked<size_t> to explicitly validate:
1. message_size = payload_size + sizeof(MessageHeader)
2. new_index = index + payload_size + sizeof(MessageHeader)
This prevents potential integer overflow attacks from malicious peers.
Replace crash-on-OOM patterns with graceful error handling:
- Use try_append() instead of append() for buffer operations
- Handle ByteBuffer::copy() failure instead of using MUST()
A malicious peer could send messages with large payload sizes to
trigger OOM conditions. Instead of crashing, we now disconnect
the misbehaving peer.
The previous code used Vector::resize() which internally uses MUST(),
causing a crash if memory allocation fails. A malicious peer could
send a message with a large vector size to trigger OOM and crash the
recipient.
Use try_resize() instead to propagate the error gracefully.
A malicious peer could exploit the file descriptor acknowledgement
protocol in several ways:
1. Send messages with fd_count values that overflow when accumulated
2. Send acknowledgements claiming more FDs were received than we sent
Both attacks would crash the recipient via VERIFY failures in
Queue::dequeue() or integer overflow.
Fix by using Checked<u32> for fd counters and validating the queue
isn't empty before dequeuing acknowledged FDs.