Skip to content

fix(daemon): move PAM transaction into a dedicated session helper - #116

Merged
Groveer merged 2 commits into
linuxdeepin:masterfrom
wineee:fix
Oct 14, 2026
Merged

Groveer merged 2 commits into
linuxdeepin:masterfrom
wineee:fix

Conversation

@wineee

@wineee wineee commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

DDM 调研报告

版本:0.4.0(截至 2026-10)
范围:源码仓库 linuxdeepin/ddm(fork 自 SDDM),本文基于源码分析 + 一次线上死锁问题的实战排查。


1. 项目概述

DDM(Deepin Display Manager)是 deepin 桌面发行版的显示管理器,fork 自 SDDM。

维度 说明
定位 图形登录管理器(Display Manager)
上游 SDDM(C++/Qt)
技术栈 C++20 + Qt6(Core / DBus / Network)
认证 Linux-PAM(强制)
会话 systemd-logind(org.freedesktop.login1)
显示服务器 Treeland(Wayland,deepin 自研合成器)为主,兼容 X11 / Wayland
许可证 GPL-2.0-or-later
构建 CMake ≥ 3.25,Qt ≥ 6.6

与上游 SDDM 相比,ddm 最大的差异在于:greeter(登录界面)不再是一个独立的 QtQuick 进程,而是由 Treeland 合成器直接承载,ddm 只保留守护进程(daemon),通过 QLocalSocket 与 Treeland 通信。


2. 进程模型

┌─────────────────────────────────────────────────────────────┐
│                        ddm (daemon)                          │
│  root 运行 · QCoreApplication · 多线程(含 QDBus 线程)          │
│                                                              │
│  DaemonApp ─┬─ DisplayManager  (org.freedesktop.DisplayManager)│
│             ├─ SeatManager     (seat/display 生命周期)         │
│             ├─ PowerManager    (关机/重启/休眠)                │
│             ├─ SignalHandler   (SIGTERM/SIGINT/SIGHUP)        │
│             ├─ DdeSeatdControl (VT 分配 / VT 切换)             │
│             └─ TreelandConnector (treeland-ddm Wayland 协议)   │
│                                                              │
│  每个 seat 一个 Display ── TreelandDisplayServer / XorgDisplayServer│
│                        └─ SocketServer (greeter QLocalSocket) │
└──────────────────┬──────────────────────────────────────────┘
                   │ fork + exec + socketpair (帧协议)
                   ▼
┌─────────────────────────────────────────────────────────────┐
│                     ddm-helper (会话助手)                      │
│  root 运行 · 单线程 QCoreApplication · 不建立任何 DBus 连接     │
│                                                              │
│  · pam_start / pam_authenticate / pam_acct_mgmt              │
│  · pam_setcred / pam_open_session / pam_getenvlist           │
│  · 启动 UserSession(fork → setuid → setsid → exec 桌面会话)   │
└──────────────────┬──────────────────────────────────────────┘
                   │ fork + setuid + exec
                   ▼
┌─────────────────────────────────────────────────────────────┐
│                   用户桌面会话 (dde-session)                    │
│  普通用户权限 · 由 logind 管理 cgroup / session scope          │
└─────────────────────────────────────────────────────────────┘

关键设计:ddm-helper 是本次死锁修复新增的独立二进制,负责完整 PAM 事务,与 daemon 通过 socketpair 通信。daemon 保持 Qt 多线程(QDBus),而 helper 保持单线程、无 DBus,二者各司其职。


3. 源码结构

