17aa40
From a42cf9af339f48f633fa0b17a960e1e407b7450f Mon Sep 17 00:00:00 2001
17aa40
From: Lorenz Bauer <lmb@cloudflare.com>
17aa40
Date: Mon, 4 Nov 2019 16:35:46 +0000
17aa40
Subject: [PATCH] journal: refresh cached credentials of stdout streams
17aa40
17aa40
journald assumes that getsockopt(SO_PEERCRED) correctly identifies the
17aa40
process on the remote end of the socket. However, this is incorrect
17aa40
according to man 7 socket:
17aa40
17aa40
    The returned  credentials  are  those that were in effect at the
17aa40
    time of the call to connect(2) or socketpair(2).
17aa40
17aa40
This becomes a problem when a new process inherits the stdout stream
17aa40
from a parent. First, log messages from the child process will
17aa40
be attributed to the parent. Second, the struct ucred used by journald
17aa40
becomes invalid as soon as the parent exits. Further sendmsg calls then
17aa40
fail with ENOENT. Logs for the child process then vanish from the journal.
17aa40
17aa40
Fix this by using recvmsg on the stdout stream, and refreshing the cached
17aa40
struct ucred if SCM_CREDENTIALS indicate a new process.
17aa40
17aa40
Fixes #13708
17aa40
17aa40
(cherry picked from commit 09d0b46ab61bebafe5bdc1be95ee153dfb13d6bc)
17aa40
17aa40
Resolves: #1931806
17aa40
---
17aa40
 src/journal/journald-stream.c        | 49 ++++++++++++++++++++++++++--
17aa40
 test/TEST-04-JOURNAL/test-journal.sh | 13 ++++++++
17aa40
 2 files changed, 60 insertions(+), 2 deletions(-)
17aa40
17aa40
diff --git a/src/journal/journald-stream.c b/src/journal/journald-stream.c
17aa40
index 6f8a4011ff..302a82d3d7 100644
17aa40
--- a/src/journal/journald-stream.c
17aa40
+++ b/src/journal/journald-stream.c
17aa40
@@ -484,11 +484,22 @@ static int stdout_stream_scan(StdoutStream *s, bool force_flush) {
17aa40
 }
17aa40
 
