lib/daemon: support --user option for all OVS daemon
[cascardo/ovs.git] / lib / daemon-unix.c
index eb95521..6438061 100644 (file)
@@ -19,6 +19,8 @@
 #include "daemon-private.h"
 #include <errno.h>
 #include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
 #include <signal.h>
 #include <stdlib.h>
 #include <string.h>
@@ -26,6 +28,9 @@
 #include <sys/wait.h>
 #include <sys/stat.h>
 #include <unistd.h>
+#if HAVE_LIBCAPNG
+#include <cap-ng.h>
+#endif
 #include "command-line.h"
 #include "fatal-signal.h"
 #include "dirs.h"
 
 VLOG_DEFINE_THIS_MODULE(daemon_unix);
 
+#ifdef __linux__
+#define LINUX 1
+#else
+#define LINUX 0
+#endif
+
+#if HAVE_LIBCAPNG
+#define LIBCAPNG 1
+#else
+#define LIBCAPNG 0
+#endif
+
 /* --detach: Should we run in the background? */
 bool detach;                    /* Was --detach specified? */
 static bool detached;           /* Have we already detached? */
@@ -64,6 +81,15 @@ static int daemonize_fd = -1;
  * it dies due to an error signal? */
 static bool monitor;
 
+/* --user: Only root can use this option. Switch to new uid:gid after
+ * initially running as root.  */
+static bool switch_user = false;
+static bool non_root_user = false;
+static uid_t uid;
+static gid_t gid;
+static char *user = NULL;
+static void daemon_become_new_user__(bool access_datapath);
+
 static void check_already_running(void);
 static int lock_pidfile(FILE *, int command);
 static pid_t fork_and_clean_up(void);
@@ -409,11 +435,21 @@ monitor_daemon(pid_t daemon_pid)
  * daemon_complete()) or that it failed to start up (by exiting with a nonzero
  * exit code). */
 void