ddm/
├── CMakeLists.txt            # 顶层构建,依赖探测(Qt6/PAM/systemd/wayland)
├── services/                 # systemd unit / PAM 配置 / tmpfiles
├── data/                     # DBus 接口 XML、脚本、配置模板
├── src/
│   ├── common/               # daemon 与 helper 共享库 libddm-common
│   │   ├── Configuration.*   # ddm.conf 读取(ConfigReader)
│   │   ├── Session.*         # .desktop 会话文件解析
│   │   ├── Messages.h        # greeter↔daemon 消息枚举
│   │   ├── SocketWriter.*    # QLocalSocket 序列化
│   │   ├── XAuth.*           # X11 MIT-MAGIC-COOKIE
│   │   ├── SignalHandler.*   # POSIX 信号 → Qt 信号桥
│   │   └── LogindDBusTypes.* # logind DBus 接口
│   ├── daemon/               # ddm 守护进程
│   │   ├── DaemonApp.*       # 入口 + 各管理器装配
│   │   ├── DisplayManager.*  # org.freedesktop.DisplayManager DBus 实现
│   │   ├── SeatManager.*     # seat 创建/移除/切换
│   │   ├── Display.*         # 单 seat 的显示逻辑(VT/显示服务器/登录编排)
│   │   ├── Auth.*            # 认证编排(现已委托给 helper)
│   │   ├── UserSession.*     # 用户会话进程启动(helper 复用)
│   │   ├── SocketServer.*    # greeter QLocalSocket 服务端
│   │   ├── TreelandConnector.*      # treeland-ddm Wayland 客户端协议
│   │   ├── TreelandDisplayServer.*  # Treeland greeter 生命周期
│   │   ├── XorgDisplayServer.*      # X11 显示服务器
│   │   ├── PowerManager.*    # 电源操作
│   │   └── DdeSeatdControl.* # seatd VT 管理
│   └── helper/               # ddm-helper 会话助手(新增)
│       ├── HelperApp.*       # 单线程 PAM 事务 + 会话启动
│       └── CMakeLists.txt
└── docs/                     # 文档(本报告所在目录)

构建产物:

  • /usr/bin/ddm(daemon)
  • /usr/libexec/ddm-helper(会话助手)
  • libddm-common.so(共享库)

4. 核心模块详解

4.1 DaemonApp — 守护进程装配

DaemonApp 继承 QCoreApplication,是 daemon 的单例入口(daemonApp 宏),负责实例化并持有所有管理器:

DisplayManager  *m_displayManager;   // DBus 显示管理器
SeatManager     *m_seatManager;      // seat 生命周期
PowerManager    *m_powerManager;     // 电源管理
SignalHandler   *m_signalHandler;    // 信号处理
DdeSeatdControl *m_seatdControl;     // VT 分配/切换
TreelandConnector *m_treelandConnector; // 与 Treeland 的 Wayland 连接

4.2 DisplayManager — DBus 接口

实现 org.freedesktop.DisplayManager(以及 deepin 扩展 org.deepin.DisplayManager),对外暴露 Seats / Sessions / LastSession / AuthInfo 等属性。session 对象通过 Qt DBus adaptor 从 XML 自动生成。

4.3 SeatManager + Display — seat/display 生命周期

  • SeatManager 监听 logind 的 seat 增删,为每个图形 seat 创建一个 Display。
  • Display 代表一个 seat 的完整登录上下文:
    • 分配 greeter 的 VT(fetchAvailableVt,默认 DDM_INITIAL_VT=1)
    • 启动/停止 Treeland greeter
    • 持有 SocketServer 与 greeter 通信
    • 维护 auths 列表(进行中的认证对象)

4.4 SocketServer + Messages — greeter 通信协议

SocketServer 在 /run/ddm 下创建 QLocalServer,与 Treeland 中的 greeter 通信。消息用 SocketWriter(QDataStream)序列化。

消息枚举(Messages.h):

方向 消息
greeter → daemon Login / Logout / Lock / Unlock / PowerOff / Reboot / ...
daemon → greeter ShowGreeter / LoginFailed / Capabilities / HostName / UserLoggedIn / ...

4.5 Auth — 认证编排(daemon 侧)

Auth 是登录流程的核心编排对象,对外接口保持稳定:

bool    authenticate(const QByteArray &secret);   // 认证
QString openSession(const QString &command,        // 打开会话,返回 XDG_SESSION_ID
                    QProcessEnvironment env,
                    const QByteArray &cookie = {});
bool    closeSession();                            // 关闭会话

本次重构后,Auth 内部不再直接调用 PAM,而是通过 AuthPrivate 持有 QProcess(helper)和 socketpair,用 sendRequest(op, payload, reply) 与 helper 通信。

4.6 HelperApp — 会话助手(helper 侧)

ddm-helper 是单线程 QCoreApplication,构造时:

  1. 从 argv[1] 读取继承的 socket fd
  2. 设为 O_NONBLOCK(避免与阻塞在 read() 的 daemon 互相死锁)
  3. 用 QSocketNotifier 监听数据