17aa40
 static int stdout_stream_process(sd_event_source *es, int fd, uint32_t revents, void *userdata) {
17aa40
+        uint8_t buf[CMSG_SPACE(sizeof(struct ucred))];
17aa40
         StdoutStream *s = userdata;
17aa40
+        struct ucred *ucred = NULL;
17aa40
+        struct cmsghdr *cmsg;
17aa40
+        struct iovec iovec;
17aa40
         size_t limit;
17aa40
         ssize_t l;
17aa40
         int r;
17aa40
 
17aa40
+        struct msghdr msghdr = {
17aa40
+                .msg_iov = &iovec,
17aa40
+                .msg_iovlen = 1,
17aa40
+                .msg_control = buf,
17aa40
+                .msg_controllen = sizeof(buf),
17aa40
+        };
17aa40
+
17aa40
         assert(s);
17aa40
 
17aa40
         if ((revents|EPOLLIN|EPOLLHUP) != (EPOLLIN|EPOLLHUP)) {
17aa40
@@ -508,20 +519,50 @@ static int stdout_stream_process(sd_event_source *es, int fd, uint32_t revents,
17aa40
          * always leave room for a terminating NUL we might need to add. */
17aa40
         limit = MIN(s->allocated - 1, s->server->line_max);
17aa40
 
17aa40
-        l = read(s->fd, s->buffer + s->length, limit - s->length);
17aa40
+        iovec = IOVEC_MAKE(s->buffer + s->length, limit - s->length);
17aa40
+
17aa40
+        l = recvmsg(s->fd, &msghdr, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
17aa40
         if (l < 0) {
17aa40
-                if (errno == EAGAIN)
17aa40
+                if (IN_SET(errno, EINTR, EAGAIN))
17aa40
                         return 0;
17aa40
 
17aa40
                 log_warning_errno(errno, "Failed to read from stream: %m");
17aa40
                 goto terminate;
17aa40
         }
17aa40
+        cmsg_close_all(&msghdr);
17aa40
 
17aa40
         if (l == 0) {
17aa40
                 stdout_stream_scan(s, true);
17aa40
                 goto terminate;
17aa40
         }
17aa40
 
17aa40
+        CMSG_FOREACH(cmsg, &msghdr)
17aa40
+                if (cmsg->cmsg_level == SOL_SOCKET &&
17aa40
+                    cmsg->cmsg_type == SCM_CREDENTIALS &&
17aa40
+                    cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) {
17aa40
+                        ucred = (struct ucred *)CMSG_DATA(cmsg);
17aa40
+                        break;
17aa40
+                }
17aa40
+
17aa40
+        /* Invalidate the context if the pid of the sender changed.
17aa40
+         * This happens when a forked process inherits stdout / stderr
17aa40
+         * from a parent. In this case getpeercred returns the ucred
17aa40
+         * of the parent, which can be invalid if the parent has exited
17aa40
+         * in the meantime.
17aa40
+         */
17aa40
+        if (ucred && ucred->pid != s->ucred.pid) {
17aa40
+                /* force out any previously half-written lines from a
17aa40
+                 * different process, before we switch to the new ucred
17aa40
+                 * structure for everything we just added */
17aa40
+                r = stdout_stream_scan(s, true);
17aa40
+                if (r < 0)
17aa40
+                        goto terminate;
17aa40
+
17aa40
+                s->ucred = *ucred;
17aa40
+                client_context_release(s->server, s->context);
17aa40
+                s->context = NULL;
17aa40
+        }
17aa40
+
17aa40
         s->length += l;
17aa40
         r = stdout_stream_scan(s, false);
17aa40
         if (r < 0)
17aa40
@@ -559,6 +600,10 @@ int stdout_stream_install(Server *s, int fd, StdoutStream **ret) {
17aa40
         if (r < 0)
17aa40
                 return log_error_errno(r, "Failed to determine peer credentials: %m");
17aa40
 
17aa40
+        r = setsockopt_int(fd, SOL_SOCKET, SO_PASSCRED, true);
17aa40
+        if (r < 0)
17aa40
+                return log_error_errno(r, "SO_PASSCRED failed: %m");
17aa40
+
17aa40
         if (mac_selinux_use()) {
17aa40
                 r = getpeersec(fd, &stream->label);
17aa40
                 if (r < 0 && r != -EOPNOTSUPP)
17aa40
diff --git a/test/TEST-04-JOURNAL/test-journal.sh b/test/TEST-04-JOURNAL/test-journal.sh
17aa40
index 260cae09ab..52a6ee84d1 100755
17aa40
--- a/test/TEST-04-JOURNAL/test-journal.sh
17aa40
+++ b/test/TEST-04-JOURNAL/test-journal.sh
17aa40
@@ -63,6 +63,19 @@ grep -q '^PRIORITY=6$' /output
17aa40
 ! grep -q '^FOO=' /output
17aa40
 ! grep -q '^SYSLOG_FACILITY=' /output
17aa40
 
17aa40
+# https://github.com/systemd/systemd/issues/13708
17aa40
+ID=$(journalctl --new-id128 | sed -n 2p)
17aa40
+systemd-cat -t "$ID" bash -c 'echo parent; (echo child) & wait' &
17aa40
+PID=$!
17aa40
+wait %%
17aa40
+journalctl --sync
17aa40
+# We can drop this grep when https://github.com/systemd/systemd/issues/13937
17aa40
+# has a fix.
17aa40
+journalctl -b -o export -t "$ID" --output-fields=_PID | grep '^_PID=' >/output
17aa40
+[[ `grep -c . /output` -eq 2 ]]
17aa40
+grep -q "^_PID=$PID" /output
17aa40
+grep -vq "^_PID=$PID" /output
17aa40
+
17aa40
 # Don't lose streams on restart
17aa40
 systemctl start forever-print-hola
17aa40
 sleep 3