From 6888aaf3ad365ef772f8189c9958f58b85ec62d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= <jpokorny@redhat.com>
Date: Mon, 15 Apr 2019 23:20:42 +0200
Subject: [PATCH 3/7] High: pacemakerd vs. IPC/procfs confused deputy
 authenticity issue (1/4)

[1/4: new helpers to allow IPC client side to authenticate the server]

The title problem here could possibly lead to local privilege escalation
up to the root's level (and implicitly unguarded by some additional
protection layers like SELinux unless the defaults constrained further).

Main problem is that the authenticity assumptions were built on two,
seemingly mutually supporting legs leading to two CVEs assigned:

* procfs (mere process existence and right path to binary check)
  used to verify (this part was assigned CVE-2018-16878), and

* one-way only client-server authentication, putting the client
  here at the mercy of the server not necessarily cross-verified
  per the above point if at all (this part was assigned
  CVE-2018-16877)

whereas these two were in fact orthogonal, tearing security
assumptions about the "passive influencers" in the pacemaker's daemon
resilience-friendly constellation (orchestrated by the main of them,
pacemakerd) apart.  Moreover, procfs-based approach is discouraged
for other reasons.

The layout of the basic fix is as follows:
* 1/4: new helpers to allow IPC client side to authenticate the server
       (this commit, along with unifying subsequent solution for
       both CVEs)
* 2/4: pacemakerd to trust pre-existing processes via new checks instead
       (along with unifying solution for both CVEs)
* 3/4: other daemons to authenticate IPC servers of fellow processes
       (along with addressing CVE-2018-16877 alone, for parts of
       pacemaker not covered earlier)
* 4/4: CPG users to be careful about now-more-probable rival processes
       (this is merely to mitigate corner case fallout from the new
       approaches taken to face CVE-2018-16878 in particular;
       courtesy of Yan Gao of SUSE for reporting this)

With "basic", it is meant that it constitutes a self-contained best
effort solution with some compromises that can only be overcome with the
assistance of IPC library, libqb, as is also elaborated in messages of
remaining "fix" commits.  Beside that, also conventional encapsulation
of server-by-client authentication would be useful, but lack thereof
is not an obstacle (more so should there by any security related
neglectations on the IPC client side and its connection initiating
arrangement within libqb that has a potential to strike as early as
when the authenticity of the server side is yet to be examined).

One extra kludge that's introduced for FreeBSD lacking Unix socket to
remote peer PID mapping is masquerading such an unspecified PID with
value of 1, since that shall always be around as "init" task and,
deferring to proof by contradiction, cannot be pacemakerd-spawned
child either even if PID 1 was pacemakerd (and running such a child
alone is rather nonsensical).  The code making decisions based on that
value must acknowledge this craze and refrain from killing/signalling
the underlying process on this platform (but shall in general follow
the same elsewhere, keep in mind systemd socket-based activation for
instance, which would end up in such a situation easily!).
---
 configure.ac                      |  43 +++++++++++
 include/crm/common/Makefile.am    |   7 +-
 include/crm/common/ipc.h          |  55 +++++++++----
 include/crm/common/ipc_internal.h |  69 +++++++++++++++++
 lib/common/ipc.c                  | 158 ++++++++++++++++++++++++++++++++++----
 5 files changed, 303 insertions(+), 29 deletions(-)
 create mode 100644 include/crm/common/ipc_internal.h

Index: pacemaker-1.1.19+20181105.ccd6b5b10/configure.ac
===================================================================
--- pacemaker-1.1.19+20181105.ccd6b5b10.orig/configure.ac
+++ pacemaker-1.1.19+20181105.ccd6b5b10/configure.ac
@@ -454,6 +454,48 @@ do
   fi
 done
 