处理两条指令:

opcode 指令 动作
1 Authenticate 认证 pam_start → pam_authenticate → pam_acct_mgmt
2 OpenSession 打开会话 pam_setcred → pam_open_session → pam_getenvlist → 启动 UserSession

完整 PAM 会话链:

pam_start
  → pam_authenticate   (验证密码)
  → pam_acct_mgmt      (账户有效性,过期/锁定)
  → pam_setcred        (建立凭据,PAM_ESTABLISH_CRED)
  → pam_open_session   (会话建立,gnome-keyring 在此 fork)
  → pam_getenvlist     (收集 PAM 设置的环境变量)

会话结束(onSessionFinished)时执行 pam_close_session → pam_setcred(PAM_DELETE_CRED) → pam_end。

4.7 UserSession — 用户会话进程启动

UserSession 继承 QProcess,通过 childModifier()(在子进程中执行)完成:

  1. 打开并占用 VT(X11 / Wayland)
  2. setuid 降权到目标用户
  3. setsid + 设置进程组
  4. 设置会话环境、stdout/stderr 重定向到会话日志
  5. exec 桌面会话命令

5. 登录流程时序

greeter(Treeland)      ddm(daemon)              ddm-helper           logind
      │                    │                        │                  │
      │── Login(user,pwd) ─▶│                        │                  │
      │                    │ Display::login          │                  │
      │                    │  └─ Auth::authenticate  │                  │
      │                    │     └─ sendRequest(Authenticate) ────────▶│
      │                    │                        │ pam_authenticate │
      │                    │◀──── AuthResult(ok) ────│                  │
      │                    │ Display::startUserSession                 │
      │                    │  ├─ 分配 VT             │                  │
      │                    │  ├─ 启动显示服务器(X11时)│                  │
      │                    │  └─ Auth::openSession   │                  │
      │                    │     └─ sendRequest(OpenSession) ─────────▶│
      │                    │                        │ pam_open_session │
      │                    │                        │ fork+setuid+exec │
      │                    │                        │  (用户会话启动)   │
      │                    │◀── SessionResult(xdgId, pid) ─────────────│
      │                    │  └─ utmpLogin + sessionOpened=true        │
      │                    │ logind ActivateSession ──────────────────▶│
      │                    │ treeland activateUser(xdgId)              │
      │◀─ ShowGreeter 关闭 │                        │                  │

会话结束:

用户会话退出 ──▶ ddm-helper onSessionFinished
                 ├─ pam_close_session / pam_setcred / pam_end
                 ├─ sendMsg(SessionFinished)
                 └─ QCoreApplication::exit
                              │
ddm-helper 进程退出 ──▶ ddm Auth::onHelperFinished (QProcess::finished)
                        └─ emit sessionFinished() → Display 清理 auths

6. 关键问题调研:多线程 fork 死锁

6.1 现象

用户报告:更新后点击登录无响应(ddm 图形登录死锁)。gdb 附加后确认卡死点:

#2  QWaitCondition::wait
#3  QThread::wait
#4-#5 libQt6DBus.so  (QDBusConnectionManager 析构)
#6  exit()
#7-#10 pam_gnome_keyring.so  ← pam_sm_open_session
#11 DDM::Auth::openSessionInternal

6.2 根因链

ddm (Qt6 多线程,含 QDBus 线程)
  └─ 裸 fork() 创建子进程处理 PAM session
       └─ pam_open_session → gnome-keyring PAM 模块再 fork 孙进程
            └─ 孙进程调用 exit()(而非 _exit())
                 └─ 触发从 ddm 继承的 Qt atexit 处理器
                      └─ QDBusConnectionManager 析构 → QThread::wait
                           └─ 等待 fork 时未被复制的 QDBus 线程 → 永久死锁

本质:glibc 手册明确规定,多线程进程 fork 后,子进程在调用 exec 之前只能使用 async-signal-safe 函数。exit() 会遍历 atexit 处理器和全局析构,在多线程 fork 的子进程中是不安全的。gnome-keyring 的 PAM 模块(pam_gnome_keyring.so)在 fork 的孙进程里调用 exit(),触发 Qt 的线程清理死锁。

为何 lightdm 无此问题:lightdm 是 GLib 程序,其 helper 与主进程隔离良好;而 ddm 是 Qt6 程序,QDBus 线程的存在使 fork 后 exit() 必然踩雷。

