This repository contains both a protocol specification and a Rust implementation
(jsonrpc-fdpass crate) for JSON-RPC 2.0 with file descriptor passing over Unix
domain sockets.
This document specifies a variant of the JSON-RPC 2.0 protocol designed for reliable inter-process communication (IPC) over stream-oriented sockets. It is intended for use on POSIX-compliant systems where SOCK_SEQPACKET is unavailable (such as macOS) or undesirable.
It uses Unix domain sockets of type SOCK_STREAM, leverages JSON's self-delimiting nature for message framing, and extends the JSON-RPC 2.0 data model to support passing file descriptors using ancillary data.
The primary design goal is to provide a portable, unambiguous protocol for passing file descriptors alongside structured JSON messages over a standard byte stream.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
The transport for this protocol MUST be a Unix domain socket created with the type SOCK_STREAM.
JSON is a self-delimiting format—a compliant parser can determine where one JSON value ends and the next begins without external delimiters. This protocol leverages streaming JSON parsing for message framing.
- The JSON text MUST be encoded using UTF-8.
- Each message MUST be a complete, valid JSON object.
- Whitespace between messages is permitted but not required. Per RFC 8259, the valid whitespace characters are: space (
0x20), tab (0x09), line feed (0x0A), and carriage return (0x0D). Receivers MUST use a streaming JSON parser that skips such whitespace between values. (Note: inter-message whitespace is actively used by the FD batching mechanism; see Section 4.1.)
To ensure file descriptors are correctly associated with their corresponding messages, a sending party MUST adhere to the following rules:
- File Descriptor Ordering: All file descriptors referenced by a message MUST be sent (via ancillary data) before or with the final bytes of that message. The receiver dequeues FDs in order as complete messages are parsed; if the required FDs have not yet arrived, the connection is terminated with a Mismatched Count error.
The protocol is a strict extension of JSON-RPC 2.0. All standard rules regarding the structure of Request, Response, and Notification objects apply.
When a JSON-RPC message is accompanied by file descriptors, the message MUST include an fds field at the top level of the JSON object. This field indicates how many file descriptors are attached to the message.
{
"jsonrpc": "2.0",
"method": "writeFile",
"params": { "data": "..." },
"id": 1,
"fds": 1
}fds(integer): A non-negative integer specifying the number of file descriptors attached to this message.
When N file descriptors are passed with a message (N > 0), the fds field MUST be present and set to N. The file descriptors are passed positionally—the application layer defines the semantic mapping between FD positions and parameters. If fds is 0 or absent, no file descriptors are associated with the message.
File descriptors MUST be passed using ancillary data via the sendmsg(2) and recvmsg(2) system calls.
- The control message header (cmsghdr) MUST specify cmsg_level as SOL_SOCKET and cmsg_type as SCM_RIGHTS.
- The control message data (CMSG_DATA) MUST contain the array of integer file descriptors.
Operating systems impose limits on the number of file descriptors that can be passed in a single sendmsg() call. These limits vary by platform and generally cannot be queried at runtime.
When a message requires more file descriptors than can be sent in a single sendmsg() call, the additional FDs MUST be sent before any bytes of the next message. Since some systems require non-empty data for ancillary data delivery, these continuation calls MUST send a single whitespace byte (space, 0x20) as payload. The receiver's JSON parser will ignore inter-message whitespace per RFC 8259.
This ensures the receiver can dispatch each message as soon as it is fully parsed, without buffering subsequent messages while waiting for FDs.
Implementations SHOULD use a batch size in the range of 200-500 FDs and handle EINVAL (or equivalent) by reducing the batch size and retrying.
Because SOCK_STREAM does not preserve message boundaries, the receiver MUST implement its own buffering and parsing logic. The logic MUST correctly associate file descriptors with their corresponding message by processing both the byte stream and the ancillary data stream in the strict order they are received.
-
State Maintenance: The receiver MUST maintain two data structures in its state:
- A byte buffer for incoming data from the socket.
- A first-in, first-out (FIFO) queue for received file descriptors.
-
Reading: When the recvmsg(2) system call returns data, any received bytes MUST be appended to the end of the byte buffer. Any received file descriptors MUST be enqueued, in the order they were provided by the system call, to the back of the file descriptor queue.
-
Processing Loop: The receiver MUST process the byte buffer by repeatedly performing the following steps until no more complete messages can be extracted:
- Streaming Parse: Attempt to parse a complete JSON object from the beginning of the byte buffer using a streaming JSON parser. If the buffer contains an incomplete JSON value (e.g., the parser encounters EOF mid-value), the processing loop terminates until more data is received.
- Handle Parse Result: If parsing succeeds, record the number of bytes consumed. If parsing fails with a syntax error (not EOF), this is a fatal Framing Error (see Section 7), and the connection MUST be closed.
- Read FD Count: Read the
fdsfield from the parsed JSON message to determine the number of file descriptors (N) associated with this message. If the field is absent, N is 0. - Check FD Queue: Check if the file descriptor queue contains at least N FDs. If it contains fewer than N FDs, this is a fatal Mismatched Count error (see Section 7). The protocol state is desynchronized, and the connection MUST be closed.
- Dequeue and Associate: Dequeue the first N file descriptors from the front of the queue. These FDs correspond positionally (0 through N-1) to the file descriptors expected by the application for this message.
- Dispatch: The fully-formed message (with FDs) is now ready and SHOULD be dispatched to the application logic for handling.
- Consume Bytes: The consumed bytes MUST be removed from the front of the byte buffer.
This algorithmic approach ensures that file descriptors are always correctly matched to their corresponding messages, even when multiple messages are received in a single recvmsg() call.
A client asks a server to write to a file.
Client-side Action:
- Open a file, yielding fd = 5.
- Construct the JSON payload:
{"jsonrpc":"2.0","method":"writeFile","params":{"data":"..."},"id":1,"fds":1} - Call sendmsg() with the JSON payload and one control message containing the file descriptor 5.
Server-side Action:
- Call recvmsg(), receiving a data chunk and the file descriptor 5.
- Append the data to its byte buffer. Enqueue 5 into its FD queue.
- Begin the processing loop. The streaming parser finds a complete JSON object.
- It parses the JSON message. It reads
fds: 1, so N=1. - It checks that the FD queue size is >= 1. It is.
- It dequeues the FD 5 and associates it with the message.
- The complete message is dispatched. The processed bytes are removed from the buffer.
Protocol errors related to framing and file descriptor handling are fatal, as they indicate a desynchronization between the sender and receiver. Upon detecting such an error, the receiver MUST close the connection.
The primary error code for these issues is:
| Code | Message | Meaning |
|---|---|---|
| -32050 | File Descriptor Error | A fatal error occurred during protocol framing or FD association. The connection state is now invalid. |
Conditions that MUST be treated as fatal errors:
- Framing Error: The byte stream cannot be parsed as valid JSON (syntax error, not incomplete data).
- Mismatched Count: A parsed message's
fdsfield specifies N file descriptors, but the receiver's file descriptor queue contains fewer than N available FDs at the time of processing.
The security considerations are identical to those for other Unix domain socket protocols:
- Socket Permissions: Filesystem permissions on the socket file are the primary access control mechanism.
- Trust Boundary: The communicating processes must have a degree of mutual trust, as passing a file descriptor is a grant of capability.
- Resource Management: The receiving process is responsible for closing all file descriptors it receives to prevent resource leaks. If a connection is terminated due to a protocol error, the receiver MUST ensure that any FDs remaining in its queue are closed.