[systemd-devel] [HACK/RFC/PATCH] systemd-su: "su" on steroids

David Herrmann dh.herrmann at gmail.com
Mon Dec 2 12:47:00 PST 2013


So people continue asking how to spawn a process in a new session from the
command-line. Turns out, it's not as easy as you might think. I stopped at
some point trying, but something like this should work:
  systemd-run --description="Test" -- /bin/sh -- XDG_SEAT=seat0 XDG_VTNR=5 su - david -- /some/command

However, without modifying the pam-su configuration, this still doesn't
spawn a new session (su takes over the existing session). "sudo" allows
changing the pam-service to "login" on the command line, so maybe that
would work..

Fortunately, I was hacking on systemd-welcomed the last few days and had
to deal with PAM, anyway. So I thought why no hack on a small "su" helper
which spawns the given command in a fresh new session. As a bonus, I
wanted to pass file-descriptors to the new process, so I could run bash in
a new session but connected to STDIN/OUT of the caller.

4h later, I present "systemd-su":

If you want to give it a try, run:
  systemd-su -u david /bin/sh

It requires the systemd-suexec helper internally, so if you didn't install
it, use something like this:
  SYSTEMD_SUEXEC=/home/david/dev/systemd/systemd-suexec ./systemd-su -u david sh
Otherwise, systemd-su cannot find the systemd-suexec binary.

Last but not least, systemd-su is probably not SETUID, so run it via sudo!

What does this do?
Quite easy: systemd-su parses it's arguments, rearranges them and prepends
them by "systemd-suexec <some-internal-args> -- <your-command>". Then it
passes this to systemd and spawns it as transient unit (like systemd-run).
The systemd-suexec helper uses pam to do the initialization dance, creates
the new session and then spawns your command in it.

Additionally, I create an anonymous AF_UNIX socket in systemd-su which
systemd-suexec connects to. I then pass file-descriptors to systemd-suexec
which installes them as STDIN/OUT/ERR.
I actually think it would be quite useful to extend the
StartTransientUnit() dbus call to allow passing FDs. It would allow us to
"store" FDs with active units, which would be useful for a lot of other
concepts (like storing DRM framebuffer handles..).

This also requires /etc/pam.d/systemd-suexec to be:
    #%PAM-1.0
    # Used by systemd-suexec.
    auth            requisite       pam_nologin.so
    auth            sufficient      pam_rootok.so
    auth            required        pam_deny.so
    account         required        pam_nologin.so
    account         required        pam_unix.so
    password        required        pam_deny.so
    session         required        pam_unix.so
    session         required        pam_systemd.so
    session         required        pam_env.so
Or something equivalent..

Also note, this is a HACK! I can run /bin/sh now, but this helper still
lacks several features and I'm not sure it's that useful to other people..
I just thought sharing it doesn't hurt.

Cheers
David
---
 .gitignore               |   2 +
 Makefile.am              |  38 +++++
 src/shared/session-pam.c | 394 +++++++++++++++++++++++++++++++++++++++++++
 src/shared/session-pam.h |  53 ++++++
 src/su/Makefile          |   1 +
 src/su/su.c              | 429 +++++++++++++++++++++++++++++++++++++++++++++++
 src/su/suexec.c          | 377 +++++++++++++++++++++++++++++++++++++++++
 7 files changed, 1294 insertions(+)
 create mode 100644 src/shared/session-pam.c
 create mode 100644 src/shared/session-pam.h
 create mode 120000 src/su/Makefile
 create mode 100644 src/su/su.c
 create mode 100644 src/su/suexec.c

diff --git a/.gitignore b/.gitignore
index 9fd42d5..22298b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,6 +78,8 @@
 /systemd-shutdownd
 /systemd-sleep
 /systemd-bus-proxyd
+/systemd-su
+/systemd-suexec
 /systemd-sysctl
 /systemd-system-update-generator
 /systemd-timedated
diff --git a/Makefile.am b/Makefile.am
index 5c65281..b5788a9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1880,6 +1880,44 @@ systemd_run_LDADD = \
 	libsystemd-shared.la
 
 # ------------------------------------------------------------------------------
+if ENABLE_LOGIND
+bin_PROGRAMS += \
+	systemd-su
+rootlibexec_PROGRAMS += \
+	systemd-suexec
+
+systemd_su_SOURCES = \
+	src/su/su.c
+
+systemd_su_CFLAGS = \
+	$(AM_CFLAGS)
+
+systemd_su_LDADD = \
+	libsystemd-label.la \
+	libsystemd-capability.la \
+	libsystemd-bus-internal.la \
+	libsystemd-login-internal.la \
+	libsystemd-daemon-internal.la \
+	libsystemd-id128-internal.la \
+	libsystemd-shared.la
+
+systemd_suexec_SOURCES = \
+	src/su/suexec.c \
+	src/shared/session-pam.h \
+	src/shared/session-pam.c
+
+systemd_suexec_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(PAM_CLFAGS)
+
+systemd_suexec_LDADD = \
+	$(PAM_LIBS) \
+	libsystemd-login-internal.la \
+	libsystemd-daemon-internal.la \
+	libsystemd-shared.la
+endif
+
+# ------------------------------------------------------------------------------
 systemd_bus_proxyd_SOURCES = \
 	src/bus-proxyd/bus-proxyd.c
 