6.3 解决方案(两条互补路线)

路线一:ddm 架构对齐(已合并,commit 346ba8f)

仿 sddm 的 helper 架构,把完整 PAM 事务从 daemon 迁移到独立单线程 ddm-helper 进程:

  • daemon 通过 fork + exec 启动 helper(不再在 daemon 内 fork 后调 PAM)
  • helper 是全新的 QCoreApplication,从不建立 DBus 连接,保持单线程
  • 这样 gnome-keyring 在 helper 里 fork + exit 时,触发的是 helper 自己的 atexit(无后台线程),安全

路线二:gnome-keyring 源码修复(独立仓库)

将 gnome-keyring PAM 模块 6 处 fork 子进程里的 exit() 改为 _exit(),从源头消除「多线程 fork 后调 exit()」这一 glibc 禁止行为,对所有 Qt 显示管理器通用安全。

6.4 死锁问题对架构设计的启示

  1. 多线程进程内裸 fork 是危险操作——fork 后子进程只能安全调用 async-signal-safe 函数,任何 atexit/析构都可能死锁。
  2. 认证/session 这类会触发第三方 PAM 模块 fork 的逻辑,应隔离到单线程进程。
  3. IPC 协议要避免阻塞式读写互等——本次实现中,socket 必须是 O_NONBLOCK,否则 daemon 阻塞在 read() 等回复、helper 阻塞在第二次 read(),形成新的死锁。

7. daemon ↔ helper 通信协议

7.1 传输层

  • socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC)
  • daemon 持 sv[0],helper 持 sv[1](通过 argv[1] 传 fd 号,exec 前清除 FD_CLOEXEC)
  • helper 端 socket 设为 O_NONBLOCK

7.2 帧格式

所有帧用 QDataStream(big-endian)序列化:

[quint32 len][quint32 op][payload 原始字节]
              └── len = sizeof(op) + payload.size(),不含长度头本身
opcode 值 方向 含义
Authenticate 1 daemon→helper 认证请求
OpenSession 2 daemon→helper 打开会话请求
AuthResult 3 helper→daemon 认证结果(bool)
SessionResult 4 helper→daemon 会话结果(status + xdgId + pid)
SessionFinished 5 helper→daemon 会话结束通知

Summary by Sourcery

Move PAM authentication and user-session management into an isolated helper process to avoid multithreaded fork deadlocks in the daemon.

New Features:

  • Add a standalone ddm-helper process for handling PAM authentication, session setup, and desktop-session lifecycle.

Bug Fixes:

  • Prevent login deadlocks caused by running PAM session operations in the multithreaded daemon process.
  • Improve session cleanup and process-exit handling, including PAM teardown and login accounting records.

Enhancements:

  • Replace direct daemon-side PAM and session forking with framed socketpair IPC between the daemon and a single-threaded helper.
  • Refactor UserSession to operate independently of Auth and support explicit session configuration.

Build:

  • Build and install the new ddm-helper executable.

@deepin-ci-robot

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR isolates the complete PAM transaction and desktop-session startup in a newly installed, exec-launched single-threaded ddm-helper, with the daemon communicating through a bounded framed socket protocol and retaining responsibility for orchestration, VT switching, utmp bookkeeping, and session state.

Sequence diagram for authentication and session startup via ddm-helper

sequenceDiagram
    participant D as ddm daemon
    participant H as ddm-helper
    participant P as PAM
    participant S as UserSession

    D->>D: ensureHelper()
    D->>H: fork+exec ddm-helper(socketFd)
    D->>H: sendRequest(Authenticate)
    H->>P: pam_start()
    H->>P: pam_authenticate()
    H->>P: pam_acct_mgmt()
    P-->>H: authentication result
    H-->>D: AuthResult
    D->>H: sendRequest(OpenSession)
    H->>P: pam_setcred()
    H->>P: pam_open_session()
    P-->>H: session environment
    H->>S: start()
    S-->>H: process PID
    H-->>D: SessionResult(XDG_SESSION_ID, PID)
Loading

Flow diagram for PAM session isolation and cleanup

