/* * Session process wrapper * Copyright (C) 2015 Pier Luigi Fiorini * Copyright (C) 2014 Martin Bříza * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ #include #include "Configuration.h" #include "Constants.h" #include "UserSession.h" #include "HelperApp.h" #include "VirtualTerminal.h" #include "XAuth.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_FREEBSD #include #endif namespace SDDM { UserSession::UserSession(HelperApp *parent) : QProcess(parent) { connect(this, QOverload::of(&QProcess::finished), this, &UserSession::finished); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) setChildProcessModifier(std::bind(&UserSession::childModifier, this)); #endif } bool UserSession::start() { auto helper = qobject_cast(parent()); QProcessEnvironment env = processEnvironment(); bool isWaylandGreeter = false; // If the Xorg display server was already started, write the passed // auth cookie to /tmp/xauth_XXXXXX. This is done in the parent process // so that it can clean up the file on session end. if (env.value(QStringLiteral("XDG_SESSION_TYPE")) == QLatin1String("x11") && m_displayServerCmd.isEmpty()) { // Create the Xauthority file QByteArray cookie = helper->cookie(); if (cookie.isEmpty()) { qCritical() << "Can't start X11 session with empty auth cookie"; return false; } // Place it into /tmp, which is guaranteed to be read/writeable by // everyone while having the sticky bit set to avoid messing with // other's files. m_xauthFile.setFileTemplate(QStringLiteral("/tmp/xauth_XXXXXX")); if (!m_xauthFile.open()) { qCritical() << "Could not create the Xauthority file"; return false; } QString display = processEnvironment().value(QStringLiteral("DISPLAY")); if (!XAuth::writeCookieToFile(display, m_xauthFile.fileName(), cookie)) { qCritical() << "Failed to write the Xauthority file"; m_xauthFile.close(); return false; } env.insert(QStringLiteral("XAUTHORITY"), m_xauthFile.fileName()); setProcessEnvironment(env); } if (env.value(QStringLiteral("XDG_SESSION_TYPE")) == QLatin1String("x11")) { QString command; if (env.value(QStringLiteral("XDG_SESSION_CLASS")) == QLatin1String("greeter")) { command = m_path; } else { command = QStringLiteral("%1 \"%2\"").arg(mainConfig.X11.SessionCommand.get()).arg(m_path); } qInfo() << "Starting X11 session:" << m_displayServerCmd << command; if (m_displayServerCmd.isEmpty()) { auto args = QProcess::splitCommand(command); setProgram(args.takeFirst()); setArguments(args); } else { setProgram(QStringLiteral(LIBEXEC_INSTALL_DIR "/sddm-helper-start-x11user")); setArguments({m_displayServerCmd, command}); } QProcess::start(); } else if (env.value(QStringLiteral("XDG_SESSION_TYPE")) == QLatin1String("wayland")) { if (env.value(QStringLiteral("XDG_SESSION_CLASS")) == QLatin1String("greeter")) { Q_ASSERT(!m_displayServerCmd.isEmpty()); setProgram(QStringLiteral(LIBEXEC_INSTALL_DIR "/sddm-helper-start-wayland")); setArguments({m_displayServerCmd, m_path}); QProcess::start(); isWaylandGreeter = true; } else { setProgram(mainConfig.Wayland.SessionCommand.get()); setArguments(QStringList{m_path}); qInfo() << "Starting Wayland user session:" << program() << m_path; QProcess::start(); closeWriteChannel(); closeReadChannel(QProcess::StandardOutput); } } else { qCritical() << "Unable to run user session: unknown session type"; } const bool started = waitForStarted(); m_cachedProcessId = processId(); if (started) { return true; } else if (isWaylandGreeter) { // This is probably fine, we need the compositor to start first return true; } return false; } void UserSession::stop() { if (state() != QProcess::NotRunning) { terminate(); const bool isGreeter = processEnvironment().value(QStringLiteral("XDG_SESSION_CLASS")) == QLatin1String("greeter"); // Wait longer for a session than a greeter if (!waitForFinished(isGreeter ? 5000 : 60000)) { kill(); if (!waitForFinished(5000)) { qWarning() << "Could not fully finish the process" << program(); } } } else { Q_EMIT finished(Auth::HELPER_OTHER_ERROR); } } QString UserSession::displayServerCommand() const { return m_displayServerCmd; } void UserSession::setDisplayServerCommand(const QString &command) { m_displayServerCmd = command; } void UserSession::setPath(const QString& path) { m_path = path; } QString UserSession::path() const { return m_path; } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void UserSession::childModifier() { #else void UserSession::setupChildProcess() { #endif // Session type QString sessionType = processEnvironment().value(QStringLiteral("XDG_SESSION_TYPE")); QString sessionClass = processEnvironment().value(QStringLiteral("XDG_SESSION_CLASS")); const bool hasDisplayServer = !m_displayServerCmd.isEmpty(); const bool x11UserSession = sessionType == QLatin1String("x11") && sessionClass == QLatin1String("user"); const bool waylandUserSession = sessionType == QLatin1String("wayland") && sessionClass == QLatin1String("user"); // When the display server is part of the session, we leak the VT into // the session as stdin so that it stays open without races if (hasDisplayServer || waylandUserSession) { // open VT and get the fd int vtNumber = processEnvironment().value(QStringLiteral("XDG_VTNR")).toInt(); QString ttyString = VirtualTerminal::path(vtNumber); int vtFd = ::open(qPrintable(ttyString), O_RDWR | O_NOCTTY); // when this is true we'll take control of the tty bool takeControl = false; if (vtNumber > 0 && vtFd > 0) { dup2(vtFd, STDIN_FILENO); ::close(vtFd); takeControl = true; } else { int stdinFd = ::open("/dev/null", O_RDWR); dup2(stdinFd, STDIN_FILENO); ::close(stdinFd); } // set this process as session leader if (setsid() < 0) { qCritical("Failed to set pid %lld as leader of the new session and process group: %s", QCoreApplication::applicationPid(), strerror(errno)); _exit(Auth::HELPER_OTHER_ERROR); } // take control of the tty if (takeControl) { if (ioctl(STDIN_FILENO, TIOCSCTTY) < 0) { const auto error = strerror(errno); qCritical().nospace() << "Failed to take control of " << ttyString << " (" << QFileInfo(ttyString).owner() << "): " << error; _exit(Auth::HELPER_TTY_ERROR); } } if (vtNumber > 0) VirtualTerminal::jumpToVt(vtNumber, x11UserSession); } #ifdef Q_OS_LINUX // enter Linux namespaces for (const QString &ns: mainConfig.Namespaces.get()) { qInfo() << "Entering namespace" << ns; int fd = ::open(qPrintable(ns), O_RDONLY); if (fd < 0) { qCritical("open(%s) failed: %s", qPrintable(ns), strerror(errno)); exit(Auth::HELPER_OTHER_ERROR); } if (setns(fd, 0) != 0) { qCritical("setns(open(%s), 0) failed: %s", qPrintable(ns), strerror(errno)); exit(Auth::HELPER_OTHER_ERROR); } ::close(fd); } #endif // switch user const QByteArray username = qobject_cast(parent())->user().toLocal8Bit(); struct passwd pw; struct passwd *rpw; long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); if (bufsize == -1) bufsize = 16384; QScopedPointer buffer(static_cast(malloc(bufsize))); if (buffer.isNull()) { qCritical() << "Could not allocate buffer of size" << bufsize; exit(Auth::HELPER_OTHER_ERROR); } int err = getpwnam_r(username.constData(), &pw, buffer.data(), bufsize, &rpw); if (rpw == NULL) { if (err == 0) qCritical() << "getpwnam_r(" << username << ") username not found!"; else qCritical() << "getpwnam_r(" << username << ") failed with error: " << strerror(err); exit(Auth::HELPER_OTHER_ERROR); } const int xauthHandle = m_xauthFile.handle(); if (xauthHandle != -1 && fchown(xauthHandle, pw.pw_uid, pw.pw_gid) != 0) { qCritical() << "fchown failed for" << m_xauthFile.fileName(); exit(Auth::HELPER_OTHER_ERROR); } #if defined(Q_OS_FREEBSD) // execve() uses the environment prepared in Backend::openSession(), // therefore environment variables which are set here are ignored. if (setusercontext(NULL, &pw, pw.pw_uid, LOGIN_SETALL) != 0) { qCritical() << "setusercontext(NULL, *, " << pw.pw_uid << ", LOGIN_SETALL) failed for user: " << username; exit(Auth::HELPER_OTHER_ERROR); } #else if (setgid(pw.pw_gid) != 0) { qCritical() << "setgid(" << pw.pw_gid << ") failed for user: " << username; exit(Auth::HELPER_OTHER_ERROR); } // fetch ambient groups from PAM's environment; // these are set by modules such as pam_groups.so int n_pam_groups = getgroups(0, NULL); gid_t *pam_groups = NULL; if (n_pam_groups > 0) { pam_groups = new gid_t[n_pam_groups]; if ((n_pam_groups = getgroups(n_pam_groups, pam_groups)) == -1) { qCritical() << "getgroups() failed to fetch supplemental" << "PAM groups for user:" << username; exit(Auth::HELPER_OTHER_ERROR); } } else { n_pam_groups = 0; } // fetch session's user's groups int n_user_groups = 0; gid_t *user_groups = NULL; if (-1 == getgrouplist(pw.pw_name, pw.pw_gid, NULL, &n_user_groups)) { user_groups = new gid_t[n_user_groups]; if ((n_user_groups = getgrouplist(pw.pw_name, pw.pw_gid, user_groups, &n_user_groups)) == -1 ) { qCritical() << "getgrouplist(" << pw.pw_name << ", " << pw.pw_gid << ") failed"; exit(Auth::HELPER_OTHER_ERROR); } } // set groups to concatenation of PAM's ambient // groups and the session's user's groups int n_groups = n_pam_groups + n_user_groups; if (n_groups > 0) { gid_t *groups = new gid_t[n_groups]; memcpy(groups, pam_groups, (n_pam_groups * sizeof(gid_t))); memcpy((groups + n_pam_groups), user_groups, (n_user_groups * sizeof(gid_t))); // setgroups(2) handles duplicate groups if (setgroups(n_groups, groups) != 0) { qCritical() << "setgroups() failed for user: " << username; exit (Auth::HELPER_OTHER_ERROR); } delete[] groups; } delete[] pam_groups; delete[] user_groups; if (setuid(pw.pw_uid) != 0) { qCritical() << "setuid(" << pw.pw_uid << ") failed for user: " << username; exit(Auth::HELPER_OTHER_ERROR); } #endif /* Q_OS_FREEBSD */ if (chdir(pw.pw_dir) != 0) { qCritical() << "chdir(" << pw.pw_dir << ") failed for user: " << username; qCritical() << "verify directory exist and has sufficient permissions"; exit(Auth::HELPER_OTHER_ERROR); } if (sessionClass != QLatin1String("greeter")) { //we cannot use setStandardError file as this code is run in the child process //we want to redirect after we setuid so that the log file is owned by the user // determine stderr log file based on session type QString sessionLog = QStringLiteral("%1/%2") .arg(QString::fromLocal8Bit(pw.pw_dir)) .arg(sessionType == QLatin1String("x11") ? mainConfig.X11.SessionLogFile.get() : mainConfig.Wayland.SessionLogFile.get()); // create the path QFileInfo finfo(sessionLog); QDir().mkpath(finfo.absolutePath()); //swap the stderr pipe of this subprcess into a file int fd = ::open(qPrintable(sessionLog), O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd >= 0) { dup2 (fd, STDERR_FILENO); ::close(fd); } else { qWarning() << "Could not open stderr to" << sessionLog; } //redirect any stdout to /dev/null fd = ::open("/dev/null", O_WRONLY); if (fd >= 0) { dup2 (fd, STDOUT_FILENO); ::close(fd); } else { qWarning() << "Could not redirect stdout"; } } } qint64 UserSession::cachedProcessId() { return m_cachedProcessId; } }