-daemonize_start(void)
+daemonize_start(bool access_datapath)
 {
     assert_single_threaded();
     daemonize_fd = -1;
 
+    if (switch_user) {
+        daemon_become_new_user__(access_datapath);
+        switch_user = false;
+    }
+
+    /* If --user is specified, make sure user switch has completed by now.  */
+    if (non_root_user) {
+        ovs_assert(geteuid() && getuid());
+    }
+
     if (detach) {
         pid_t pid;
 
@@ -684,3 +720,326 @@ should_service_stop(void)
 {
     return false;
 }
+
+\f
+static bool
+gid_matches(gid_t expected, gid_t value)
+{
+    return expected == -1 || expected == value;
+}
+
+static bool
+gid_verify(gid_t real, gid_t effective, gid_t saved)
+{
+    gid_t r, e, s;
+
+    return (getresgid(&r, &e, &s) == 0 &&
+            gid_matches(real, r) &&
+            gid_matches(effective, e) &&
+            gid_matches(saved, s));
+}
+
+static void
+daemon_switch_group(gid_t real, gid_t effective,
+                    gid_t saved)
+{
+    if ((setresgid(real, effective, saved) == -1) ||
+        !gid_verify(real, effective, saved)) {
+        VLOG_FATAL("%s: fail to switch group to gid as %d, aborting",
+                   pidfile, gid);
+    }
+}
+
+static bool
+uid_matches(uid_t expected, uid_t value)
+{
+    return expected == -1 || expected == value;
+}
+
+static bool
+uid_verify(const uid_t real, const uid_t effective, const uid_t saved)
+{
+    uid_t r, e, s;
+
+    return (getresuid(&r, &e, &s) == 0 &&
+            uid_matches(real, r) &&
+            uid_matches(effective, e) &&
+            uid_matches(saved, s));
+}
+
+static void
+daemon_switch_user(const uid_t real, const uid_t effective, const uid_t saved,
+                   const char *user)
+{
+    if ((setresuid(real, effective, saved) == -1) ||
+        !uid_verify(real, effective, saved)) {
+        VLOG_FATAL("%s: fail to switch user to %s, aborting",
+                   pidfile, user);
+    }
+}
+
+/* Use portable Unix APIs to switch uid:gid, when datapath
+ * access is not required.  On Linux systems, all capabilities
+ * will be dropped.  */
+static void
+daemon_become_new_user_unix(void)
+{
+    /* "Setuid Demystified" by Hao Chen, etc outlines some caveats of
+     * around unix system call setuid() and friends. This implementation
+     * mostly follow the advice given by the paper.  The paper is
+     * published in 2002, so things could have changed.  */
+
+    /* Change both real and effective uid and gid will permanently
+     * drop the process' privilege.  "Setuid Demystified" suggested
+     * that calling getuid() after each setuid() call to verify they
+     * are actually set, because checking return code alone is not
+     * sufficient.  */
+    daemon_switch_group(gid, gid, gid);
+    if (user && initgroups(user, gid) == -1) {
+        VLOG_FATAL("%s: fail to add supplementary group gid %d, "
+                   "aborting", pidfile, gid);
+    }
+    daemon_switch_user(uid, uid, uid, user);
+}
+
+/* Linux specific implementation of daemon_become_new_user()
+ * using libcap-ng.   */
+#if defined __linux__ &&  HAVE_LIBCAPNG
+static void
+daemon_become_new_user_linux(bool access_datapath)
+{
+    int ret;
+
+    ret = capng_get_caps_process();
+
+    if (!ret) {
+        if (capng_have_capabilities(CAPNG_SELECT_CAPS) > CAPNG_NONE) {
+            const capng_type_t cap_sets = CAPNG_EFFECTIVE|CAPNG_PERMITTED;
+
+            capng_clear(CAPNG_SELECT_BOTH);
+
+            ret = capng_update(CAPNG_ADD, cap_sets, CAP_IPC_LOCK)
+                  || capng_update(CAPNG_ADD, cap_sets, CAP_NET_BIND_SERVICE);
+
+            if (access_datapath && !ret) {
+                ret = capng_update(CAPNG_ADD, cap_sets, CAP_NET_ADMIN)
+                      || capng_update(CAPNG_ADD, cap_sets, CAP_NET_RAW);
+            }
+        } else {
+            ret = -1;
+        }
+    }
+
+    if (!ret) {
+        /* CAPNG_INIT_SUPP_GRP will be a better choice than
+         * CAPNG_DROP_SUPP_GRP. However this enum value is only defined
+         * with libcap-ng higher than version 0.7.4, which is not wildly
+         * available on many Linux distributions yet. Taking a more
+         * conservative approach to make sure OVS behaves consistently.
+         *
+         * XXX We may change this for future OVS releases.
+         */
+        ret = capng_change_id(uid, gid, CAPNG_DROP_SUPP_GRP
+                              | CAPNG_CLEAR_BOUNDING);
+    }
+
+    if (ret) {
+        VLOG_FATAL("%s: libcap-ng fail to switch to user and group "
+                   "%d:%d, aborting", pidfile, uid, gid);
+    }
+}
+#endif
+
+static void
+daemon_become_new_user__(bool access_datapath)
+{
+    if (LINUX) {
+        if (LIBCAPNG) {
+            daemon_become_new_user_linux(access_datapath);
+        } else {
+            VLOG_FATAL("%s: fail to downgrade user using libcap-ng. "
+                       "(libcap-ng is not configured at compile time), "
+                       "aborting.", pidfile);
+        }
+    } else {
+        daemon_become_new_user_unix();
+    }
+}
+
+/* Noramlly, user switch is embedded within daemonize_start().
+ * However, there in case the user switch needs to be done
+ * before daemonize_start(), the following API can be used.  */
+void
+daemon_become_new_user(bool access_datapath)
+{
+    assert_single_threaded();
+    if (switch_user) {
+        daemon_become_new_user__(access_datapath);
+
+        /* Make sure daemonize_start() will not switch
+         * user again. */
+        switch_user = false;
+    }
+}
+
+/* Return the maximun suggested buffer size for both getpwname_r()
+ * and getgrnam_r().
+ *
+ * This size may still not be big enough. in case getpwname_r()
+ * and friends return ERANGE, a larger buffer should be supplied to
+ * retry. (The man page did not specify the max size to stop at, we
+ * will keep trying with doubling the buffer size for each round until
+ * the size wrapps around size_t.  */
+static size_t
+get_sysconf_buffer_size(void)
+{
+    size_t bufsize, pwd_bs = 0, grp_bs = 0;
+    const size_t default_bufsize = 1024;
+
+    errno = 0;
+    if ((pwd_bs = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) {
+        if (errno) {
+            VLOG_FATAL("%s: Read initial passwordd struct size "
+                       "failed (%s), aborting. ", pidfile,
+                       ovs_strerror(errno));
+        }
+    }
+
+    if ((grp_bs = sysconf(_SC_GETGR_R_SIZE_MAX)) == -1) {
+        if (errno) {
+            VLOG_FATAL("%s: Read initial group struct size "
+                       "failed (%s), aborting. ", pidfile,
+                       ovs_strerror(errno));
+        }
+    }
+
+    bufsize = MAX(pwd_bs, grp_bs);
+    return bufsize ? bufsize : default_bufsize;
+}
+
+/* Try to double the size of '*buf', return true
+ * if successful, and '*sizep' will be updated with
+ * the new size. Otherwise, return false.  */
+static bool
+enlarge_buffer(char **buf, size_t *sizep)
+{
+    size_t newsize = *sizep * 2;
+
+    if (newsize > *sizep) {
+        *buf = xrealloc(*buf, newsize);
+        *sizep = newsize;
+        return true;
+    }
+
+    return false;
+}
+
+/* Parse and sanity check user_spec.
+ *
+ * If successful, set global variables 'uid' and 'gid'
+ * with the parsed results. Global variable 'user'
+ * will be pointing to a string that stores the name
+ * of the user to be switched into.
+ *
+ * Also set 'switch_to_new_user' to true, The actual
+ * user switching is done as soon as daemonize_start()
+ * is called. I/O access before calling daemonize_start()
+ * will still be with root's credential.  */
+void
+daemon_set_new_user(const char *user_spec)
+{
+    char *pos = strchr(user_spec, ':');
+    size_t init_bufsize, bufsize;
+
+    init_bufsize = get_sysconf_buffer_size();
+    uid = getuid();
+    gid = getgid();
+
+    if (geteuid() || uid) {
+        VLOG_FATAL("%s: only root can use --user option", pidfile);
+    }
+
+    user_spec += strspn(user_spec, " \t\r\n");
+    size_t len = pos ? pos - user_spec : strlen(user_spec);
+    char *buf;
+    struct passwd pwd, *res;
+    int e;
+
+    bufsize = init_bufsize;
+    buf = xmalloc(bufsize);
+    if (len) {
+        user = xmemdup0(user_spec, len);
+
+        while ((e = getpwnam_r(user, &pwd, buf, bufsize, &res)) == ERANGE) {
+            if (!enlarge_buffer(&buf, &bufsize)) {
+                break;
+            }
+        }
+
+        if (e != 0) {
+            VLOG_FATAL("%s: Failed to retrive user %s's uid (%s), aborting.",
+                       pidfile, user, ovs_strerror(e));
+        }
+    } else {
+        /* User name is not specified, use current user.  */
+        while ((e = getpwuid_r(uid, &pwd, buf, bufsize, &res)) == ERANGE) {
+            if (!enlarge_buffer(&buf, &bufsize)) {
+                break;
+            }
+        }
+
+        if (e != 0) {
+            VLOG_FATAL("%s: Failed to retrive current user's name "
+                       "(%s), aborting.", pidfile, ovs_strerror(e));
+        }
+        user = xstrdup(pwd.pw_name);
+    }
+
+    uid = pwd.pw_uid;
+    gid = pwd.pw_gid;
+    free(buf);
+
+    if (pos) {
+        char *grpstr = pos + 1;
+        grpstr += strspn(grpstr, " \t\r\n");
+
+        if (*grpstr) {
+            struct group grp, *res;
+
+            bufsize = init_bufsize;
+            buf = xmalloc(bufsize);
+            while ((e = getgrnam_r(grpstr, &grp, buf, bufsize, &res))
+                         == ERANGE) {
+                if (!enlarge_buffer(&buf, &bufsize)) {
+                    break;
+                }
+            }
+
+            if (e) {
+                VLOG_FATAL("%s: Failed to get group entry for %s, "
+                           "(%s), aborting.", pidfile, grpstr,
+                           ovs_strerror(e));
+            }
+
+            if (gid != grp.gr_gid) {
+                char **mem;
+
+                for (mem = grp.gr_mem; *mem; ++mem) {
+                    if (!strcmp(*mem, user)) {
+                        break;
+                    }
+                }
+
+                if (!*mem) {
+                    VLOG_FATAL("%s: Invalid --user option %s (user %s is "
+                               "not in group %s), aborting.", pidfile,
+                               user_spec, user, grpstr);
+                }
+                gid = grp.gr_gid;
+            }
+            free(buf);
+        }
+    }
+
+    switch_user = non_root_user = true;
+}