flowchart TD
    Start[Daemon receives login request] --> Spawn[Spawn exec-launched ddm-helper]
    Spawn --> Auth[Helper performs PAM authentication]
    Auth --> Open[Helper performs pam_open_session]
    Open --> ForkSafe[ PAM module may fork safely in isolated process ]
    ForkSafe --> Run[Helper starts desktop session]
    Run --> Exit{Desktop session exits}
    Exit --> Cleanup["Helper calls pam_close_session\nand pam_setcred(PAM_DELETE_CRED)"]
    Cleanup --> Notify[Helper notifies daemon and exits]
Loading

File-Level Changes

Change Details Files
Move PAM authentication and session lifecycle out of the multithreaded daemon into an exec-launched helper process.
  • Add a standalone single-threaded QCoreApplication helper executable linked with PAM and session-launching code.
  • Keep the PAM handle in the helper across authentication and session opening, including credentials, PAM environment, and session cleanup.
  • Launch the helper through QProcess after creating a CLOEXEC socketpair, preserving only the communication descriptor across exec.
src/CMakeLists.txt
src/helper/CMakeLists.txt
src/helper/HelperApp.cpp
src/helper/HelperApp.h
src/daemon/Auth.cpp
src/daemon/Auth.h
Introduce a framed socket protocol for daemon-helper requests and replies.
  • Encode authenticate and open-session requests with QDataStream frames and bounded lengths.
  • Return authentication, session-open, and session-finished results over the socket.
  • Buffer partial reads and drain nonblocking helper input through QSocketNotifier.
src/daemon/Auth.cpp
src/helper/HelperApp.cpp
src/helper/HelperApp.h
Refactor session process startup so UserSession can run independently of Auth and be reused by the helper.
  • Replace the Auth parent dependency with explicit user and TTY configuration.
  • Store the display type, TTY, and user locally for child-process setup and environment handling.
  • Build the helper with the shared UserSession implementation.
src/daemon/UserSession.cpp
src/daemon/UserSession.h
src/helper/CMakeLists.txt
Update daemon-side session lifecycle and failure handling around the helper process.
  • Request authentication and session opening through the helper while retaining VT switching, utmp updates, and returned session metadata in the daemon.
  • Treat helper termination as session completion and terminate the helper when closing a session.
  • Remove daemon-side PAM conversation, direct PAM calls, forked session leader, inherited-FD cleanup, and socket notifier monitoring.
src/daemon/Auth.cpp
src/daemon/Auth.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

PAM cleanup, callback lifetime, environment precedence, session orphaning, and logout accounting have correctness issues. / PAM 清理、回调生命周期、环境优先级、会话孤儿进程及注销记录存在正确性问题。

5 open findings
What changed in this PR

Moves PAM authentication and session management into an isolated helper process to avoid daemon deadlocks.
将 PAM 认证和会话管理迁移至独立 helper 进程,以避免 daemon 死锁。

Changes:

  • Adds framed daemon/helper IPC and PAM lifecycle handling. / 新增 daemon/helper 帧式 IPC 与 PAM 生命周期处理。
  • Decouples UserSession from Auth. / 将 UserSession 与 Auth 解耦。
  • Builds and installs ddm-helper. / 构建并安装 ddm-helper。
File Description
src/​helper/​HelperApp.h Defines helper protocol and state. / 定义 helper 协议与状态。
src/​helper/​HelperApp.cpp Implements PAM and session execution. / 实现 PAM 与会话执行。
src/​helper/​CMakeLists.txt Builds and installs the helper. / 构建并安装 helper。
src/​daemon/​UserSession.h Adds explicit session properties. / 新增显式会话属性。
src/​daemon/​UserSession.cpp Removes dependency on Auth. / 移除对 Auth 的依赖。
src/​daemon/​Auth.h Declares helper IPC management. / 声明 helper IPC 管理。
src/​daemon/​Auth.cpp Delegates authentication and sessions. / 委托认证与会话处理。
src/​CMakeLists.txt Includes the helper target. / 引入 helper 目标。

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/daemon/Auth.cpp
Comment thread src/daemon/Auth.cpp
Comment thread src/helper/HelperApp.cpp Outdated
Comment thread src/helper/HelperApp.cpp Outdated
Comment thread src/helper/HelperApp.cpp Outdated
@wineee
wineee marked this pull request as ready for review October 13, 2026 07:48
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Sorry @wineee, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 23 hours by commenting @sourcery-ai review. Upgrade to get a review now.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

