json-stream: stop concatenating fd-bearing queue items with prior output-buffer bytes

json_stream_format_queue() drains queued output items into the output buffer and
stages their fds in n_output_fds, relying on the downstream sendmsg() to deliver
bytes-and-ancillary atomically as one SCM_RIGHTS message. If the output buffer
already holds bytes (from a prior fast-path enqueue that hasn't been sent yet or
from a partial write), concatenating a new fd-bearing item's JSON into it means
the next sendmsg() ships the combined bytes with those fds attached — violating
the per-message fd boundary on transports where that boundary is load-bearing.

Bail out of the drain loop when we would cross that boundary, so the next
write() first sends the buffered bytes with no ancillary, then pulls the
fd-bearing item into a clean buffer and ships it on its own sendmsg.

This only produces an observable difference for SOCK_SEQPACKET / SOCK_DGRAM
consumers: on those transports each sendmsg() is its own datagram with its own
SCM_RIGHTS cmsg, so whether we concatenate matters. On AF_UNIX SOCK_STREAM
(today's sole consumer shape, used by sd-varlink and the QMP client) the kernel
absorbs a preceding non-scm skb forward into the next scm-bearing skb's recv,
so per-sendmsg separation is invisible to the receiver anyway — the guard is
cheap defensive sender hygiene there, not a behaviour change. It becomes load-
bearing the moment a SEQPACKET/DGRAM consumer wires JsonStream up.
This commit is contained in:
Daan De Meyer
2026-04-24 14:29:31 +02:00
committed by Daan De Meyer
parent 54c124eb6c
commit 5f420abe1a
+20 -3
View File
@@ -990,8 +990,9 @@ static int json_stream_format_queue(JsonStream *s) {
assert(s);
/* Drain entries out of the output queue and format them into the output buffer. Stop
* if there are unwritten output_fds, since adding more would corrupt the fd boundary. */
/* Drain entries out of the output queue and format them into the output buffer.
* Stop if there are unwritten output_fds or if the next item carries fds but
* the output buffer is non-empty, since adding more would corrupt the fd boundary. */
while (s->output_queue) {
assert(s->n_output_queue > 0);
@@ -1000,8 +1001,24 @@ static int json_stream_format_queue(JsonStream *s) {
return 0;
JsonStreamQueueItem *q = s->output_queue;
_cleanup_free_ int *array = NULL;
/* If the next item carries fds but the output buffer still holds bytes from
* a prior fast-path enqueue or a partial write, we must not concatenate its
* JSON into that same buffer: the subsequent sendmsg() in json_stream_write()
* would attach the fds to the combined bytes and break the message-to-fd boundary.
* Stop here and let json_stream_write() drain the buffer first; the next write()
* call will pull this item into a clean buffer.
*
* Note: this only produces a difference on SOCK_SEQPACKET / SOCK_DGRAM, where
* each sendmsg() is its own datagram with its own SCM_RIGHTS cmsg. On AF_UNIX
* SOCK_STREAM the kernel absorbs a preceding non-scm skb forward into the
* next scm-bearing skb's recv, so per-sendmsg separation is invisible to the
* receiver anyway. Kept as cheap defensive sender hygiene that's necessary
* the moment a SEQPACKET/DGRAM consumer wires JsonStream up. */
if (q->n_fds > 0 && s->output_buffer_size > 0)
return 0;
_cleanup_free_ int *array = NULL;
if (q->n_fds > 0) {
array = newdup(int, q->fds, q->n_fds);
if (!array)