From 2d60f2b43df4473a3016ac0807aa3dcd8f50a14e Mon Sep 17 00:00:00 2001 From: rewine Date: Tue, 13 Oct 2026 13:56:47 +0800 Subject: [PATCH 1/2] fix(daemon): move PAM transaction into a dedicated session helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full PAM transaction (authenticate + open_session) now runs in a standalone single-threaded ddm-helper process spawned via fork+exec. gnome-keyring's PAM module forks inside pam_open_session and calls exit() in the grandchild; doing that inside the multithreaded Qt daemon triggered inherited atexit handlers and deadlocked on QThread::wait(). 将 PAM 完整流程(认证+会话)迁移到独立的单线程 ddm-helper 进程, 该进程通过 fork+exec 启动,不建立 DBus 连接。gnome-keyring 的 PAM 模块会在 pam_open_session 中 fork 子进程并调用 exit(),在多线程的 Qt 守护进程中会触发继承的 atexit 处理器并死锁在 QThread::wait()。 PMS: BUG-379365 Log: 将 PAM 认证与会话迁移到独立单线程 helper 进程 Influence: 修复图形登录时 gnome-keyring 触发 Qt 多线程 fork 死锁的问题 --- debian/ddm.install | 1 + src/CMakeLists.txt | 1 + src/daemon/Auth.cpp | 634 +++++++++++++++---------------------- src/daemon/Auth.h | 27 +- src/daemon/UserSession.cpp | 33 +- src/daemon/UserSession.h | 15 +- src/helper/CMakeLists.txt | 25 ++ src/helper/HelperApp.cpp | 498 +++++++++++++++++++++++++++++ src/helper/HelperApp.h | 105 ++++++ 9 files changed, 929 insertions(+), 410 deletions(-) create mode 100644 src/helper/CMakeLists.txt create mode 100644 src/helper/HelperApp.cpp create mode 100644 src/helper/HelperApp.h diff --git a/debian/ddm.install b/debian/ddm.install index 33ae3be..9fea34e 100644 --- a/debian/ddm.install +++ b/debian/ddm.install @@ -1,5 +1,6 @@ usr/bin/* usr/share/* +usr/libexec/* usr/lib/systemd/* usr/lib/tmpfiles.d/* usr/lib/sysusers.d/* diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bf56561..31b20b9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(common) add_subdirectory(daemon) +add_subdirectory(helper) diff --git a/src/daemon/Auth.cpp b/src/daemon/Auth.cpp index 76e9e87..3208ac0 100644 --- a/src/daemon/Auth.cpp +++ b/src/daemon/Auth.cpp @@ -3,120 +3,58 @@ #include "Auth.h" -#include "UserSession.h" - +#include "Constants.h" #include "DaemonApp.h" #include "DdeSeatdControl.h" -#include "Login1Manager.h" -#include "Login1Session.h" -#include "SignalHandler.h" -#include "TtyUtils.h" -#include "TreelandConnector.h" #include -#include #include #include -#include -#include + +#include +#include #include #include -namespace DDM { - -#ifdef SYS_close_range - bool tryCloseInheritedWithCloseRange(int preservedFd) - { - const unsigned int first = STDERR_FILENO + 1; - const unsigned int last = UINT_MAX; - - // No preserved FD in range: just close everything - if (preservedFd < 0 || static_cast(preservedFd) < first) - return syscall(SYS_close_range, first, last, 0) == 0; - - const unsigned int uPreservedFd = static_cast(preservedFd); - - // Close [first, preservedFd - 1] - if (uPreservedFd > first) { - if (syscall(SYS_close_range, first, uPreservedFd - 1, 0) == -1) - return false; - } - - // Close [preservedFd + 1, last] - if (uPreservedFd < last) { - if (syscall(SYS_close_range, uPreservedFd + 1, last, 0) == -1) - return false; - } - return true; - } -#endif +#include +#include +#include - void closeInheritedFileDescriptors(int preservedFd) - { -#ifdef SYS_close_range - if (tryCloseInheritedWithCloseRange(preservedFd)) - return; -#endif - const long maxFd = sysconf(_SC_OPEN_MAX); - for (int fd = STDERR_FILENO + 1; fd < maxFd; ++fd) { - if (fd != preservedFd) - close(fd); - } - } - - // Complete-transfer helpers for the parent/child session pipe. - // Plain read()/write() may return fewer bytes than requested (short - // transfer) or fail with EINTR when interrupted by a signal. Treating - // those as fatal would leak an already-opened logind session, so retry - // until the whole frame is transferred or a real error/EOF occurs. - static bool writeAll(int fd, const void *buffer, size_t size) - { - const auto *data = static_cast(buffer); - size_t written = 0; - while (written < size) { - const ssize_t ret = write(fd, data + written, size - written); - if (ret == -1) { - if (errno == EINTR) - continue; - return false; - } - if (ret == 0) { - // write() returning 0 means nothing was transferred. On a - // blocking pipe this should not happen for a non-zero size, - // but treat it as a hard error to avoid an infinite loop. - errno = EIO; - return false; - } - written += static_cast(ret); - } - return true; - } +namespace DDM { - static bool readFull(int fd, void *buffer, size_t size) - { - auto *data = static_cast(buffer); - size_t received = 0; - while (received < size) { - const ssize_t ret = read(fd, data + received, size - received); - if (ret == -1) { - if (errno == EINTR) - continue; - return false; - } - if (ret == 0) { // EOF: the child closed the pipe early - errno = EPIPE; - return false; + namespace { + constexpr int kHeaderSize = sizeof(quint32); + constexpr quint32 kMaxFrameSize = 1 << 20; + + // Helper protocol opcodes. Keep in sync with HelperApp::Msg. + constexpr quint32 kOpAuthenticate = 1; + constexpr quint32 kOpOpenSession = 2; + constexpr quint32 kOpAuthResult = 3; + constexpr quint32 kOpSessionResult = 4; + constexpr quint32 kOpSessionFinished = 5; + + bool writeAll(int fd, const void *buf, size_t len) + { + const char *p = static_cast(buf); + size_t off = 0; + while (off < len) { + // MSG_NOSIGNAL: the peer (helper) may already have exited, + // so write() must return EPIPE instead of raising SIGPIPE + // and killing the daemon. + const ssize_t w = ::send(fd, p + off, len - off, MSG_NOSIGNAL); + if (w < 0) { + if (errno == EINTR) + continue; + return false; + } + if (w == 0) + return false; + off += static_cast(w); } - received += static_cast(ret); + return true; } - return true; } - // XDG_SESSION_ID is a short opaque string (numeric or e.g. "c1"). The - // length prefix is validated against this bound to avoid allocating a - // huge buffer from a corrupted/oversized pipe frame. - constexpr quint32 kMaxSessionIdLen = 256; - /////////////////////////// // utmp helper functions // /////////////////////////// @@ -187,56 +125,6 @@ namespace DDM { updwtmpx("/var/log/wtmp", &entry); } - /////////////////////////////// - // PAM conversation function // - /////////////////////////////// - - /** PAM conversation function */ - static int converse(int num_msg, - const struct pam_message **msg, - struct pam_response **resp, - void *secret_ptr) { - *resp = (struct pam_response *) calloc(num_msg, sizeof(struct pam_response)); - if (!*resp) - return PAM_BUF_ERR; - - // We only handle secret (password) sending here, which is - // prompt by PAM_PROMPT_ECHO_OFF. Message types (error/info) - // are just logged. - // - // Prompts with PAM_PROMPT_ECHO_ON (most likely asking for - // username) are not expected, since we required username is - // set before. - for (int i = 0; i < num_msg; ++i) { - switch (msg[i]->msg_style) { - case PAM_PROMPT_ECHO_OFF: - resp[i]->resp = strdup(*static_cast(secret_ptr)); - resp[i]->resp_retcode = 0; - break; - case PAM_ERROR_MSG: - qWarning() << "[Converse] Error message:" << msg[i]->msg; - resp[i]->resp = nullptr; - resp[i]->resp_retcode = 0; - break; - case PAM_TEXT_INFO: - qInfo() << "[Converse] Info message:" << msg[i]->msg; - resp[i]->resp = nullptr; - resp[i]->resp_retcode = 0; - break; - default: - qCritical("[Converse] Unsupported message style %d: %s", msg[i]->msg_style, msg[i]->msg); - for (int j = 0; j < i; j++) { - free(resp[j]->resp); - resp[j]->resp = nullptr; - } - free(*resp); - *resp = nullptr; - return PAM_CONV_ERR; - } - } - return PAM_SUCCESS; - } - ///////////////////////// // Auth implementation // ///////////////////////// @@ -246,19 +134,15 @@ namespace DDM { public: AuthPrivate(Auth *parent) : QObject(parent) - , secretPtr(new const char *) - , conv({ converse, static_cast(secretPtr) }) { - *secretPtr = nullptr; - } - - ~AuthPrivate() { - delete secretPtr; + { } - pam_handle_t *handle{ nullptr }; - const char **secretPtr{}; - pam_conv conv{}; - int ret{}; + // socketpair for talking to the session helper. + int socketFd { -1 }; + // Helper process. + QProcess *helper { nullptr }; + // Buffer for streaming replies from the helper. + QByteArray recvBuffer; }; Auth::Auth(QObject *parent, QString user) @@ -268,48 +152,176 @@ namespace DDM { Auth::~Auth() { if (sessionOpened) { - delete m_notifier; if (sessionPid > 0) kill(sessionPid, SIGTERM); - if (sessionLeaderPid > 0) - kill(sessionLeaderPid, SIGTERM); closeSession(); - utmpLogout(); } - if (d->handle) { - d->ret = pam_end(d->handle, d->ret); - if (d->ret != PAM_SUCCESS) - qWarning() << "[Auth] PAM handle end with error!"; + // closeSession() records logout while sessionOpened is still true, + // but a session that was never opened (e.g. authenticate-only) needs + // no utmp logout. The helper is always terminated as a fallback here. + if (d->helper) { + d->helper->terminate(); + if (!d->helper->waitForFinished(3000)) + d->helper->kill(); + } + if (d->socketFd >= 0) { + ::close(d->socketFd); + d->socketFd = -1; } qInfo() << "[Auth] Auth for user" << user << "ended."; } -#define CHECK_RET_AUTH \ - if (d->ret != PAM_SUCCESS) { \ - qWarning() << "[Auth] Authenticate:" << pam_strerror(d->handle, d->ret); \ - utmpLogin(false); \ - return false; \ + bool Auth::ensureHelper() + { + if (d->helper) + return true; + + int sv[2]; + if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv) == -1) { + qWarning() << "[Auth] socketpair failed:" << strerror(errno); + return false; + } + + d->helper = new QProcess(this); + + // The helper is exec()'d from a fresh, single-threaded process. It + // never opens a DBus connection, so gnome-keyring's fork()+exit() + // inside pam_open_session() cannot deadlock on Qt atexit handlers. + // + // All of the daemon's sockets are FD_CLOEXEC, so exec() closes them + // automatically (no inherited system-bus fd, no retained DBus names). + // The only fd that must survive exec() is our socketpair write end, + // whose number is passed through argv after clearing its CLOEXEC flag. + d->helper->setChildProcessModifier([sv]() { + ::close(sv[0]); + ::fcntl(sv[1], F_SETFD, 0); // clear FD_CLOEXEC + }); + + d->helper->setProgram(QStringLiteral("%1/ddm-helper").arg(QStringLiteral(LIBEXEC_INSTALL_DIR))); + d->helper->setArguments({ QString::number(sv[1]) }); + + connect(d->helper, &QProcess::errorOccurred, this, [this](QProcess::ProcessError e) { + qCritical() << "[Auth] helper error:" << e; + }); + connect(d->helper, QOverload::of(&QProcess::finished), + this, &Auth::onHelperFinished); + + d->helper->start(); + + ::close(sv[1]); + d->socketFd = sv[0]; + + if (!d->helper->waitForStarted()) { + qWarning() << "[Auth] failed to start helper"; + ::close(d->socketFd); + d->socketFd = -1; + d->helper->deleteLater(); + d->helper = nullptr; + return false; + } + + return true; + } + + void Auth::onHelperFinished(int exitCode, QProcess::ExitStatus status) + { + qInfo() << "[Auth] session helper finished, exitCode" << exitCode + << "status" << status; + // Whether it exited cleanly or crashed, the user session is over. + // Do NOT clear sessionOpened here: the destructor (invoked by Display + // from this signal) uses that flag to record the utmp/wtmp logout. + Q_EMIT sessionFinished(); + } + + bool Auth::sendRequest(quint32 op, const QByteArray &payload, QByteArray &reply) + { + if (!ensureHelper()) + return false; + + QByteArray frame; + { + QDataStream ds(&frame, QIODevice::WriteOnly); + ds << static_cast(payload.size() + sizeof(quint32)); + ds << op; + if (!payload.isEmpty()) + ds.writeRawData(payload.constData(), payload.size()); + } + + if (!writeAll(d->socketFd, frame.constData(), frame.size())) { + qWarning() << "[Auth] failed to write request"; + return false; + } + + // Read one complete framed reply. + for (;;) { + if (d->recvBuffer.size() >= kHeaderSize) { + // The helper writes the frame header with QDataStream + // (big-endian); read it the same way. + quint32 len = 0; + { + QDataStream lenStream(d->recvBuffer); + lenStream >> len; + } + if (len < sizeof(quint32) || len > kMaxFrameSize) { + qWarning() << "[Auth] invalid reply length" << len; + return false; + } + if (d->recvBuffer.size() >= static_cast(len + kHeaderSize)) { + // len counts the op (4 bytes) + payload bytes that follow + // the 4-byte length header. + reply = d->recvBuffer.mid(kHeaderSize, len); + d->recvBuffer.remove(0, static_cast(len + kHeaderSize)); + return true; + } + } + char chunk[4096]; + const ssize_t r = ::read(d->socketFd, chunk, sizeof(chunk)); + if (r <= 0) { + if (r < 0 && errno == EINTR) + continue; + qWarning() << "[Auth] helper closed channel unexpectedly"; + return false; + } + d->recvBuffer.append(chunk, static_cast(r)); + } } + bool Auth::authenticate(const QByteArray &secret) { Q_ASSERT(!user.isEmpty()); qInfo() << "[Auth] Starting..."; - d->ret = pam_start(pamService.toLocal8Bit().constData(), user.toLocal8Bit().constData(), &d->conv, &d->handle); - CHECK_RET_AUTH - qInfo() << "[Auth] Authenticating user" << user; + if (!ensureHelper()) { + utmpLogin(false); + return false; + } - // Set the secret, authenticate, then clear the secret - // immediately to avoid leak - *d->secretPtr = secret.constData(); - d->ret = pam_authenticate(d->handle, 0); - *d->secretPtr = nullptr; + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << user << pamService << secret; + } - CHECK_RET_AUTH - qInfo() << "[Auth] Authenticated."; + QByteArray reply; + if (!sendRequest(kOpAuthenticate, payload, reply)) { + utmpLogin(false); + return false; + } + + // Reply payload: [quint32 op(AuthResult)][bool ok] + QDataStream in(reply); + quint32 op = 0; + in >> op; + bool ok = false; + if (op == kOpAuthResult) { + in >> ok; + } - d->ret = pam_acct_mgmt(d->handle, 0); - CHECK_RET_AUTH + if (!ok) { + qWarning() << "[Auth] Authentication failed for user" << user; + utmpLogin(false); + return false; + } authenticated = true; return true; @@ -320,166 +332,68 @@ namespace DDM { const QByteArray &cookie) { Q_ASSERT(authenticated); - int pipefd[2]; - if (pipe(pipefd) == -1) { - qWarning() << "[Auth] pipe failed:" << strerror(errno); + if (!ensureHelper()) { return {}; } // Here is most safe place to request the VT switch before opening the session. if (!daemonApp->seatdControl()->requestSwitchVt(tty)) { qWarning() << "[Auth] Failed to switch to VT" << tty << ":" << strerror(errno); - close(pipefd[0]); - close(pipefd[1]); return {}; } - sessionLeaderPid = fork(); - switch (sessionLeaderPid) { - case -1: { - // Fork failed - qWarning() << "[Auth] fork failed:" << strerror(errno); - close(pipefd[0]); - close(pipefd[1]); - return {}; + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << env.toStringList() + << cookie + << display + << tty + << static_cast(type) + << command; } - case 0: { - // Child (session leader) process - close(pipefd[0]); - - // Delete old signal handlers, in order to close old fds - // which are shared with the parent process. - delete daemonApp->signalHandler(); - // The session leader must not keep daemon-owned sockets alive. - // In particular, an inherited system bus fd would retain DDM's - // well-known names after the daemon exits. - closeInheritedFileDescriptors(pipefd[1]); - - // Restore default SIGINT and SIGTERM handlers. We need - // the signal hander to terminate ourself, since we're - // going to enter an infinite waiting loop and no one can - // interrupt us after fork(), except the signal handler. - signal(SIGINT, SIG_DFL); - signal(SIGTERM, SIG_DFL); - - // Insert necessary environment - struct passwd *pw = getpwnam(qPrintable(user)); - if (pw) { - env.insert(QStringLiteral("HOME"), QString::fromLocal8Bit(pw->pw_dir)); - env.insert(QStringLiteral("PWD"), QString::fromLocal8Bit(pw->pw_dir)); - env.insert(QStringLiteral("SHELL"), QString::fromLocal8Bit(pw->pw_shell)); - env.insert(QStringLiteral("USER"), QString::fromLocal8Bit(pw->pw_name)); - env.insert(QStringLiteral("LOGNAME"), QString::fromLocal8Bit(pw->pw_name)); - } - - // Open session - auto sessionEnv = openSessionInternal(env); - if (!sessionEnv.has_value()) { - qCritical() << "[SessionLeader] Failed to open session. Exit now."; - exit(1); - } - env = *sessionEnv; - // Retrieve XDG_SESSION_ID - xdgSessionId = env.value(QStringLiteral("XDG_SESSION_ID")); - if (xdgSessionId.isEmpty()) { - qCritical() << "[SessionLeader] Invalid XDG_SESSION_ID from pam_open_session()"; - exit(1); - } - const QByteArray idBytes = xdgSessionId.toLocal8Bit(); - const quint32 idLen = static_cast(idBytes.size()); - if (!writeAll(pipefd[1], &idLen, sizeof(idLen)) - || !writeAll(pipefd[1], idBytes.constData(), idBytes.size())) { - qCritical() << "[SessionLeader] Failed to write XDG_SESSION_ID to parent process!"; - exit(1); - } - - // RUN!!! - UserSession session(this); - session.setProcessEnvironment(env); - session.start(command, type, cookie); - if (!session.waitForStarted()) { - qCritical() << "[SessionLeader] Failed to start session process. Exit now."; - exit(1); - } - - // Send session PID to parent - sessionPid = session.processId(); - if (!writeAll(pipefd[1], &sessionPid, sizeof(qint64))) { - qCritical() << "[SessionLeader] Failed to write session PID to parent process!"; - exit(1); - } - qInfo() << "[SessionLeader] Session started with PID" << sessionPid; + QByteArray reply; + if (!sendRequest(kOpOpenSession, payload, reply)) { + return {}; + } - session.waitForFinished(-1); + QDataStream in(reply); + quint32 op = 0; + in >> op; + if (op != kOpSessionResult) { + qWarning() << "[Auth] unexpected reply op" << op; + return {}; + } - // Handle session end - if (session.exitStatus() == QProcess::CrashExit) { - qCritical() << "[SessionLeader] Session process crashed. Exit now."; - exit(1); - } - qInfo() << "[SessionLeader] Session process finished with exit code" - << session.exitCode() << ". Exiting."; - exit(session.exitCode()); + quint32 status = 0; + in >> status; + if (status != 0 /* Success */) { + qWarning() << "[Auth] openSession failed with status" << status; + return {}; } - default: { - // Parent process - close(pipefd[1]); - - quint32 idLen = 0; - if (!readFull(pipefd[0], &idLen, sizeof(idLen))) { - qWarning() << "[Auth] Failed to read XDG_SESSION_ID from child process:" << strerror(errno); - close(pipefd[0]); - return {}; - } - // XDG_SESSION_ID is a short opaque string (numeric or e.g. "c1"), - // so guard against a corrupted/oversized length field from the pipe. - if (idLen == 0 || idLen > kMaxSessionIdLen) { - qWarning() << "[Auth] Invalid XDG_SESSION_ID length:" << idLen; - close(pipefd[0]); - return {}; - } - QByteArray idBytes(static_cast(idLen), Qt::Uninitialized); - if (!readFull(pipefd[0], idBytes.data(), idLen)) { - qWarning() << "[Auth] Failed to read XDG_SESSION_ID from child process:" << strerror(errno); - close(pipefd[0]); - return {}; - } - xdgSessionId = QString::fromLocal8Bit(idBytes); - if (!readFull(pipefd[0], &sessionPid, sizeof(qint64))) { - qWarning() << "[Auth] Failed to read session PID from child process:" << strerror(errno); - close(pipefd[0]); - return {}; - } - if (sessionPid <= 0) { - qWarning() << "[Auth] Invalid session PID from child process:" << sessionPid; - close(pipefd[0]); - return {}; - } - utmpLogin(true); - - // Monitor child process ends - const int readFd = pipefd[0]; - m_notifier = new QSocketNotifier(readFd, QSocketNotifier::Read); - connect(m_notifier, &QSocketNotifier::activated, this, [this, readFd] { - close(readFd); - m_notifier->setEnabled(false); - m_notifier->deleteLater(); - Q_EMIT sessionFinished(); - }); - - sessionOpened = true; - return xdgSessionId; + QString xdgId; + qint64 pid = 0; + in >> xdgId >> pid; + + if (xdgId.isEmpty()) { + qCritical() << "[Auth] empty XDG_SESSION_ID from helper"; + return {}; } + if (pid <= 0) { + qCritical() << "[Auth] invalid session PID from helper" << pid; + return {}; } - } -#define CHECK_RET_CLOSE \ - if (d->ret != PAM_SUCCESS) { \ - qWarning() << "[Auth] closeSession:" << pam_strerror(d->handle, d->ret); \ - return false; \ + xdgSessionId = xdgId; + sessionPid = pid; + utmpLogin(true); + sessionOpened = true; + + return xdgSessionId; } + bool Auth::closeSession() { if (!sessionOpened) { qWarning() << "[Auth] closeSession: Session was not opened."; @@ -487,67 +401,23 @@ namespace DDM { } qWarning() << "[Auth] Closing session for user" << user; - d->ret = pam_close_session(d->handle, 0); - CHECK_RET_CLOSE + // The helper runs the PAM teardown (pam_close_session / + // PAM_DELETE_CRED / pam_end) in ~HelperApp(). SIGTERM makes it exit + // its event loop gracefully so that destructor runs; kill() is only a + // timeout fallback. + if (d->helper && d->helper->state() != QProcess::NotRunning) { + d->helper->terminate(); + if (!d->helper->waitForFinished(5000)) + d->helper->kill(); + } - sessionOpened = false; - d->ret = pam_setcred(d->handle, PAM_DELETE_CRED); - CHECK_RET_CLOSE + // Record logout while sessionOpened is still set. + utmpLogout(); + sessionOpened = false; qInfo() << "[Auth] Session closed."; return true; } - -#define CHECK_RET_OPEN \ - if (d->ret != PAM_SUCCESS) { \ - qWarning() << "[Auth] openSessionInternal:" << pam_strerror(d->handle, d->ret); \ - return std::nullopt; \ - } - std::optional Auth::openSessionInternal(const QProcessEnvironment &sessionEnv) { - qInfo() << "[Auth] Opening session for user" << user; - - d->ret = pam_setcred(d->handle, PAM_ESTABLISH_CRED); - CHECK_RET_OPEN - - // Set PAM_TTY - QString vtPath = TtyUtils::path(tty); - d->ret = pam_set_item(d->handle, PAM_TTY, qPrintable(vtPath)); - CHECK_RET_OPEN - - // Set PAM_XDISPLAY - if (!display.isEmpty()) { - d->ret = pam_set_item(d->handle, PAM_XDISPLAY, qPrintable(display)); - CHECK_RET_OPEN - } - - // Insert environments into new session - QStringList envStrs = sessionEnv.toStringList(); - for (const QString &s : std::as_const(envStrs)) { - d->ret = pam_putenv(d->handle, qPrintable(s)); - CHECK_RET_OPEN - } - - // OPEN!!! - d->ret = pam_open_session(d->handle, 0); - CHECK_RET_OPEN - - qInfo() << "[Auth] Session opened."; - - // Retrieve env vars in new session - QProcessEnvironment env; - char **envlist = pam_getenvlist(d->handle); - if (envlist) { - for (int i = 0; envlist[i] != nullptr; ++i) { - QString str = QString::fromLocal8Bit(envlist[i]); - int equalPos = str.indexOf('='); - if (equalPos != -1) - env.insert(str.left(equalPos), str.mid(equalPos + 1)); - free(envlist[i]); - } - free(envlist); - } - return env; - } } // namespace DDM #include "Auth.moc" diff --git a/src/daemon/Auth.h b/src/daemon/Auth.h index dc90ce4..5aa4fa8 100644 --- a/src/daemon/Auth.h +++ b/src/daemon/Auth.h @@ -7,8 +7,8 @@ #include "Display.h" #include +#include #include -#include namespace DDM { class AuthPrivate; @@ -49,10 +49,7 @@ namespace DDM { /** Logind session ID (the XDG_SESSION_ID env var). Empty means no session */ QString xdgSessionId{}; - /** PID of the session leader. Positive values are valid */ - pid_t sessionLeaderPid{ 0 }; - - /** PID of the session process started by session leader. Positive values are valid */ + /** PID of the session process started by session helper. Positive values are valid */ qint64 sessionPid{ 0 }; public Q_SLOTS: @@ -83,16 +80,6 @@ namespace DDM { */ bool closeSession(); - /** - * Opens PAM session and returns the environment variables set - * by PAM modules. Must be called in the child process after - * fork() - * - * @param sessionEnv Environment variables to set for the session - * @return Environs retrieved from pam_getenvlist on success, std::nullopt on failure - */ - std::optional openSessionInternal(const QProcessEnvironment &sessionEnv); - Q_SIGNALS: /** * Emitted when the session process ends. @@ -112,8 +99,14 @@ namespace DDM { */ void utmpLogout(); - /** Child process listener */ - QSocketNotifier *m_notifier{ nullptr }; + /** Ensure the session helper is spawned and connected. */ + bool ensureHelper(); + + /** Send a framed request to the helper and read one framed reply. */ + bool sendRequest(quint32 op, const QByteArray &payload, QByteArray &reply); + + /** Handle helper process exit (session finished). */ + void onHelperFinished(int exitCode, QProcess::ExitStatus status); AuthPrivate *d{ nullptr }; }; diff --git a/src/daemon/UserSession.cpp b/src/daemon/UserSession.cpp index 4a13109..5db68bf 100644 --- a/src/daemon/UserSession.cpp +++ b/src/daemon/UserSession.cpp @@ -23,10 +23,8 @@ #include #include -#include "Auth.h" #include "Configuration.h" #include "TtyUtils.h" -#include "TreelandConnector.h" #include "UserSession.h" #include "XAuth.h" @@ -46,15 +44,30 @@ #include namespace DDM { - UserSession::UserSession(Auth *parent) + UserSession::UserSession(QObject *parent) : QProcess(parent) { setChildProcessModifier(std::bind(&UserSession::childModifier, this)); + // Wire the forwarded finished signal up front (like SDDM does in its + // constructor) so a session that exits quickly cannot be missed. + connect(this, QOverload::of(&QProcess::finished), + this, &UserSession::finished); + } + + void UserSession::setTty(int tty) + { + m_tty = tty; + } + + void UserSession::setUser(const QString &user) + { + m_user = user; } void UserSession::start(const QString &command, Display::DisplayServerType type, const QByteArray &cookie) { QProcessEnvironment env = processEnvironment(); + m_type = type; switch (type) { case Display::Treeland: { @@ -125,24 +138,24 @@ namespace DDM { } } } else { - Q_EMIT finished(1); + Q_EMIT finished(1, QProcess::NormalExit); } } void UserSession::childModifier() { - Auth *auth = qobject_cast(parent()); + const Display::DisplayServerType type = m_type; // When the display server is part of the session, leak the VT into // the session as stdin so that it stays open without races. Treeland // single-mode sessions run under an already active compositor, so they // must not take the VT as their controlling terminal. - if (auth->type != Display::X11) { + if (type != Display::X11) { QString ttyString; int vtFd = -1; - if (auth->type != Display::Treeland) { + if (type != Display::Treeland) { // open VT and get the fd - ttyString = TtyUtils::path(auth->tty); + ttyString = TtyUtils::path(m_tty); vtFd = ::open(qPrintable(ttyString), O_RDWR | O_NOCTTY); } @@ -197,7 +210,7 @@ namespace DDM { } // switch user - const QByteArray username = auth->user.toLocal8Bit(); + const QByteArray username = m_user.toLocal8Bit(); struct passwd pw; struct passwd *rpw; long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); @@ -294,7 +307,7 @@ namespace DDM { // determine stderr log file based on session type QString sessionLog = QStringLiteral("%1/%2") .arg(QString::fromLocal8Bit(pw.pw_dir)) - .arg(auth->type == Display::X11 + .arg(type == Display::X11 ? mainConfig.X11.SessionLogFile.get() : mainConfig.Wayland.SessionLogFile.get()); diff --git a/src/daemon/UserSession.h b/src/daemon/UserSession.h index 5e395a7..e5ceffc 100644 --- a/src/daemon/UserSession.h +++ b/src/daemon/UserSession.h @@ -33,18 +33,31 @@ namespace DDM { class UserSession : public QProcess { Q_OBJECT public: - explicit UserSession(Auth *parent); + explicit UserSession(QObject *parent = nullptr); + + void setTty(int tty); + void setUser(const QString &user); void start(const QString &command, Display::DisplayServerType type, const QByteArray &cookie = QByteArray()); void stop(); + Q_SIGNALS: + // Forwarded from QProcess::finished in the constructor, before any + // session process is started. Mirrors SDDM's UserSession so callers + // (e.g. HelperApp) can connect to a stable signal that is wired up + // ahead of time and cannot miss a short-lived process exit. + void finished(int exitCode, QProcess::ExitStatus exitStatus); + private: // Don't call it directly, it will be invoked by the child process only void childModifier(); QTemporaryFile m_xauthFile; + Display::DisplayServerType m_type{ Display::X11 }; + int m_tty{ 0 }; + QString m_user; }; } diff --git a/src/helper/CMakeLists.txt b/src/helper/CMakeLists.txt new file mode 100644 index 0000000..82a3c98 --- /dev/null +++ b/src/helper/CMakeLists.txt @@ -0,0 +1,25 @@ +# The session helper. It is a standalone single-threaded QCoreApplication that +# runs the full PAM transaction (authenticate + open_session) and starts the +# user's desktop session. It deliberately links only against the "common" +# library (Configuration, Session, XAuth, ...) and must NOT pull in daemon-only +# machinery (DBus, display-server management, seatd control), so that it stays +# single-threaded and safe to fork() inside PAM modules. +set(HELPER_SOURCES + HelperApp.cpp + ${CMAKE_SOURCE_DIR}/src/daemon/UserSession.cpp +) + +add_executable(ddm-helper ${HELPER_SOURCES}) + +target_link_libraries(ddm-helper + PRIVATE + common + Qt6::Core + PkgConfig::Pam +) + +target_include_directories(ddm-helper PRIVATE + ${CMAKE_SOURCE_DIR}/src/daemon +) + +install(TARGETS ddm-helper DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}") diff --git a/src/helper/HelperApp.cpp b/src/helper/HelperApp.cpp new file mode 100644 index 0000000..19e6092 --- /dev/null +++ b/src/helper/HelperApp.cpp @@ -0,0 +1,498 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "HelperApp.h" + +#include "Configuration.h" +#include "SignalHandler.h" +#include "TtyUtils.h" +#include "UserSession.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace DDM { + + namespace { + // Each frame: [quint32 payloadLen][payload]. Length excludes the header. + constexpr int kHeaderSize = sizeof(quint32); + constexpr quint32 kMaxFrameSize = 1 << 20; // 1 MiB guard + + bool writeAll(int fd, const void *buf, size_t len) + { + const char *p = static_cast(buf); + size_t off = 0; + while (off < len) { + // MSG_NOSIGNAL: the peer (daemon) may already have closed the + // channel, so write() must return EPIPE instead of SIGPIPE. + const ssize_t w = ::send(fd, p + off, len - off, MSG_NOSIGNAL); + if (w < 0) { + if (errno == EINTR) + continue; + return false; + } + if (w == 0) + return false; + off += static_cast(w); + } + return true; + } + } + + HelperApp::HelperApp(int &argc, char **argv) + : QCoreApplication(argc, argv) + { + // argv: ddm-helper + if (argc < 2) { + qCritical() << "[Helper] This application is not supposed to be executed manually"; + ::exit(InternalError); + } + bool ok = false; + m_socketFd = QString::fromLocal8Bit(argv[1]).toInt(&ok); + if (!ok || m_socketFd < 0) { + qCritical() << "[Helper] invalid socket fd"; + ::exit(InternalError); + } + + // The socket must be non-blocking: QSocketNotifier fires only when + // data is available, and onReadyRead() must never block on a second + // read() after draining the available bytes, or the daemon (blocked + // in sendRequest()) and this helper would deadlock each other. + ::fcntl(m_socketFd, F_SETFL, ::fcntl(m_socketFd, F_GETFL, 0) | O_NONBLOCK); + + auto *notifier = new QSocketNotifier(m_socketFd, QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, &HelperApp::onReadyRead); + + // SIGTERM (sent by Auth::closeSession()/terminate()) must exit the + // event loop gracefully so ~HelperApp() runs the PAM teardown, rather + // than the default action of killing the process immediately. + auto *signalHandler = new SignalHandler(this); + connect(signalHandler, &SignalHandler::sigtermReceived, this, [] { + QCoreApplication::instance()->exit(0); + }); + + qInfo() << "[Helper] started, socket fd" << m_socketFd; + } + + HelperApp::~HelperApp() + { + // Stop the desktop process, then run PAM teardown (pam_close_session / + // PAM_DELETE_CRED / pam_end). This is the single cleanup path shared by + // every exit route: normal session end, SIGTERM from the daemon, or a + // failed openSession. + if (m_session) { + m_session->stop(); + m_session->deleteLater(); + } + closePamSession(); + } + + void HelperApp::sendMsg(quint32 op, const QByteArray &payload) + { + QByteArray frame; + { + QDataStream ds(&frame, QIODevice::WriteOnly); + ds << static_cast(payload.size() + sizeof(quint32)); + ds << op; + if (!payload.isEmpty()) + ds.writeRawData(payload.constData(), payload.size()); + } + if (!writeAll(m_socketFd, frame.constData(), frame.size())) + qCritical() << "[Helper] failed to write reply"; + } + + void HelperApp::onReadyRead() + { + // Drain available bytes into the buffer, then try to parse complete + // frames. Keep any partial frame for the next activation. + char chunk[4096]; + for (;;) { + const ssize_t r = ::read(m_socketFd, chunk, sizeof(chunk)); + if (r > 0) { + m_recvBuffer.append(chunk, static_cast(r)); + continue; + } + if (r == 0) { + qInfo() << "[Helper] daemon closed the channel"; + QCoreApplication::exit(0); + return; + } + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + qCritical() << "[Helper] read error:" << strerror(errno); + QCoreApplication::exit(InternalError); + return; + } + + while (m_recvBuffer.size() >= kHeaderSize) { + // The frame header is written by the daemon with QDataStream + // (big-endian), so read it the same way. + quint32 len = 0; + { + QDataStream lenStream(m_recvBuffer); + lenStream >> len; + } + if (len < sizeof(quint32) || len > kMaxFrameSize) { + qCritical() << "[Helper] invalid frame length" << len; + QCoreApplication::exit(InternalError); + return; + } + if (m_recvBuffer.size() < static_cast(len + kHeaderSize)) + break; // wait for the rest + + // len counts the op (4 bytes) + payload bytes that follow the + // 4-byte length header. + const QByteArray payload = m_recvBuffer.mid(kHeaderSize, len); + m_recvBuffer.remove(0, static_cast(len + kHeaderSize)); + + QDataStream in(payload); + quint32 op = 0; + in >> op; + switch (op) { + case Authenticate: + handleAuthenticate(in); + break; + case OpenSession: + handleOpenSession(in); + break; + default: + qCritical() << "[Helper] unknown opcode" << op; + QCoreApplication::exit(InternalError); + return; + } + } + } + + void HelperApp::handleAuthenticate(QDataStream &in) + { + QString user, pamService; + QByteArray secret; + in >> user >> pamService >> secret; + + const bool ok = doAuthenticate(user, pamService, secret); + sendMsg(AuthResult, ok ? QByteArray("\x01", 1) : QByteArray("\x00", 1)); + } + + void HelperApp::handleOpenSession(QDataStream &in) + { + QStringList envList; + QByteArray cookie; + QString display; + int tty = 0; + int type = 0; + QString command; + in >> envList >> cookie >> display >> tty >> type >> command; + + QProcessEnvironment env; + for (const QString &s : envList) { + const int eq = s.indexOf('='); + if (eq != -1) + env.insert(s.left(eq), s.mid(eq + 1)); + } + + if (!m_pamHandle) { + qCritical() << "[Helper] openSession requested but no authenticated handle"; + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << static_cast(SessionError); + } + sendMsg(SessionResult, payload); + return; + } + + // Insert user identity environment (HOME, USER, ...). + struct passwd *pw = getpwnam(qPrintable(m_user)); + if (pw) { + env.insert(QStringLiteral("HOME"), QString::fromLocal8Bit(pw->pw_dir)); + env.insert(QStringLiteral("PWD"), QString::fromLocal8Bit(pw->pw_dir)); + env.insert(QStringLiteral("SHELL"), QString::fromLocal8Bit(pw->pw_shell)); + env.insert(QStringLiteral("USER"), QString::fromLocal8Bit(pw->pw_name)); + env.insert(QStringLiteral("LOGNAME"), QString::fromLocal8Bit(pw->pw_name)); + } + + m_display = display; + m_tty = tty; + m_type = static_cast(type); + m_command = command; + m_cookie = cookie; + + QProcessEnvironment envOut; + if (!doOpenSession(env, envOut)) { + // doOpenSession() may fail after pam_setcred(PAM_ESTABLISH_CRED), + // which leaves credentials established and the handle open. Clean + // up here so the PAM teardown is not skipped (the helper keeps + // running its event loop and would otherwise never reach it). + closePamSession(); + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << static_cast(SessionError); + } + sendMsg(SessionResult, payload); + return; + } + + // Start the user's desktop session. + // Validate the session id BEFORE starting the desktop process, so a + // PAM stack that omits XDG_SESSION_ID cannot leave an orphaned desktop + // process behind (the daemon rejects an empty id and deletes Auth). + // Precedence: the original environment is the base; PAM-produced + // variables (envOut) override same-named entries. + QProcessEnvironment finalEnv = env; + finalEnv.insert(envOut); + const QString xdgSessionId = finalEnv.value(QStringLiteral("XDG_SESSION_ID")); + if (xdgSessionId.isEmpty()) { + qCritical() << "[Helper] PAM did not provide XDG_SESSION_ID"; + closePamSession(); + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << static_cast(SessionError); + } + sendMsg(SessionResult, payload); + return; + } + + m_session = new UserSession(this); + m_session->setUser(m_user); + m_session->setTty(m_tty); + m_session->setProcessEnvironment(finalEnv); + + // Connect to UserSession's forwarded finished signal BEFORE starting + // the process (the signal itself is wired from QProcess::finished in + // the UserSession constructor, mirroring SDDM). A short-lived session + // wrapper that exits between start() and waitForStarted() therefore + // still delivers the signal. + connect(m_session, &UserSession::finished, this, &HelperApp::onSessionFinished); + + m_session->start(m_command, m_type, m_cookie); + + if (!m_session->waitForStarted()) { + qCritical() << "[Helper] failed to start session process"; + closePamSession(); + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << static_cast(SessionError); + } + sendMsg(SessionResult, payload); + return; + } + + const qint64 sessionPid = m_session->processId(); + + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + ds << static_cast(Success) << xdgSessionId << sessionPid; + } + sendMsg(SessionResult, payload); + } + + void HelperApp::closePamSession() + { + if (!m_pamHandle) + return; + + if (m_sessionOpened) { + pam_close_session(m_pamHandle, 0); + m_sessionOpened = false; + } + pam_setcred(m_pamHandle, PAM_DELETE_CRED); + pam_end(m_pamHandle, PAM_SUCCESS); + m_pamHandle = nullptr; + m_secret.clear(); + m_secretPtr = nullptr; + } + + void HelperApp::onSessionFinished(int exitCode, QProcess::ExitStatus status) + { + qInfo() << "[Helper] session process finished, exitCode" << exitCode + << "status" << status; + + closePamSession(); + + QByteArray payload; + { + QDataStream ds(&payload, QIODevice::WriteOnly); + quint32 code = static_cast(exitCode); + if (status == QProcess::CrashExit) + code = static_cast(SessionError); + ds << code; + } + sendMsg(SessionFinished, payload); + + // The helper is one-shot per session: exit cleanly. + QCoreApplication::exit(static_cast(exitCode)); + } + + int HelperApp::converse(int num_msg, + const struct pam_message **msg, + struct pam_response **resp, + void *secret_ptr) + { + *resp = static_cast(calloc(num_msg, sizeof(struct pam_response))); + if (!*resp) + return PAM_BUF_ERR; + + const char *secret = *static_cast(secret_ptr); + + for (int i = 0; i < num_msg; ++i) { + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + resp[i]->resp = strdup(secret); + resp[i]->resp_retcode = 0; + break; + case PAM_ERROR_MSG: + qWarning() << "[Helper][Converse] Error message:" << msg[i]->msg; + resp[i]->resp = nullptr; + resp[i]->resp_retcode = 0; + break; + case PAM_TEXT_INFO: + qInfo() << "[Helper][Converse] Info message:" << msg[i]->msg; + resp[i]->resp = nullptr; + resp[i]->resp_retcode = 0; + break; + default: + qCritical("[Helper][Converse] Unsupported message style %d: %s", msg[i]->msg_style, msg[i]->msg); + for (int j = 0; j < i; j++) { + free(resp[j]->resp); + resp[j]->resp = nullptr; + } + free(*resp); + *resp = nullptr; + return PAM_CONV_ERR; + } + } + return PAM_SUCCESS; + } + + bool HelperApp::doAuthenticate(const QString &user, const QString &pamService, const QByteArray &secret) + { + m_user = user; + m_secret = secret; + + // A stale handle should never survive here, but be safe. + if (m_pamHandle) { + pam_end(m_pamHandle, PAM_SUCCESS); + m_pamHandle = nullptr; + } + + const char *secretPtr = m_secret.constData(); + m_secretPtr = secretPtr; + const pam_conv conv = { converse, const_cast(&m_secretPtr) }; + + pam_handle_t *handle = nullptr; + int ret = pam_start(pamService.toLocal8Bit().constData(), + user.toLocal8Bit().constData(), + &conv, &handle); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_start:" << pam_strerror(handle, ret); + return false; + } + + qInfo() << "[Helper] Authenticating user" << user; + ret = pam_authenticate(handle, 0); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_authenticate:" << pam_strerror(handle, ret); + pam_end(handle, ret); + m_secret.fill(0); + m_secretPtr = nullptr; + return false; + } + + // The secret is no longer needed once authentication completed. Wipe it + // immediately so it does not linger for the whole desktop session. + // Keep m_secretPtr pointing at the (now zeroed) buffer: PAM may still + // invoke converse() during pam_acct_mgmt()/pam_open_session(), and + // nulling it would leave a dangling conversation appdata pointer. + m_secret.fill(0); + + ret = pam_acct_mgmt(handle, 0); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_acct_mgmt:" << pam_strerror(handle, ret); + pam_end(handle, ret); + m_secretPtr = nullptr; + return false; + } + + m_pamHandle = handle; + return true; + } + + bool HelperApp::doOpenSession(const QProcessEnvironment &env, QProcessEnvironment &envOut) + { + pam_handle_t *handle = m_pamHandle; + if (!handle) + return false; + + int ret = pam_setcred(handle, PAM_ESTABLISH_CRED); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_setcred:" << pam_strerror(handle, ret); + return false; + } + + const QString vtPath = TtyUtils::path(m_tty); + ret = pam_set_item(handle, PAM_TTY, qPrintable(vtPath)); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_set_item(PAM_TTY):" << pam_strerror(handle, ret); + return false; + } + + if (!m_display.isEmpty()) { + ret = pam_set_item(handle, PAM_XDISPLAY, qPrintable(m_display)); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_set_item(PAM_XDISPLAY):" << pam_strerror(handle, ret); + return false; + } + } + + const QStringList envStrs = env.toStringList(); + for (const QString &s : envStrs) { + ret = pam_putenv(handle, qPrintable(s)); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_putenv:" << pam_strerror(handle, ret); + return false; + } + } + + qInfo() << "[Helper] Opening session for user" << m_user; + ret = pam_open_session(handle, 0); + if (ret != PAM_SUCCESS) { + qWarning() << "[Helper] pam_open_session:" << pam_strerror(handle, ret); + return false; + } + m_sessionOpened = true; + + char **envlist = pam_getenvlist(handle); + if (envlist) { + for (int i = 0; envlist[i] != nullptr; ++i) { + const QString str = QString::fromLocal8Bit(envlist[i]); + const int eq = str.indexOf('='); + if (eq != -1) + envOut.insert(str.left(eq), str.mid(eq + 1)); + free(envlist[i]); + } + free(envlist); + } + return true; + } +} // namespace DDM + +int main(int argc, char **argv) +{ + DDM::HelperApp app(argc, argv); + return app.exec(); +} diff --git a/src/helper/HelperApp.h b/src/helper/HelperApp.h new file mode 100644 index 0000000..e205d3a --- /dev/null +++ b/src/helper/HelperApp.h @@ -0,0 +1,105 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef DDM_HELPERAPP_H +#define DDM_HELPERAPP_H + +#include "Display.h" + +#include +#include +#include + +#include + +namespace DDM { + class UserSession; + + /** + * Session helper run in a clean, single-threaded process. + * + * The daemon spawns this binary with fork()+exec() and communicates over an + * inherited socketpair file descriptor (passed as argv[1]). The helper is a + * long-running QCoreApplication that services two commands: + * + * - authenticate(user, pamService, secret): full PAM authentication + * - openSession(env, cookie, display, tty, type, command): PAM session + + * start the user's desktop session + * + * Running the whole PAM transaction (authenticate + open_session) here is + * what makes ddm immune to the fork()-in-a-multithreaded-Qt-process + * deadlock: gnome-keyring's PAM module forks inside pam_open_session() and + * calls exit() in the grandchild, which would otherwise trigger Qt's + * inherited atexit handlers and deadlock on QThread::wait(). Since this + * helper is exec()'d and never opens a DBus connection, it is + * single-threaded and safe. + */ + class HelperApp : public QCoreApplication { + Q_OBJECT + public: + HelperApp(int &argc, char **argv); + ~HelperApp() override; + + enum Result { + Success = 0, + InternalError = 1, + AuthError = 2, + SessionError = 3, + }; + + // Protocol message opcodes. + enum Msg : quint32 { + Authenticate = 1, + OpenSession = 2, + AuthResult = 3, + SessionResult = 4, + SessionFinished = 5, + }; + + private: + void onReadyRead(); + + void handleAuthenticate(QDataStream &in); + void handleOpenSession(QDataStream &in); + + bool doAuthenticate(const QString &user, const QString &pamService, const QByteArray &secret); + bool doOpenSession(const QProcessEnvironment &env, QProcessEnvironment &envOut); + + static int converse(int num_msg, + const struct pam_message **msg, + struct pam_response **resp, + void *secret_ptr); + + void sendMsg(quint32 op, const QByteArray &payload = QByteArray()); + + void onSessionFinished(int exitCode, QProcess::ExitStatus status); + + /** Close the PAM session (if open) and end the handle. Idempotent. */ + void closePamSession(); + + // PAM conversation state (per authenticate transaction). + QByteArray m_secret; + // Stable pointer into m_secret. The PAM conversation callback's appdata + // is &m_secretPtr, so it must outlive m_pamHandle and must not dangle + // after doAuthenticate() returns. + const char *m_secretPtr { nullptr }; + pam_handle *m_pamHandle { nullptr }; + // True once pam_open_session() succeeded and the session must be closed. + bool m_sessionOpened { false }; + + // Session parameters (set by the latest authenticate/openSession). + QString m_user; + QString m_display; + int m_tty { 0 }; + Display::DisplayServerType m_type { Display::X11 }; + QString m_command; + QByteArray m_cookie; + + UserSession *m_session { nullptr }; + + int m_socketFd { -1 }; + QByteArray m_recvBuffer; + }; +} // namespace DDM + +#endif // DDM_HELPERAPP_H From 737905947dbb711f9191c54a2bd56aa6f0039127 Mon Sep 17 00:00:00 2001 From: rewine Date: Wed, 14 Oct 2026 15:21:00 +0800 Subject: [PATCH 2/2] refactor(helper): move utmp/wtmp accounting into session helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move utmp/wtmp/btmp bookkeeping from the daemon into ddm-helper, matching SDDM's design where accounting follows the PAM session lifecycle. 将 utmp/wtmp 记账从 daemon 迁移到 ddm-helper,对齐 SDDM 设计, 使记账随 PAM 会话生命周期进行。 Log: 将 utmp/wtmp 记账下沉到 helper Influence: 注销记录不再依赖 daemon 侧 sessionOpened 状态时序,避免会话结束后遗留陈旧的 utmp/wtmp 记录。 --- src/daemon/Auth.cpp | 75 ----------------------------------- src/daemon/Auth.h | 12 ------ src/helper/HelperApp.cpp | 84 ++++++++++++++++++++++++++++++++++++++++ src/helper/HelperApp.h | 11 ++++++ 4 files changed, 95 insertions(+), 87 deletions(-) diff --git a/src/daemon/Auth.cpp b/src/daemon/Auth.cpp index 3208ac0..8b99d6e 100644 --- a/src/daemon/Auth.cpp +++ b/src/daemon/Auth.cpp @@ -13,8 +13,6 @@ #include #include -#include -#include #include #include @@ -59,72 +57,6 @@ namespace DDM { // utmp helper functions // /////////////////////////// - void Auth::utmpLogin(bool success) { - struct utmpx entry { }; - struct timeval tv; - - entry.ut_type = USER_PROCESS; - entry.ut_pid = sessionPid; - - // ut_line: vt - if (tty > 0) - strncpy(entry.ut_line, QStringLiteral("tty%1").arg(tty).toLocal8Bit().constData(), sizeof(entry.ut_line) - 1); - - // ut_host: displayName - if (!display.isEmpty()) - strncpy(entry.ut_host, display.toLocal8Bit().constData(), sizeof(entry.ut_host) - 1); - - // ut_user: user - strncpy(entry.ut_user, user.toLocal8Bit().constData(), sizeof(entry.ut_user) -1); - - gettimeofday(&tv, NULL); - entry.ut_tv.tv_sec = tv.tv_sec; - entry.ut_tv.tv_usec = tv.tv_usec; - - // write to utmp - setutxent(); - if (!pututxline (&entry)) - qWarning() << "Failed to write utmpx: " << strerror(errno); - endutxent(); - - // append to failed login database btmp - if (!success) { - updwtmpx("/var/log/btmp", &entry); - } else { - // append to wtmp - updwtmpx("/var/log/wtmp", &entry); - } - } - - void Auth::utmpLogout() { - struct utmpx entry { }; - struct timeval tv; - - entry.ut_type = DEAD_PROCESS; - entry.ut_pid = sessionPid; - - // ut_line: vt - if (tty > 0) - strncpy(entry.ut_line, QStringLiteral("tty%1").arg(tty).toLocal8Bit().constData(), sizeof(entry.ut_line) - 1); - - // ut_host: displayName - if (!display.isEmpty()) - strncpy(entry.ut_host, display.toLocal8Bit().constData(), sizeof(entry.ut_host) - 1); - - gettimeofday(&tv, NULL); - entry.ut_tv.tv_sec = tv.tv_sec; - entry.ut_tv.tv_usec = tv.tv_usec; - - // write to utmp - setutxent(); - if (!pututxline (&entry)) - qWarning() << "Failed to write utmpx: " << strerror(errno); - endutxent(); - - // append to wtmp - updwtmpx("/var/log/wtmp", &entry); - } - ///////////////////////// // Auth implementation // ///////////////////////// @@ -292,7 +224,6 @@ namespace DDM { qInfo() << "[Auth] Starting..."; if (!ensureHelper()) { - utmpLogin(false); return false; } @@ -304,7 +235,6 @@ namespace DDM { QByteArray reply; if (!sendRequest(kOpAuthenticate, payload, reply)) { - utmpLogin(false); return false; } @@ -319,7 +249,6 @@ namespace DDM { if (!ok) { qWarning() << "[Auth] Authentication failed for user" << user; - utmpLogin(false); return false; } @@ -388,7 +317,6 @@ namespace DDM { xdgSessionId = xdgId; sessionPid = pid; - utmpLogin(true); sessionOpened = true; return xdgSessionId; @@ -411,9 +339,6 @@ namespace DDM { d->helper->kill(); } - // Record logout while sessionOpened is still set. - utmpLogout(); - sessionOpened = false; qInfo() << "[Auth] Session closed."; return true; diff --git a/src/daemon/Auth.h b/src/daemon/Auth.h index 5aa4fa8..8bb115e 100644 --- a/src/daemon/Auth.h +++ b/src/daemon/Auth.h @@ -87,18 +87,6 @@ namespace DDM { void sessionFinished(); private: - /** - * Write utmp/wtmp/btmp records when a user logs in - * - * @param success Was authentication successful - */ - void utmpLogin(bool success); - - /** - * Write utmp/wtmp records when a user logs out - */ - void utmpLogout(); - /** Ensure the session helper is spawned and connected. */ bool ensureHelper(); diff --git a/src/helper/HelperApp.cpp b/src/helper/HelperApp.cpp index 19e6092..211d74f 100644 --- a/src/helper/HelperApp.cpp +++ b/src/helper/HelperApp.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -94,6 +96,9 @@ namespace DDM { m_session->deleteLater(); } closePamSession(); + + // Write the logout record (utmp/wtmp), mirroring SDDM's ~HelperApp(). + utmpLogout(); } void HelperApp::sendMsg(quint32 op, const QByteArray &payload) @@ -181,6 +186,8 @@ namespace DDM { in >> user >> pamService >> secret; const bool ok = doAuthenticate(user, pamService, secret); + if (!ok) + utmpLogin(false); sendMsg(AuthResult, ok ? QByteArray("\x01", 1) : QByteArray("\x00", 1)); } @@ -292,6 +299,11 @@ namespace DDM { } const qint64 sessionPid = m_session->processId(); + m_sessionPid = sessionPid; + m_sessionStarted = true; + + // Write the successful login record (utmp/wtmp), mirroring SDDM. + utmpLogin(true); QByteArray payload; { @@ -338,6 +350,78 @@ namespace DDM { QCoreApplication::exit(static_cast(exitCode)); } + void HelperApp::utmpLogin(bool success) + { + struct utmpx entry { }; + struct timeval tv; + + entry.ut_type = USER_PROCESS; + entry.ut_pid = m_sessionPid; + + // ut_line: vt + if (m_tty > 0) + strncpy(entry.ut_line, QStringLiteral("tty%1").arg(m_tty).toLocal8Bit().constData(), sizeof(entry.ut_line) - 1); + + // ut_host: displayName + if (!m_display.isEmpty()) + strncpy(entry.ut_host, m_display.toLocal8Bit().constData(), sizeof(entry.ut_host) - 1); + + // ut_user: user + strncpy(entry.ut_user, m_user.toLocal8Bit().constData(), sizeof(entry.ut_user) - 1); + + gettimeofday(&tv, NULL); + entry.ut_tv.tv_sec = tv.tv_sec; + entry.ut_tv.tv_usec = tv.tv_usec; + + // write to utmp + setutxent(); + if (!pututxline(&entry)) + qWarning() << "Failed to write utmpx: " << strerror(errno); + endutxent(); + + // append to failed login database btmp + if (!success) { + updwtmpx("/var/log/btmp", &entry); + } else { + // append to wtmp + updwtmpx("/var/log/wtmp", &entry); + } + } + + void HelperApp::utmpLogout() + { + // Only write a logout record if a login was written. + if (!m_sessionStarted) + return; + + struct utmpx entry { }; + struct timeval tv; + + entry.ut_type = DEAD_PROCESS; + entry.ut_pid = m_sessionPid; + + // ut_line: vt + if (m_tty > 0) + strncpy(entry.ut_line, QStringLiteral("tty%1").arg(m_tty).toLocal8Bit().constData(), sizeof(entry.ut_line) - 1); + + // ut_host: displayName + if (!m_display.isEmpty()) + strncpy(entry.ut_host, m_display.toLocal8Bit().constData(), sizeof(entry.ut_host) - 1); + + gettimeofday(&tv, NULL); + entry.ut_tv.tv_sec = tv.tv_sec; + entry.ut_tv.tv_usec = tv.tv_usec; + + // write to utmp + setutxent(); + if (!pututxline(&entry)) + qWarning() << "Failed to write utmpx: " << strerror(errno); + endutxent(); + + // append to wtmp + updwtmpx("/var/log/wtmp", &entry); + } + int HelperApp::converse(int num_msg, const struct pam_message **msg, struct pam_response **resp, diff --git a/src/helper/HelperApp.h b/src/helper/HelperApp.h index e205d3a..907113e 100644 --- a/src/helper/HelperApp.h +++ b/src/helper/HelperApp.h @@ -77,6 +77,12 @@ namespace DDM { /** Close the PAM session (if open) and end the handle. Idempotent. */ void closePamSession(); + /** Write utmp/wtmp/btmp records when a user logs in. */ + void utmpLogin(bool success); + + /** Write utmp/wtmp records when a user logs out. */ + void utmpLogout(); + // PAM conversation state (per authenticate transaction). QByteArray m_secret; // Stable pointer into m_secret. The PAM conversation callback's appdata @@ -97,6 +103,11 @@ namespace DDM { UserSession *m_session { nullptr }; + // Session process PID (cached for utmp logout after the process ends). + qint64 m_sessionPid { 0 }; + // True once a session was started and a login record was written. + bool m_sessionStarted { false }; + int m_socketFd { -1 }; QByteArray m_recvBuffer; };