PAM cleanup and forced-session shutdown contain lifecycle and error-reporting defects. / PAM 清理与强制会话关闭存在生命周期和错误报告缺陷。

2 open findings
5 resolved since last review

🧠 Review effort: Balanced

Comment thread src/daemon/Auth.cpp Outdated
Comment thread src/helper/HelperApp.cpp
@wineee
wineee force-pushed the fix branch 2 times, most recently from e56a62c to 5046516 Compare October 13, 2026 12:22
@wineee

wineee commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

完整时间线

┌────────────┬───────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 时间 │ 提交 │ 事件 │
├────────────┼───────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 最初 │ 92a01c0 (init) │ fork sddm,原样继承了独立 helper 架构(src/helper/HelperApp.cpp + src/auth/Auth.cpp,含 QLocalServer/SocketServer/--socket/--id,与 sddm 完全同源) │
├────────────┼───────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-12-16 │ b11c146 │ 重构 PAM 模块,删掉 sddm 的 Backend/PamBackend/PamHandle,合并为单文件 Pam.cpp(此时 helper 仍在) │
├────────────┼───────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-12-18 │ 4d68247 │ ⭐ "refactor: cut off helper" —— 关键转折点 │
├────────────┼───────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2026-10 │ 346ba8f / 5046516 │ 本次 PR:把 PAM 事务迁回独立 ddm-helper │
└────────────┴───────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

⭐ 答案:4d68247 是「砍掉 helper」的那次提交

commit 4d68247

  • 作者:April & May & June(us@calsys.org)
  • 合入者:zccrs
  • 提交信息:
    │ refactor: cut off helper
    │ - Merge HelperApp into Auth
    │ - Move Auth & UserSession into daemon
    │ - Simplify Auth, remove redundant logics

