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