diff --git a/src/shared/session-pam.c b/src/shared/session-pam.c
new file mode 100644
index 0000000..ce491ad
--- /dev/null
+++ b/src/shared/session-pam.c
@@ -0,0 +1,394 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2013 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd 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.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <security/pam_appl.h>
+#include <security/pam_misc.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "macro.h"
+#include "session-pam.h"
+#include "util.h"
+
+typedef int (*session_pam_t) (const struct pam_message *msg,
+                              struct pam_response *resp,
+                              void *data);
+
+int session_pam_foreach(int msg_count,
+                        const struct pam_message **msg,
+                        struct pam_response **resp,
+                        session_pam_t event_fn,
+                        void *data) {
+        struct pam_response *rs = NULL;
+        int i, pamr;
+
+        if (msg_count < 0)
+                return PAM_CONV_ERR;
+
+        if (!msg_count) {
+                if (resp)
+                        *resp = NULL;
+
+                return PAM_SUCCESS;
+        }
+
+        /* If we have no conversation-function, we fail if the caller expects
+         * responses from us; otherwise, we simply ignore all requests. */
+        if (!event_fn)
+                return resp ? PAM_CONV_ERR : PAM_SUCCESS;
+
+        if (resp) {
+                rs = calloc(msg_count, sizeof(struct pam_response));
+                if (!rs) {
+                        log_oom();
+                        return PAM_BUF_ERR;
+                }
+        }
+
+        for (i = 0; i < msg_count; ++i) {
+                pamr = event_fn(msg[i], rs ? &rs[i] : NULL, data);
+                if (pamr != PAM_SUCCESS)
+                        goto error;
+        }
+
+        if (resp)
+                *resp = rs;
+
+        return PAM_SUCCESS;
+
+error:
+        if (rs) {
+                for (i = 0; i < msg_count; ++i) {
+                        if (rs[i].resp) {
+                                /* clear potential passwords */
+                                memzero(rs[i].resp, strlen(rs[i].resp));
+                                free(rs[i].resp);
+                        }
+                }
+                free(rs);
+        }
+
+        return pamr;
+}
+
+static int session_pam_fallback_fn(int msg_count,
+                                   const struct pam_message **msg,
+                                   struct pam_response **resp,
+                                   void *data) {
+        return session_pam_foreach(msg_count, msg, resp, NULL, NULL);
+}
+
+static int session_pam_setenv(pam_handle_t *pamh, const char *key, const char *value, int override) {
+        int r;
+
+        assert(pamh);
+        assert(key);
+
+        if (isempty(value)) {
+                unsetenv(key);
+                return 0;
+        }
+
+        r = setenv(key, value, override);
+        if (r < 0) {
+                r = -errno;
+                if (r == -ENOMEM) {
+                        return log_oom();
+                } else {
+                        log_error("session-pam: setenv(%s, %s, %d): %m", key, value, override);
+                        return r;
+                }
+        }
+
+        r = pam_misc_setenv(pamh, key, value, !override);
+        if (r < 0) {
+                if (r == PAM_BUF_ERR) {
+                        return log_oom();
+                } else {
+                        log_error("session-pam: pam_misc_setenv(%s, %s, %d): %s",
+                                  key, value, override, pam_strerror(pamh, r));
+                        return -EINVAL;
+                }
+        }
+
+        return 0;
+}
+
+static int session_pam_init_env(pam_handle_t *pamh,
+                                const char *seat,
+                                unsigned int vtnr,
+                                const char *session_class) {
+        char buf[128];
+        int r;
+
+        if (streq_ptr(seat, "seat0") && vtnr > 0) {
+                sprintf(buf, "%u", vtnr);
+                r = session_pam_setenv(pamh, "XDG_VTNR", buf, 1);
+                if (r < 0)
+                        return r;
+        } else {
+                unsetenv("XDG_VTNR");
+        }
+
+        r = session_pam_setenv(pamh, "XDG_SEAT", seat, 1);
+        if (r < 0)
+                return r;
+
+        r = session_pam_setenv(pamh, "XDG_SESSION_CLASS", session_class, 1);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int session_pam_init_creds(pam_handle_t *pamh,
+                                  const char *username,
+                                  gid_t gid,
+                                  const char *home,
+                                  const char *shell) {
+        int r, i;
+        char **env;
+
+        r = setgid(gid);
+        if (r < 0) {
+                r = -errno;
+                log_error("session-pam: setgid(%d): %m", (int)gid);
+                return r;
+        }
+
+        env = calloc(1, sizeof(*env));
+        if (!env)
+                return log_oom();
+
+        environ = env;
+
+        setenv("HOME", home, 1);
+        setenv("PATH", "/usr/bin", 1);
+        setenv("SHELL", shell, 1);
+        setenv("USER", username, 1);
+
+        env = pam_getenvlist(pamh);
+        if (!env) {
+                log_error("session-pam: pam_getenvlist() failed");
+        } else {
+                for (i = 0; env[i]; ++i) {
+                        r = putenv(env[i]);
+                        if (r < 0)
+                                log_warning("session-pam: putenv(%s): %m", env[i]);
+                }
+                free(env);
+        }
+
+        return 0;
+}
+
+static int session_pam_dance(pam_handle_t *pamh,
+                             bool silent,
+                             const char **username,
+                             uid_t *uid_out,
+                             const char **home_out) {
+        const char *shell, *home;
+        gid_t gid;
+        uid_t uid;
+        int pamr, r;
+        int pam_silent = silent ? PAM_SILENT : 0;
+
+        pamr = pam_authenticate(pamh, pam_silent);
+        if (pamr != PAM_SUCCESS) {
+                log_error("session-pam: cannot authenticate '%s': %s",
+                          *username, pam_strerror(pamh, pamr));
+                return pamr;
+        }
+
+        pamr = pam_acct_mgmt(pamh, pam_silent | PAM_DISALLOW_NULL_AUTHTOK);
+        if (pamr == PAM_NEW_AUTHTOK_REQD) {
+                log_info("session-pam: auth-token expired for '%s'",
+                         *username);
+
+                pamr = pam_chauthtok(pamh, pam_silent | PAM_CHANGE_EXPIRED_AUTHTOK);
+                if (pamr != PAM_SUCCESS) {
+                        log_error("session-pam: cannot extend auth-token for '%s': %s",
+                                  *username, pam_strerror(pamh, pamr));
+                        return pamr;
+                }
+        } else if (pamr != PAM_SUCCESS) {
+                log_error("session-pam: invalid account '%s': %s",
+                          *username, pam_strerror(pamh, pamr));
+                return pamr;
+        }
+
+        /* retrieve credentials *after* pam_acct_mgmt() and friends */
+        r = get_user_creds(username, &uid, &gid, &home, &shell);
+        if (r < 0) {
+                log_error("session-pam: cannot get credentials for '%s': %d",
+                          *username, r);
+                return PAM_SYSTEM_ERR;
+        }
+
+        /* Init groups *before* pam_setcred(), but gid/pid afterwards so
+         * pam-modules can add groups. */
+        if (uid)
+                r = initgroups(*username, gid);
+        else
+                r = setgroups(0, NULL);
+        if (r < 0) {
+                log_error("session-pam: cannot initialize groups for '%s': %m",
+                          *username);
+                return PAM_SYSTEM_ERR;
+        }
+
+        pamr = pam_setcred(pamh, pam_silent | PAM_ESTABLISH_CRED);
+        if (pamr != PAM_SUCCESS) {
+                log_error("session-pam: cannot establish pam credentials for '%s': %s",
+                          *username, pam_strerror(pamh, pamr));
+                return pamr;
+        }
+
+        pamr = pam_open_session(pamh, pam_silent);
+        if (pamr != PAM_SUCCESS) {
+                log_error("session-pam: cannot open new session for '%s': %s",
+                          *username, pam_strerror(pamh, pamr));
+                return pam_setcred(pamh, pam_silent | PAM_DELETE_CRED);
+        }
+
+        pamr = pam_setcred(pamh, pam_silent | PAM_REINITIALIZE_CRED);
+        if (pamr < 0) {
+                log_error("session-pam: cannot re-initialize pam credentials for '%s': %s",
+                          *username, pam_strerror(pamh, pamr));
+                pam_setcred(pamh, pam_silent | PAM_DELETE_CRED);
+                return pam_close_session(pamh, pam_silent);
+        }
+
+        r = session_pam_init_creds(pamh, *username, gid, home, shell);
+        if (r < 0) {
+                pam_setcred(pamh, pam_silent | PAM_DELETE_CRED);
+                return pam_close_session(pamh, pam_silent);
+        }
+
+        *uid_out = uid;
+        *home_out = home;
+        return 0;
+}
+
+int session_pam_open(const char *seat,
+                     unsigned int vtnr,
+                     const char *session_class,
+                     const char *pam_service,
+                     const char **username,
+                     bool silent,
+                     const struct pam_conv *conv,
+                     pam_handle_t **pam_out,
+                     uid_t *uid_out,
+                     const char **home_out) {
+        static const struct pam_conv pconv = { session_pam_fallback_fn, NULL };
+        pam_handle_t *pamh = NULL;
+        const char *home;
+        int pamr, r;
+        uid_t uid;
+
+        assert(pam_out);
+        assert(!isempty(pam_service));
+        assert(username && !isempty(*username));
+
+        if (!conv)
+                conv = &pconv;
+
+        pamr = pam_start(pam_service, *username, conv, &pamh);
+        if (pamr != PAM_SUCCESS) {
+                log_error("session-pam: cannot initiate pam-transaction for '%s': %s",
+                          *username, pam_strerror(pamh, pamr));
+                return -EINVAL;
+        }
+
+        r = session_pam_init_env(pamh, seat, vtnr, session_class);
+        if (r < 0) {
+                pam_end(pamh, PAM_SUCCESS);
+                return r;
+        }
+
+        pamr = session_pam_dance(pamh, silent, username, &uid, &home);
+        if (pamr < 0) {
+                pam_end(pamh, pamr);
+                return -EINVAL;
+        }
+
+        *pam_out = pamh;
+        if (uid_out)
+                *uid_out = uid;
+        if (home_out)
+                *home_out = home;
+
+        return 0;
+}
+
+int session_pam_finalize_creds(uid_t uid, const char *home) {
+        int r;
+
+        assert(!isempty(home));
+
+        /* loose CTTY */
+        r = setsid();
+        if (r < 0 && errno != EPERM)
+                log_warning("session-pam: setsid(): %m");
+
+        /* change to "/" in case chdir(home) fails after setuid() */
+        chdir("/");
+
+        r = setuid(uid);
+        if (r < 0) {
+                r = -errno;
+                log_error("session-pam: setuid(%d): %m", (int)uid);
+                return r;
+        }
+
+        r = chdir(home);
+        if (r < 0)
+                log_warning("session-pam: chdir(%s): %m", home);
+
+        return 0;
+}
+
+void session_pam_yield(void) {
+        pid_t pid;
+
+        do {
+                pid = wait(NULL);
+        } while (pid < 0 && errno == EINTR);
+}
+
+void session_pam_close(pam_handle_t *pamh, bool silent) {
+        int r, pam_silent = silent ? PAM_SILENT : 0;
+
+        assert(pamh);
+
+        pam_setcred(pamh, pam_silent);
+        r = pam_close_session(pamh, pam_silent);
+        pam_end(pamh, r);
+}
diff --git a/src/shared/session-pam.h b/src/shared/session-pam.h
new file mode 100644
index 0000000..5a5b2f4
--- /dev/null
+++ b/src/shared/session-pam.h
@@ -0,0 +1,53 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+#pragma once
+
+/***
+  This file is part of systemd.
+
+  Copyright 2013 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd 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.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <errno.h>
+#include <security/pam_appl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <sys/types.h>
+
+#include "util.h"
+
+typedef int (*session_pam_t) (const struct pam_message *msg,
+                              struct pam_response *resp,
+                              void *data);
+
+int session_pam_foreach(int msg_count,
+                        const struct pam_message **msg,
+                        struct pam_response **resp,
+                        session_pam_t event_fn,
+                        void *data);
+int session_pam_open(const char *seat,
+                     unsigned int vtnr,
+                     const char *session_class,
+                     const char *pam_service,
+                     const char **username,
+                     bool silent,
+                     const struct pam_conv *conv,
+                     pam_handle_t **pam_out,
+                     uid_t *uid_out,
+                     const char **home_out);
+int session_pam_finalize_creds(uid_t uid, const char *home);
+void session_pam_yield(void);
+void session_pam_close(pam_handle_t *pamh, bool silent);
diff --git a/src/su/Makefile b/src/su/Makefile
new file mode 120000
index 0000000..d0b0e8e
--- /dev/null
+++ b/src/su/Makefile
@@ -0,0 +1 @@
+../Makefile
\ No newline at end of file
diff --git a/src/su/su.c b/src/su/su.c
new file mode 100644
index 0000000..fe36dd3
--- /dev/null
+++ b/src/su/su.c
@@ -0,0 +1,429 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2013 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd 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.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/un.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "build.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "def.h"
+#include "log.h"
+#include "macro.h"
+#include "missing.h"
+#include "path-util.h"
+#include "sd-bus.h"
+#include "sd-daemon.h"
+#include "sd-login.h"
+#include "session-pam.h"
+#include "strv.h"
+#include "util.h"
+
+static const char *arg_seat = "";
+static const char *arg_user;
+static const char *arg_log_level = "info";
+static const char *arg_log_target = "journal";
+
+static int prepare_pipe(const char *path) {
+        struct sockaddr_un addr = { };
+        int fd, r;
+
+        fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
+        if (fd < 0) {
+                r = -errno;
+                log_error("socket(): %m");
+                return r;
+        }
+
+        addr.sun_family = AF_UNIX;
+        strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
+        if (*path == '@')
+                addr.sun_path[0] = 0;
+
+        r = bind(fd, (void*)&addr, sizeof(addr));
+        if (r < 0) {
+                r = -errno;
+                log_error("bind(%s): %m", path);
+                close_nointr_nofail(fd);
+                return r;
+        }
+
+        r = listen(fd, 10);
+        if (r < 0) {
+                r = -errno;
+                log_error("listen(): %m");
+                close_nointr_nofail(fd);
+                return r;
+        }
+
+        return fd;
+}
+
+static void finalize_pipe(int fd) {
+        char control[CMSG_SPACE(sizeof(int) * 3)];
+        struct cmsghdr *cmsg;
+        struct msghdr msg = { };
+        struct iovec iov = { };
+        uint8_t content = 0;
+        ssize_t l;
+        int client;
+
+        client = accept(fd, NULL, NULL);
+        if (client < 0) {
+                log_error("accept(): %m");
+                close_nointr_nofail(fd);
+                return;
+        }
+
+        iov.iov_base = &content;
+        iov.iov_len = sizeof(content);
+        msg.msg_iov = &iov;
+        msg.msg_iovlen = 1;
+        msg.msg_control = control;
+        msg.msg_controllen = sizeof(control);
+        cmsg = CMSG_FIRSTHDR(&msg);
+        cmsg->cmsg_level = SOL_SOCKET;
+        cmsg->cmsg_type = SCM_RIGHTS;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(int) * 3);
+        ((int*)CMSG_DATA(cmsg))[0] = 0;
+        ((int*)CMSG_DATA(cmsg))[1] = 1;
+        ((int*)CMSG_DATA(cmsg))[2] = 2;
+        msg.msg_controllen = cmsg->cmsg_len;
+
+        l = sendmsg(client, &msg, MSG_NOSIGNAL);
+        if (l != sizeof(content))
+                log_error("sendmsg(): %m");
+
+        close_nointr_nofail(client);
+        close_nointr_nofail(fd);
+}
+
+static int message_start_transient_unit_new(sd_bus *bus, const char *name, sd_bus_message **ret) {
+        _cleanup_bus_message_unref_ sd_bus_message *m = NULL;
+        int r;
+
+        assert(bus);
+        assert(name);
+        assert(ret);
+
+        log_info("Running as unit %s.", name);
+
+        r = sd_bus_message_new_method_call(
+                        bus,
+                        "org.freedesktop.systemd1",
+                        "/org/freedesktop/systemd1",
+                        "org.freedesktop.systemd1.Manager",
+                        "StartTransientUnit", &m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "ss", name, "fail");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(m, 'a', "(sv)");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "(sv)", "Description", "s", "systemd-su");
+        if (r < 0)
+                return r;
+
+        *ret = m;
+        m = NULL;
+
+        return 0;
+}
+
+static int message_start_transient_unit_send(sd_bus *bus, sd_bus_message *m, sd_bus_error *error, sd_bus_message **reply) {
+        int r;
+
+        assert(bus);
+        assert(m);
+
+        r = sd_bus_message_close_container(m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "a(sa(sv))", 0);
+        if (r < 0)
+                return r;
+
+        return sd_bus_call(bus, m, 0, error, reply);
+}
+
+static int start_unit(sd_bus *bus, char **argv, const char *ttypipe, sd_bus_error *error) {
+        const char *cmd[] = {
+                getenv("SYSTEMD_SUEXEC") ? : "/usr/lib/systemd/systemd-suexec",
+                "--log-level",
+                arg_log_level,
+                "--log-target",
+                arg_log_target,
+                "--seat",
+                arg_seat,
+                "--user",
+                arg_user,
+                "--ttypipe",
+                ttypipe,
+                "--",
+                NULL
+        };
+        _cleanup_bus_message_unref_ sd_bus_message *m = NULL;
+        _cleanup_free_ char *name = NULL;
+        char **i;
+        int r;
+
+        asprintf(&name, "su-%lu.service", (unsigned long) getpid());
+        if (!name)
+                return -ENOMEM;
+
+        r = message_start_transient_unit_new(bus, name, &m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(m, 'r', "sv");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "s", "ExecStart");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(m, 'v', "a(sasb)");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(m, 'a', "(sasb)");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(m, 'r', "sasb");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "s", cmd[0]);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(m, 'a', "s");
+        if (r < 0)
+                return r;
+
+        for (i = (char**)cmd; *i; ++i) {
+                r = sd_bus_message_append(m, "s", *i);
+                if (r < 0)
+                        return r;
+        }
+
+        STRV_FOREACH(i, argv) {
+                r = sd_bus_message_append(m, "s", *i);
+                if (r < 0)
+                        return r;
+        }
+
+        r = sd_bus_message_close_container(m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "b", false);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_close_container(m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_close_container(m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_close_container(m);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_close_container(m);
+        if (r < 0)
+                return r;
+
+        return message_start_transient_unit_send(bus, m, error, NULL);
+}
+
+static void wait_unit(sd_bus *bus) {
+        /* TODO: We should register a PropertiesChanged dbus-match here and wait
+         * for the unit to exit. Any volunteers? */
+        log_error("TODO: waiting not implemented, sleeping instead..");
+        sleep(10000);
+}
+
+static int spawn_cmd(sd_bus *bus, char **argv) {
+        _cleanup_bus_error_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
+        char buf[64];
+        int pipefd, r;
+
+        snprintf(buf, sizeof(buf) - 1, "@/org/freedesktop/systemd/su/%d", getpid());
+        pipefd = prepare_pipe(buf);
+        if (pipefd < 0)
+                return pipefd;
+
+        r = start_unit(bus, argv, buf, &error);
+        if (r < 0) {
+                log_error("Failed start transient unit: %s", bus_error_message(&error, r));
+                close_nointr_nofail(pipefd);
+                return r;
+        }
+
+        finalize_pipe(pipefd);
+        wait_unit(bus);
+
+        return 0;
+}
+
+static int help(void) {
+        printf("%s [OPTIONS...] COMMAND [ARGS...]\n\n"
+               "Run command as another user in a new session and fresh environment.\n\n"
+               "  -h --help                  Show this help\n"
+               "     --version               Show package version\n"
+               "  -s --seat=SEAT             Seat the command should run on\n"
+               "  -u --user=NAME             Run command as specified username or UID\n"
+               "     --log-level=LEVEL       Set log level\n"
+               "     --log-target=TARGET     Set log target\n"
+               , program_invocation_short_name);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_LOG_LEVEL,
+                ARG_LOG_TARGET,
+        };
+        static const struct option options[] = {
+                { "help",         no_argument,       NULL, 'h'              },
+                { "version",      no_argument,       NULL, ARG_VERSION      },
+                { "seat",         required_argument, NULL, 's'              },
+                { "user",         required_argument, NULL, 'u'              },
+                { "log-level",    required_argument, NULL, ARG_LOG_LEVEL    },
+                { "log-target",   required_argument, NULL, ARG_LOG_TARGET   },
+                {}
+        };
+        int c, r;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "hu:", options, NULL)) >= 0) {
+                switch(c) {
+                case 'h':
+                        return help();
+                case ARG_VERSION:
+                        puts(PACKAGE_STRING);
+                        puts(SYSTEMD_FEATURES);
+                        return 0;
+                case 's':
+                        arg_seat = optarg;
+                        break;
+                case 'u':
+                        arg_user = optarg;
+                        break;
+                case ARG_LOG_LEVEL:
+                        if ((r = log_set_max_level_from_string(optarg)) < 0) {
+                                log_error("Failed to parse log level %s.", optarg);
+                                return r;
+                        }
+                        arg_log_level = optarg;
+                        break;
+                case ARG_LOG_TARGET:
+                        if ((r = log_set_target_from_string(optarg)) < 0) {
+                                log_error("Failed to parse log target %s.", optarg);
+                                return r;
+                        }
+                        arg_log_target = optarg;
+                        break;
+                case '?':
+                        return -EINVAL;
+                default:
+                        assert_not_reached("Unhandled option");
+                }
+        }
+
+        if (optind >= argc) {
+                log_error("Command line to execute required.");
+                return -EINVAL;
+        }
+
+        if (isempty(arg_user)) {
+                log_error("Username or UID required (--user=NAME).");
+                return -EINVAL;
+        }
+
+        return 1;
+}
+
+int main(int argc, char *argv[]) {
+        _cleanup_bus_unref_ sd_bus *bus = NULL;
+        _cleanup_free_ char *cmd = NULL;
+        int r;
+
+        log_set_target(LOG_TARGET_AUTO);
+        log_parse_environment();
+        log_open();
+
+        r = parse_argv(argc, argv);
+        if (r < 0)
+                return EXIT_FAILURE;
+        if (r == 0)
+                return EXIT_SUCCESS;
+
+        if (!logind_running()) {
+                log_error("systemd-logind is required to be running.");
+                return EXIT_FAILURE;
+        }
+
+        r = find_binary(argv[optind], &cmd);
+        if (r < 0) {
+                log_error("Failed to find executable %s: %s",
+                          argv[optind], strerror(-r));
+                return EXIT_FAILURE;
+        }
+        argv[optind] = cmd;
+
+        r = sd_bus_open_system(&bus);
+        if (r < 0) {
+                log_error("Failed to create bus connection: %s", strerror(-r));
+                return EXIT_FAILURE;
+        }
+
+        r = spawn_cmd(bus, argv + optind);
+
+        return abs(r);
+}
diff --git a/src/su/suexec.c b/src/su/suexec.c
new file mode 100644
index 0000000..bdf4c76
--- /dev/null
+++ b/src/su/suexec.c
@@ -0,0 +1,377 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2013 David Herrmann <dh.herrmann at gmail.com>
+
+  systemd 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.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/un.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "build.h"
+#include "def.h"
+#include "log.h"
+#include "macro.h"
+#include "missing.h"
+#include "path-util.h"
+#include "sd-daemon.h"
+#include "sd-login.h"
+#include "session-pam.h"
+#include "util.h"
+
+static const char *arg_seat;
+static const char *arg_user;
+static const char *arg_ttypipe;
+static pid_t child_pid;
+
+static int get_fds(void) {
+        char control[CMSG_SPACE(sizeof(int) * 3)];
+        char buf[128] = { };
+        struct cmsghdr *cmsg;
+        struct msghdr msg = { };
+        struct iovec iov = { };
+        ssize_t l, j;
+        struct sockaddr_un addr = { };
+        int fd, r, *p, i;
+
+        if (isempty(arg_ttypipe))
+                return 0;
+
+        fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
+        if (fd < 0) {
+                r = -errno;
+                log_error("socket(): %m");
+                return r;
+        }
+
+        addr.sun_family = AF_UNIX;
+        strncpy(addr.sun_path, arg_ttypipe, sizeof(addr.sun_path) - 1);
+        if (*arg_ttypipe == '@')
+                addr.sun_path[0] = 0;
+
+        /* wait at most 3 seconds */
+        alarm(3);
+
+        r = connect(fd, &addr, sizeof(addr));
+        if (r < 0) {
+                r = -errno;
+                log_error("Cannot open TTY-Pipe '%s': %m", arg_ttypipe);
+                goto error;
+        }
+
+        iov.iov_base = buf;
+        iov.iov_len = sizeof(buf);
+        msg.msg_iov = &iov;
+        msg.msg_iovlen = 1;
+        msg.msg_control = control;
+        msg.msg_controllen = sizeof(control);
+
+        log_debug("Waiting for TTY-fd from '%s'..", arg_ttypipe);
+        do {
+                /* no MSG_CMSG_CLOEXEC as we want our childs to inherit them */
+                l = recvmsg(fd, &msg, 0);
+        } while (l < 0 && errno == EINTR);
+
+        if (l <= 0) {
+                r = -errno;
+                log_error("Error on TTY-Pipe '%s': %m", arg_ttypipe);
+                goto error;
+        } else if (l != 1) {
+                r = -EINVAL;
+                log_error("Invalid TTY-Pipe '%s': %m", arg_ttypipe);
+                goto error;
+        }
+
+        i = 0;
+        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+                if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
+                        continue;
+
+                p = (int*)CMSG_DATA(cmsg);
+                l = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+                for (j = 0; j < l; ++j, ++p) {
+                        r = dup3(*p, i, 0);
+                        if (r < 0)
+                                log_warning("Cannot retrieve FD %d", i);
+                        if (r < 0 || *p != i)
+                                close_nointr_nofail(*p);
+                        ++i;
+                }
+        }
+
+        alarm(0);
+        r = 0;
+
+error:
+        close_nointr(fd);
+        return r;
+}
+
+static int run_command(char **argv) {
+        sigset_t mask;
+
+        reset_all_signal_handlers();
+        sigemptyset(&mask);
+        sigprocmask(SIG_SETMASK, &mask, NULL);
+
+        execve(argv[0], argv, environ);
+        log_error("execve(): %m");
+
+        return -errno;
+}
+
+static void handle_signal(int sig) {
+        if (child_pid) {
+                log_debug("Caught signal %d, forwarding..", sig);
+                kill(-child_pid, sig);
+        }
+}
+
+static int fork_session(char **argv, pam_handle_t **pamh_out) {
+        struct sigaction sa = {
+                .sa_handler = handle_signal,
+                .sa_flags = SA_RESTART,
+        };
+        int r;
+        uid_t uid;
+        pid_t pid;
+        const char *home;
+        pam_handle_t *pamh;
+
+        /* Ignore all signals in the parent process. We reset them for the
+         * client, anyway. But this guarantees that our parent-waiter will
+         * stay around till the session dies or someone sends SIGKILL. */
+        r = sigaction_many(&sa, SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGTERM, SIGUSR1, SIGUSR2, 0);
+        if (r < 0) {
+                log_error("Cannot setup signal handlers: %d", r);
+                return r;
+        }
+
+        r = session_pam_open(arg_seat,          /* seat */
+                             5,                 /* VT (TODO) */
+                             "background",      /* background session */
+                             "systemd-suexec",  /* custom PAM-service */
+                             &arg_user,         /* username to run as */
+                             0,                 /* don't use PAM_SILENT */
+                             NULL,              /* no custom pam-conv */
+                             &pamh,
+                             &uid,
+                             &home);
+        if (r < 0)
+                return r;
+
+        pid = fork();
+        if (pid < 0) {
+                r = -errno;
+                log_error("fork(): %m");
+                session_pam_close(pamh, 0);
+                return r;
+        } else if (pid > 0) {
+                /* parent */
+                child_pid = pid;
+                *pamh_out = pamh;
+                return 0;
+        }
+
+        /* child */
+
+        r = get_fds();
+        if (r < 0)
+                _exit(r);
+
+        /* TODO: Add --ctty switch and call TIOCSCTTY if requested. However,
+         * for this to work, the systemd-su caller needs to restore it's own
+         * CTTY after return. We cannot share it as we have different
+         * session-IDs. */
+        /*
+        if (ctty_fd >= 0) {
+                r = setsid();
+                if (r < 0 && errno != EPERM)
+                        log_warning("setsid(): %m");
+
+                r = ioctl(ctty_fd, TIOCSCTTY, 1);
+                if (r < 0)
+                        log_warning("TIOCSCTTY(): %m");
+        }
+        */
+
+        r = session_pam_finalize_creds(uid, home);
+        if (r < 0)
+                _exit(r);
+
+        exit(run_command(argv));
+}
+
+static void yield_session(pam_handle_t *pamh) {
+        session_pam_yield();
+        child_pid = 0;
+        session_pam_close(pamh, 0);
+        log_debug("Session closed");
+}
+
+static int help(void) {
+        printf("%s [OPTIONS...] COMMAND [ARGS...]\n\n"
+               "Execute command in a new session.\n\n"
+               "  -h --help                  Show this help\n"
+               "     --version               Show package version\n"
+               "  -s --seat=SEAT             Seat to run command on\n"
+               "  -u --user=NAME             Run command as specified username or UID\n"
+               "  -t --ttypipe=PATH          Path to UNIX socket to retrieve TTY\n"
+               "     --log-level=LEVEL       Set log level\n"
+               "     --log-target=TARGET     Set log target\n"
+               , program_invocation_short_name);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_LOG_LEVEL,
+                ARG_LOG_TARGET,
+        };
+        static const struct option options[] = {
+                { "help",         no_argument,       NULL, 'h'              },
+                { "version",      no_argument,       NULL, ARG_VERSION      },
+                { "seat",         required_argument, NULL, 's'              },
+                { "user",         required_argument, NULL, 'u'              },
+                { "ttypipe",      required_argument, NULL, 't'              },
+                { "log-level",    required_argument, NULL, ARG_LOG_LEVEL    },
+                { "log-target",   required_argument, NULL, ARG_LOG_TARGET   },
+                {}
+        };
+        int c, r;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "hu:", options, NULL)) >= 0) {
+                switch(c) {
+                case 'h':
+                        return help();
+                case ARG_VERSION:
+                        puts(PACKAGE_STRING);
+                        puts(SYSTEMD_FEATURES);
+                        return 0;
+                case 's':
+                        arg_seat = optarg;
+                        break;
+                case 'u':
+                        arg_user = optarg;
+                        break;
+                case 't':
+                        arg_ttypipe = optarg;
+                        break;
+                case ARG_LOG_LEVEL:
+                        if ((r = log_set_max_level_from_string(optarg)) < 0) {
+                                log_error("Failed to parse log level %s.", optarg);
+                                return r;
+                        }
+                        break;
+                case ARG_LOG_TARGET:
+                        if ((r = log_set_target_from_string(optarg)) < 0) {
+                                log_error("Failed to parse log target %s.", optarg);
+                                return r;
+                        }
+                        break;
+                case '?':
+                        return -EINVAL;
+                default:
+                        assert_not_reached("Unhandled option");
+                }
+        }
+
+        if (optind >= argc) {
+                log_error("Command line to execute required.");
+                return -EINVAL;
+        }
+
+        if (isempty(arg_user)) {
+                log_error("Username or UID required (--user=NAME).");
+                return -EINVAL;
+        }
+
+        return 1;
+}
+
+int main(int argc, char *argv[]) {
+        _cleanup_free_ char *cmd = NULL;
+        pam_handle_t *pamh;
+        char *sid;
+        int r, ttyfd = -1;
+
+        log_set_target(LOG_TARGET_AUTO);
+        log_parse_environment();
+        log_open();
+
+        r = parse_argv(argc, argv);
+        if (r < 0)
+                return EXIT_FAILURE;
+        if (r == 0)
+                return EXIT_SUCCESS;
+
+        if (geteuid() > 0) {
+                log_error("Need to be root.");
+                return EXIT_FAILURE;
+        }
+
+        if (!logind_running()) {
+                log_error("systemd-logind is required to be running.");
+                return EXIT_FAILURE;
+        }
+
+        if (sd_pid_get_session(getpid(), &sid) >= 0) {
+                log_error("You cannot run this from within an existing session (sid: %s).", sid);
+                free(sid);
+                return EXIT_FAILURE;
+        }
+
+        r = find_binary(argv[optind], &cmd);
+        if (r < 0) {
+                log_error("Failed to find executable %s: %s",
+                          argv[optind], strerror(-r));
+                return EXIT_FAILURE;
+        }
+        argv[optind] = cmd;
+
+        r = fork_session(argv + optind, &pamh);
+        if (r < 0)
+                goto finish;
+
+        sd_notify(false, "READY=1\nSTATUS=Running...");
+
+        yield_session(pamh);
+
+finish:
+        sd_notify(false, "STATUS=Shutting down...");
+
+        if (ttyfd > 0)
+                close_nointr(ttyfd);
+
+        return abs(r);
+}
-- 
1.8.4.2



More information about the systemd-devel mailing list