]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
json-stream: stop concatenating fd-bearing queue items with prior output-buffer bytes
authorDaan De Meyer <daan@amutable.com>
Fri, 24 Apr 2026 07:34:07 +0000 (07:34 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 24 Apr 2026 12:29:31 +0000 (14:29 +0200)
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.

src/libsystemd/sd-json/json-stream.c

index 1900d1c7da4d3c101dae5811dd3167da7915bb0a..3775691d47f67b349e5e0f222eaadb4fbb240657 100644 (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)