+us_auth=
+AC_CHECK_HEADER([sys/socket.h], [
+    AC_CHECK_DECL([SO_PEERCRED], [
+        # Linux
+        AC_CHECK_TYPE([struct ucred], [
+            us_auth=peercred_ucred;
+            AC_DEFINE([US_AUTH_PEERCRED_UCRED], [1],
+                      [Define if Unix socket auth method is
+                       getsockopt(s, SO_PEERCRED, &ucred, ...)])
+        ], [
+            # OpenBSD
+            AC_CHECK_TYPE([struct sockpeercred], [
+                us_auth=localpeercred_sockepeercred;
+                AC_DEFINE([US_AUTH_PEERCRED_SOCKPEERCRED], [1],
+                          [Define if Unix socket auth method is
+                           getsockopt(s, SO_PEERCRED, &sockpeercred, ...)])
+            ], [], [[#include <sys/socket.h>]])
+        ], [[#define _GNU_SOURCE
+             #include <sys/socket.h>]])
+    ], [], [[#include <sys/socket.h>]])
+])
+
+if test -z "${us_auth}"; then
+    # FreeBSD
+    AC_CHECK_DECL([getpeereid], [
+        us_auth=getpeereid;
+        AC_DEFINE([US_AUTH_GETPEEREID], [1],
+                  [Define if Unix socket auth method is
+                   getpeereid(s, &uid, &gid)])
+    ], [
+        # Solaris/OpenIndiana
+        AC_CHECK_DECL([getpeerucred], [
+            us_auth=getpeerucred;
+            AC_DEFINE([US_AUTH_GETPEERUCRED], [1],
+                      [Define if Unix socket auth method is
+                       getpeercred(s, &ucred)])
+        ], [
+            AC_MSG_ERROR([No way to authenticate a Unix socket peer])
+        ], [[#include <ucred.h>]])
+    ])
+fi
+
 dnl This OS-based decision-making is poor autotools practice;
 dnl feature-based mechanisms are strongly preferred.
 dnl
@@ -2169,3 +2211,4 @@ AC_MSG_RESULT([  LDFLAGS_HARDENED_EXE
 AC_MSG_RESULT([  LDFLAGS_HARDENED_LIB     = ${LDFLAGS_HARDENED_LIB}])
 AC_MSG_RESULT([  Libraries                = ${LIBS}])
 AC_MSG_RESULT([  Stack Libraries          = ${CLUSTERLIBS}])
+AC_MSG_RESULT([  Unix socket auth method  = ${us_auth}])
Index: pacemaker-1.1.19+20181105.ccd6b5b10/include/crm/common/Makefile.am
===================================================================
--- pacemaker-1.1.19+20181105.ccd6b5b10.orig/include/crm/common/Makefile.am
+++ pacemaker-1.1.19+20181105.ccd6b5b10/include/crm/common/Makefile.am
@@ -1,5 +1,7 @@
 #
-# Copyright 2004-2018 Andrew Beekhof <andrew@beekhof.net>
+# Copyright 2004-2019 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
@@ -10,7 +12,8 @@ MAINTAINERCLEANFILES = Makefile.in
 headerdir=$(pkgincludedir)/crm/common
 
 header_HEADERS = xml.h ipc.h util.h iso8601.h mainloop.h logging.h
-noinst_HEADERS = ipcs.h internal.h remote_internal.h xml_internal.h
+noinst_HEADERS = ipcs.h internal.h remote_internal.h xml_internal.h \
+		 ipc_internal.h
 if BUILD_CIBSECRETS
 noinst_HEADERS += cib_secrets.h
 endif
Index: pacemaker-1.1.19+20181105.ccd6b5b10/include/crm/common/ipc.h
===================================================================
--- pacemaker-1.1.19+20181105.ccd6b5b10.orig/include/crm/common/ipc.h
+++ pacemaker-1.1.19+20181105.ccd6b5b10/include/crm/common/ipc.h
@@ -1,19 +1,10 @@
 /*
- * Copyright (C) 2013 Andrew Beekhof <andrew@beekhof.net>
+ * Copyright 2013-2019 the Pacemaker project contributors
  *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 #ifndef CRM_COMMON_IPC__H
 #  define CRM_COMMON_IPC__H
@@ -77,6 +68,44 @@ uint32_t crm_ipc_buffer_flags(crm_ipc_t
 const char *crm_ipc_name(crm_ipc_t * client);
 unsigned int crm_ipc_default_buffer_size(void);
 
+/*!
+ * \brief Check the authenticity of the IPC socket peer process
+ *
+ * If everything goes well, peer's authenticity is verified by the means
+ * of comparing against provided referential UID and GID (either satisfies),
+ * and the result of this check can be deduced from the return value.
+ * As an exception, detected UID of 0 ("root") satisfies arbitrary
+ * provided referential daemon's credentials.
+ *
+ * \param[in]  sock    IPC related, connected Unix socket to check peer of
+ * \param[in]  refuid  referential UID to check against
+ * \param[in]  refgid  referential GID to check against
+ * \param[out] gotpid  to optionally store obtained PID of the peer
+ *                     (not available on FreeBSD, special value of 1
+ *                     used instead, and the caller is required to
+ *                     special case this value respectively)
+ * \param[out] gotuid  to optionally store obtained UID of the peer
+ * \param[out] gotgid  to optionally store obtained GID of the peer
+ *
+ * \return 0 if IPC related socket's peer is not authentic given the
+ *         referential credentials (see above), 1 if it is,
+ *         negative value on error (generally expressing -errno unless
+ *         it was zero even on nonhappy path, -pcmk_err_generic is
+ *         returned then; no message is directly emitted)
+ *
+ * \note While this function is tolerant on what constitutes authorized
+ *       IPC daemon process (its effective user matches UID=0 or \p refuid,
+ *       or at least its group matches \p refroup), either or both (in case
+ *       of UID=0) mismatches on the expected credentials of such peer
+ *       process \e shall be investigated at the caller when value of 1
+ *       gets returned there, since higher-than-expected privileges in
+ *       respect to the expected/intended credentials possibly violate
+ *       the least privilege principle and may pose an additional risk
+ *       (i.e. such accidental inconsistency shall be eventually fixed).
+ */
+int crm_ipc_is_authentic_process(int sock, uid_t refuid, gid_t refgid,
+                                 pid_t *gotpid, uid_t *gotuid, gid_t *gotgid);
+
 /* Utils */
 xmlNode *create_hello_message(const char *uuid, const char *client_name,
                               const char *major_version, const char *minor_version);
Index: pacemaker-1.1.19+20181105.ccd6b5b10/include/crm/common/ipc_internal.h
===================================================================
--- /dev/null
+++ pacemaker-1.1.19+20181105.ccd6b5b10/include/crm/common/ipc_internal.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#ifndef PCMK__IPC_INTERNAL_H
+#define PCMK__IPC_INTERNAL_H
+
+#include <sys/types.h>
+
+#include <crm_config.h>  /* US_AUTH_GETPEEREID */
+
+
+/* denotes "non yieldable PID" on FreeBSD, or actual PID1 in scenarios that
+   require a delicate handling anyway (socket-based activation with systemd);
+   we can be reasonably sure that this PID is never possessed by the actual
+   child daemon, as it gets taken either by the proper init, or by pacemakerd
+   itself (i.e. this precludes anything else); note that value of zero
+   is meant to carry "unset" meaning, and better not to bet on/conditionalize
+   over signedness of pid_t */
+#define PCMK__SPECIAL_PID  1
+
+#if defined(US_AUTH_GETPEEREID)
+/* on FreeBSD, we don't want to expose "non-yieldable PID" (leading to
+   "IPC liveness check only") as its nominal representation, which could
+   cause confusion -- this is unambiguous as long as there's no
+   socket-based activation like with systemd (very improbable) */
+#define PCMK__SPECIAL_PID_AS_0(p)  (((p) == PCMK__SPECIAL_PID) ? 0 : (p))
+#else
+#define PCMK__SPECIAL_PID_AS_0(p)  (p)
+#endif
+
+/*!
+ * \internal
+ * \brief Check the authenticity and liveness of the process via IPC end-point
+ *
+ * When IPC daemon under given IPC end-point (name) detected, its authenticity
+ * is verified by the means of comparing against provided referential UID and
+ * GID, and the result of this check can be deduced from the return value.
+ * As an exception, referential UID of 0 (~ root) satisfies arbitrary
+ * detected daemon's credentials.
+ *
+ * \param[in]  name    IPC name to base the search on
+ * \param[in]  refuid  referential UID to check against
+ * \param[in]  refgid  referential GID to check against
+ * \param[out] gotpid  to optionally store obtained PID of the found process
+ *                     upon returning 1 or -2
+ *                     (not available on FreeBSD, special value of 1,
+ *                     see PCMK__SPECIAL_PID, used instead, and the caller
+ *                     is required to special case this value respectively)
+ *
+ * \return 0 if no trace of IPC peer's liveness detected, 1 if it was,
+ *         -1 on error, and -2 when the IPC blocked with unauthorized
+ *         process (log message emitted in both latter cases)
+ *
+ * \note This function emits a log message also in case there isn't a perfect
+ *       match in respect to \p reguid and/or \p refgid, for a possible
+ *       least privilege principle violation.
+ *
+ * \see crm_ipc_is_authentic_process
+ */
+int pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid,
+                                          gid_t refgid, pid_t *gotpid);
+
+#endif
Index: pacemaker-1.1.19+20181105.ccd6b5b10/lib/common/ipc.c
===================================================================
--- pacemaker-1.1.19+20181105.ccd6b5b10.orig/lib/common/ipc.c
+++ pacemaker-1.1.19+20181105.ccd6b5b10/lib/common/ipc.c
@@ -1,23 +1,25 @@
 /*
- * Copyright (C) 2004 Andrew Beekhof <andrew@beekhof.net>
+ * Copyright 2004-2019 the Pacemaker project contributors
  *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
+ * The version control history for this file may have further details.
  *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
+#if defined(US_AUTH_PEERCRED_UCRED) || defined(US_AUTH_PEERCRED_SOCKPEERCRED)
+#  ifdef US_AUTH_PEERCRED_UCRED
+#    ifndef _GNU_SOURCE
+#      define _GNU_SOURCE
+#    endif
+#  endif
+#  include <sys/socket.h>
+#elif defined(US_AUTH_GETPEERUCRED)
+#  include <ucred.h>
+#endif
+
 #include <sys/param.h>
 
 #include <stdio.h>
@@ -30,11 +32,13 @@
 #include <fcntl.h>
 #include <bzlib.h>
 
-#include <crm/crm.h>
+#include <crm/crm.h>   /* indirectly: pcmk_err_generic */
 #include <crm/msg_xml.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipcs.h>
 
+#include <crm/common/ipc_internal.h>  /* PCMK__SPECIAL_PID* */
+
 #define PCMK_IPC_VERSION 1
 
 /* Evict clients whose event queue grows this large (by default) */
@@ -1396,6 +1400,132 @@ crm_ipc_send(crm_ipc_t * client, xmlNode
     return rc;
 }
 
+int
+crm_ipc_is_authentic_process(int sock, uid_t refuid, gid_t refgid,
+                             pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) {
+    int ret = 0;
+    pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0;
+#if defined(US_AUTH_PEERCRED_UCRED)
+    struct ucred ucred;
+    socklen_t ucred_len = sizeof(ucred);
+
+    if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED,
+                    &ucred, &ucred_len)
+                && ucred_len == sizeof(ucred)) {
+        found_pid = ucred.pid; found_uid = ucred.uid; found_gid = ucred.gid;
+
+#elif defined(US_AUTH_PEERCRED_SOCKPEERCRED)
+    struct sockpeercred sockpeercred;
+    socklen_t sockpeercred_len = sizeof(sockpeercred);
+
+    if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED,
+                    &sockpeercred, &sockpeercred_len)
+                && sockpeercred_len == sizeof(sockpeercred_len)) {
+        found_pid = sockpeercred.pid;
+        found_uid = sockpeercred.uid; found_gid = sockpeercred.gid;
+
+#elif defined(US_AUTH_GETPEEREID)
+    if (!getpeereid(sock, &found_uid, &found_gid)) {
+        found_pid = PCMK__SPECIAL_PID;  /* cannot obtain PID (FreeBSD) */
+
+#elif defined(US_AUTH_GETPEERUCRED)
+    ucred_t *ucred;
+    if (!getpeerucred(sock, &ucred)) {
+        errno = 0;
+        found_pid = ucred_getpid(ucred);
+        found_uid = ucred_geteuid(ucred); found_gid = ucred_getegid(ucred);
+        ret = -errno;
+        ucred_free(ucred);
+        if (ret) {
+            return (ret < 0) ? ret : -pcmk_err_generic;
+        }
+
+#else
+#  error "No way to authenticate a Unix socket peer"
+    errno = 0;
+    if (0) {
+#endif
+        if (gotpid != NULL) {
+            *gotpid = found_pid;
+        }
+        if (gotuid != NULL) {
+            *gotuid = found_uid;
+        }
+        if (gotgid != NULL) {
+            *gotgid = found_gid;
+        }
+        ret = (found_uid == 0 || found_uid == refuid || found_gid == refgid);
+    } else {
+        ret = (errno > 0) ? -errno : -pcmk_err_generic;
+    }
+
+    return ret;
+}
+
+int
+pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid,
+                                      gid_t refgid, pid_t *gotpid) {
+    static char last_asked_name[PATH_MAX / 2] = "";  /* log spam prevention */
+    int fd, ret = 0;
+    pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0;
+    qb_ipcc_connection_t *c;
+
+    if ((c = qb_ipcc_connect(name, 0)) == NULL) {
+        crm_info("Could not connect to %s IPC: %s", name, strerror(errno));
+
+    } else if ((ret = qb_ipcc_fd_get(c, &fd))) {
+        crm_err("Could not get fd from %s IPC: %s (%d)", name,
+                strerror(-ret), -ret);
+        ret = -1;
+
+    } else if ((ret = crm_ipc_is_authentic_process(fd, refuid, refgid,
+                                                   &found_pid, &found_uid,
+                                                   &found_gid)) < 0) {
+        if (ret == -pcmk_err_generic) {
+            crm_err("Could not get peer credentials from %s IPC", name);
+        } else {
+            crm_err("Could not get peer credentials from %s IPC: %s (%d)",
+                    name, strerror(-ret), -ret);
+        }
+        ret = -1;
+
+    } else {
+        if (gotpid != NULL) {
+            *gotpid = found_pid;
+        }
+
+        if (!ret) {
+            crm_err("Daemon (IPC %s) effectively blocked with unauthorized"
+                    " process %lld (uid: %lld, gid: %lld)",
+                    name, (long long) PCMK__SPECIAL_PID_AS_0(found_pid),
+                    (long long) found_uid, (long long) found_gid);
+            ret = -2;
+        } else if ((found_uid != refuid || found_gid != refgid)
+                && strncmp(last_asked_name, name, sizeof(last_asked_name))) {
+            if (!found_uid && refuid) {
+                crm_warn("Daemon (IPC %s) runs as root, whereas the expected"
+                         " credentials are %lld:%lld, hazard of violating"
+                         " the least privilege principle",
+                         name, (long long) refuid, (long long) refgid);
+            } else {
+                crm_notice("Daemon (IPC %s) runs as %lld:%lld, whereas the"
+                           " expected credentials are %lld:%lld, which may"
+                           " mean a different set of privileges than expected",
+                           name, (long long) found_uid, (long long) found_gid,
+                           (long long) refuid, (long long) refgid);
+            }
+            memccpy(last_asked_name, name, '\0', sizeof(last_asked_name));
+        }
+    }
+
+    if (ret) {  /* here, !ret only when we could not initially connect */
+        qb_ipcc_disconnect(c);
+    }
+
+    return ret;
+}
+
+
 /* Utils */
 
 xmlNode *