它做的事:

  • 删除 src/helper/HelperApp.cpp(-451 行)、src/auth/*(Auth/AuthMessages/AuthPrompt/AuthRequest 等)
  • 把 Pam.cpp/UserSession.cpp 从 src/helper 移进 src/daemon
  • 把 HelperApp 的逻辑合并进 daemon 的 Auth
  • 删掉 debian/ddm.install 里的 helper 安装项

动机是"简化代码",且提交信息里自己都承认:

│ Note that the function of starting sessions other than treeland will temporarily not work after this commit...

也就是说,这次"简化"把 sddm 原本正确的独立 helper 架构改坏了 —— 正是这个合并动作,让 PAM 事务跑进了 Qt 多线程 daemon 里,埋下了 gnome-keyring fork+exit 死锁的雷。

结论

这次 PR #116(346ba8f)本质上是对 4d68247 那次"简化"的回退——把 2025 年 12 月被砍掉的独立 helper 架构重新恢复回来。所以:

  • 你的直觉是对的:ddm 理论上从一开始就该和 sddm 一样是独立进程
  • 是 4d68247 在 2025-12-18 把它改掉了
  • 本次 PR 修复死锁,本质是回到 sddm 的正确架构,只是用更简单的 socketpair 协议替代了 sddm 原来的 QLocalServer 协议

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Session-exit races, prolonged password retention, and mismatched shutdown timeouts can leak PAM or desktop-session resources. / 会话退出竞态、密码长期驻留及关闭超时不匹配可能泄漏 PAM 或桌面会话资源。

2 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment thread src/helper/HelperApp.cpp Outdated
Comment thread src/helper/HelperApp.cpp
zccrs
zccrs previously approved these changes Oct 14, 2026
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 死锁的问题
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 记录。
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

AI 代码审查报告

项目: linuxdeepin/ddm
PR: #116 — fix(daemon): move PAM transaction into a dedicated session helper
作者: wineee
分支: fix → master
审查时间: 2026-10-14
分析模式: 全量分析(GitHub PR)


总体评价

维度 评分 状态
语法逻辑 23/25 ✓ 通过
代码质量 23/25 ✓ 通过
代码性能 19/20 ✓ 通过
代码安全 28/30 ✓ 通过
总分 93/100 优秀

总体评价结论: 代码审查通过。本次提交将 PAM 事务从多线程 daemon 进程迁移到独立的单线程 helper 进程(ddm-helper),有效解决了 gnome-keyring PAM 模块在 pam_open_session() 中 fork()+exit() 导致的 Qt atexit 死锁问题。架构设计清晰,IPC 协议规范,错误处理完善,安全实践良好。

漏洞对比统计: 新增漏洞 0 个,减少漏洞 0 个,持平 0 个


提交目的分析

本次 PR 的核心目的是修复 DDM(Deepin Display Manager)登录死锁问题:

  • 根因: gnome-keyring 的 PAM 模块在 pam_open_session() 内部 fork() 孙进程并调用 exit(),触发从多线程 daemon 继承的 Qt atexit 处理器,导致 QDBusConnectionManager 析构时 QThread::wait() 死锁
  • 解决方案: 将完整 PAM 事务(认证 + 会话管理)迁移到独立的单线程 ddm-helper 进程,通过 socketpair IPC 与 daemon 通信
  • 设计要点: helper 通过 fork()+exec() 启动(非裸 fork()),从不建立 DBus 连接,保持单线程,使 gnome-keyring 的 fork()+exit() 安全

维度1:语法逻辑(23/25)✓

语法正确,逻辑清晰,边界处理完善

问题列表

  1. Auth::sendRequest() 阻塞式读取 — src/daemon/Auth.cpp
    • sendRequest() 使用阻塞式 ::read() 等待 helper 响应。虽然 helper 进程退出时内核会关闭 socket 导致 read() 返回 0(EOF),但在极端情况下(如 helper 卡死但未退出)可能导致 daemon 阻塞。建议考虑添加超时机制。
    • 严重程度: 轻微问题 (-2分)

正面评价

  • ensureHelper() 正确处理 socketpair 创建、QProcess 启动、失败清理的完整生命周期
  • onReadyRead() 帧解析逻辑正确,妥善处理 EAGAIN/EWOULDBLOCK、EINTR 和 EOF
  • handleOpenSession() 在所有失败路径都调用 closePamSession() 进行清理,无资源泄漏
  • doAuthenticate() 中 m_secret.fill(0) 在认证完成后立即擦除密码,m_secretPtr 保持指向已清零缓冲区(PAM 可能在后续操作中回调 converse()),设计正确
  • closePamSession() 实现为幂等函数,多次调用安全
  • strncpy 配合零初始化结构体 (struct utmpx entry { };) 确保字符串正确终止

维度2:代码质量(23/25)✓

代码结构清晰,注释完整,符合编码规范

问题列表

  1. 认证结果序列化不一致 — src/helper/HelperApp.cpp handleAuthenticate() 函数
    • 使用原始字节 QByteArray("\x01", 1) / QByteArray("\x00", 1) 编码认证结果,而协议中其他消息均使用 QDataStream 序列化。虽然功能正确(QDataStream 的 bool 读写使用 quint8),但风格不一致,降低可读性和可维护性。
    • 建议改为:
      QByteArray payload;
      QDataStream ds(&payload, QIODevice::WriteOnly);
      ds << ok;
      sendMsg(AuthResult, payload);
    • 严重程度: 轻微问题 (-2分)

正面评价

  • 注释质量极高:每个关键函数都有详细注释解释设计意图(如 fork+exec 策略、socket 协议、PAM 生命周期),注释解释"为什么"而非"是什么"
  • 协议定义清晰:HelperApp::Msg 枚举和帧格式在头文件和代码中都有详细文档
  • UserSession 重构干净:从依赖 Auth* 父对象改为使用成员变量(m_tty、m_user、m_type),降低了模块耦合
  • 错误处理路径完整:每个 PAM 调用都检查返回值并记录错误日志
  • 信号连接在进程启动前完成(UserSession 构造函数中连接 finished 信号),避免短命进程退出时丢失信号

维度3:代码性能(19/20)✓

性能良好,资源使用合理,无性能瓶颈

问题列表

  1. 阻塞式 IPC 可能延迟事件循环 — src/daemon/Auth.cpp sendRequest() 函数
    • sendRequest() 在 daemon 端使用阻塞式 read(),在等待 helper 响应期间 daemon 的事件循环被阻塞。对于 PAM 认证和会话打开操作,这是可接受的同步请求-响应模式,但理论上会延迟其他事件处理。考虑到 PAM 操作本身是耗时操作且需要同步等待结果,此设计是合理的性能-复杂度权衡。
    • 严重程度: 轻微建议 (-1分)

正面评价

  • socketpair IPC 开销极低,相比之前的 fork+pipe 方案无额外性能损失
  • helper 端使用 O_NONBLOCK + QSocketNotifier 是正确的异步 I/O 模式
  • 帧解析使用缓冲区流式处理,避免多次内存分配
  • kMaxFrameSize(1 MiB)限制防止内存耗尽攻击
  • writeAll() 使用 send(MSG_NOSIGNAL) 避免不必要的信号处理开销

维度4:代码安全(28/30)✓

存在 0 个安全漏洞

安全漏洞清单: 无

漏洞对比统计: 新增漏洞 0 个,减少漏洞 0 个,持平 0 个

安全实践评估

良好安全实践:

  • 密码处理:m_secret 在认证完成后立即用 fill(0) 擦除,m_secretPtr 安全管理
  • 进程隔离:helper 通过 fork()+exec() 启动,不继承 daemon 的 DBus 连接,SOCK_CLOEXEC 确保其他 fd 不泄露
  • 帧大小验证:kMaxFrameSize(1 MiB)防止内存耗尽
  • MSG_NOSIGNAL:防止 helper 异常退出时 SIGPIPE 杀死 daemon
  • PAM 清理:closePamSession() 幂等且完整(pam_close_session → pam_setcred(PAM_DELETE_CRED) → pam_end)
  • 会话 PID 验证:pid <= 0 检查防止无效进程 ID
  • XDG_SESSION_ID 验证:空值检查防止孤儿桌面进程

安全建议(非漏洞):

  1. handleAuthenticate() 中局部变量 secret(从 QDataStream 反序列化)在 doAuthenticate() 返回后未显式清零。虽然 m_secret 已被 fill(0) 擦除,但 secret 的 QByteArray 在析构时不会清零内存。建议在 handleAuthenticate() 末尾添加 secret.fill(0)。——非常重要
  2. 建议在 ensureHelper() 中验证 LIBEXEC_INSTALL_DIR 路径,防止路径被篡改导致执行恶意 helper 二进制。——非常重要

改进建议

建议代码示例

// 1. handleAuthenticate() 中擦除局部密码变量
void HelperApp::handleAuthenticate(QDataStream &in)
{
    QString user, pamService;
    QByteArray secret;
    in >> user >> pamService >> secret;

    const bool ok = doAuthenticate(user, pamService, secret);
    if (!ok)
        utmpLogin(false);

    // 擦除局部密码变量
    secret.fill(0);

    // 使用 QDataStream 一致序列化
    QByteArray payload;
    {
        QDataStream ds(&payload, QIODevice::WriteOnly);
        ds << ok;
    }
    sendMsg(AuthResult, payload);
}

// 2. sendRequest() 添加超时机制(可选)
bool Auth::sendRequest(quint32 op, const QByteArray &payload, QByteArray &reply)
{
    // ... 现有写入逻辑 ...

    // 使用 poll() 添加超时
    struct pollfd pfd;
    pfd.fd = d->socketFd;
    pfd.events = POLLIN;
    const int timeout_ms = 30000; // 30 秒超时
    for (;;) {
        int ret = ::poll(&pfd, 1, timeout_ms);
        if (ret == 0) {
            qWarning() << "[Auth] helper response timeout";
            return false;
        }
        if (ret < 0) {
            if (errno == EINTR) continue;
            return false;
        }
        // ... 现有读取和帧解析逻辑 ...
    }
}

审查清单

  • 已按四维度标准完成代码分析(语法逻辑、代码质量、代码性能、代码安全)
  • 代码安全维度第一行包含"存在 0 个安全漏洞"
  • 安全漏洞已按等级分类(无漏洞)
  • 包含漏洞对比统计行(新增/减少/持平)
  • 每个维度标记✓/✕与评价词一致
  • 审查结论考虑了 commit message 的目的(修复死锁)
  • 问题定位准确,行号和函数名正确
  • 环境变量使用正确,文件路径无误

@Groveer
Groveer merged commit 76be35a into linuxdeepin:master Oct 14, 2026
8 checks passed
@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Groveer, wineee

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@wineee
wineee deleted the fix branch October 14, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants