From: Daan De Meyer Date: Fri, 24 Apr 2026 07:34:07 +0000 (+0000) Subject: json-stream: stop concatenating fd-bearing queue items with prior output-buffer bytes X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=5f420abe1a86eca2ca0a9509edf96979530e885d;p=thirdparty%2Fsystemd.git 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. --- diff --git a/src/libsystemd/sd-json/json-stream.c b/src/libsystemd/sd-json/json-stream.c index 1900d1c7da4..3775691d47f 100644 --- a/src/libsystemd/sd-json/json-stream.c +++ b/src/libsystemd/sd-json/json-stream.c @@ -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)