Skip to content

fix(session): store logind session id as string to avoid collision - #1465

Merged
zccrs merged 1 commit into
linuxdeepin:masterfrom
wineee:dde-shell-v2
Oct 10, 2026
Merged

zccrs merged 1 commit into
linuxdeepin:masterfrom
wineee:dde-shell-v2

Conversation

@wineee

@wineee wineee commented Oct 9, 2026 •

Copy link
Copy Markdown
Member
  1. Migrate Session::m_id and the SessionManager session-id APIs from int
    to QString
  2. Remove the toInt() validation in onSessionRemoved; look up the raw id
    string so "c1" can no longer collide with the dde sentinel session ("0")
  3. Keep the DDM socket protocol as int for now, converting at the boundary

Log: No user-facing changes

Influence:

  1. Reproduce the crash under a DDM-managed treeland (--lockscreen)
  2. Run a root su/runuser to deepin and confirm the logind session ("c1")
    no longer removes the dde sentinel session
  3. Verify the wallpaper shell protocol still works for the dde user client

fix(session): 用字符串存储 logind 会话 id 避免碰撞

  1. 将 Session::m_id 及 SessionManager 会话 id 接口从 int 迁移为 QString
  2. 移除 onSessionRemoved 中的 toInt 校验,直接按原始字符串查找,
    使 "c1" 不再与 dde 哨兵会话("0")碰撞
  3. DDM socket 协议暂保持 int,在边界处转换

Log: 无用户可见变化

Influence:

  1. 在 DDM 管理的 treeland(--lockscreen)下复现崩溃
  2. 执行 root 的 su/runuser 切换到 deepin,确认 "c1" 会话不再误删 dde 哨兵会话
  3. 验证 dde 用户客户端的 wallpaper shell 协议仍正常

Fixes: #1464

Summary by Sourcery

Preserve raw logind session identifiers to prevent session collisions and improve handling of missing global sessions.

Bug Fixes:

  • Prevent logind session IDs such as "c1" from colliding with the DDE sentinel session ID and avoid crashes when the global session or client socket is unavailable.

Enhancements:

  • Preserve logind session IDs as strings throughout session management while retaining integer conversion only at the DDM socket protocol boundary.

@sourcery-ai

sourcery-ai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Migrates logind session IDs from integers to strings so IDs such as “c1” cannot collide with the DDE sentinel “0”, while preserving protocol compatibility at existing boundaries and adding defensive checks around absent global/session sockets.

Sequence diagram for string-based logind session cleanup

sequenceDiagram
    participant Logind
    participant GreeterProxy
    participant SessionManager
    participant Session

    Logind->>GreeterProxy: onSessionRemoved(id)
    GreeterProxy->>SessionManager: sessionForId(id)
    SessionManager->>SessionManager: Compare raw QString id
    SessionManager-->>GreeterProxy: Matching Session or nullptr
    GreeterProxy->>Session: username()
Loading

File-Level Changes

Change Details Files
Preserve logind session identifiers as QString values throughout session discovery, activation, locking, unlocking, and removal.
  • Changed session IDs and related UserModel/Helper/SessionManager APIs from int to QString.
  • Removed numeric parsing during session removal and lock/unlock handling.
  • Passed raw IDs to logind lookups and socket messages.
src/greeter/greeterproxy.cpp
src/greeter/usermodel.h
src/seat/helper.cpp
src/seat/helper.h
src/session/session.cpp
src/session/session.h
Prevent missing global or client sockets from causing session-management crashes.
  • Added null checks before comparing client sockets or configuring socket enablement.
  • Retained graceful behavior when the DDE sentinel or active session is unavailable.
src/session/session.cpp

Assessment against linked issues

Issue Objective Addressed Explanation
#1464 Prevent the crash caused by dereferencing a null global DDE session or socket while determining whether a client belongs to the DDE user. ✅
#1464 Prevent logind session IDs such as "c1" from being converted to integers and colliding with the DDE sentinel session ID "0", including during session creation, removal, activation, locking, and unlocking. ✅
#1464 Preserve compatibility with the existing DDM socket protocol while using string session IDs internally. ✅

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

@sourcery-ai sourcery-ai Bot 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@wineee

wineee commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

崩溃修复报告:logind 会话移除时误删 dde 哨兵会话

摘要

treeland 在 DDM 锁屏模式下(/usr/bin/treeland --lockscreen)登录后运行一段时间会崩溃,直接原因是
SessionManager::isDDEUserClient() 中解引用了空指针。

根因是 GreeterProxy::onSessionRemoved() 对 logind 会话 id 字符串使用了 QString::toInt(),
而日志会话 id 中的无 seat 会话(如 "c1")是非数字字符串,toInt() 转换失败时会返回
0,恰好命中 treeland 用 id=0 创建的 dde 哨兵会话,导致该会话被误删。后续任何客户端绑定
wallpaper shell 全局对象时,globalSession() 返回空指针并触发空指针解引用崩溃。

影响版本

  • treeland 0.10.0.22(0.10.0.22-...)

崩溃现场

#0  0x00007014c50e3764 in Session::socket (this=0x0) at src/session/session.cpp:106
#1  0x00007014c50e59dd in SessionManager::isDDEUserClient (this=..., client=...) at src/session/session.cpp:493
#2  0x00007014c4f609cf in operator() (__closure=..., client=...) at src/core/shellhandler.cpp:501
#3  std::__invoke_impl<...>
#4  std::__invoke_r<...>
#5  std::_Function_handler<...>::_M_invoke(...)
#6  std::function<bool(WClient*)>::operator()(...)
#7  Waylib::Server::globalFilter (client=..., global=..., data=...) at waylib/src/server/kernel/wserver.cpp:56
#8  (libwayland-server.so.0)
#9  (libffi.so.8)
...
#14 wl_event_loop_dispatch
#15 Waylib::Server::WServerPrivate::processWaylandEvents at waylib/src/server/kernel/wserver.cpp:204

关键代码:

// src/session/session.cpp:493
bool SessionManager::isDDEUserClient(WClient *client)
{
    return client->socket() == globalSession()->socket();
}

globalSession() 内部实现为 sessionForUser("dde"),当 dde 会话已不在 m_sessions 中时返回
nullptr,随后 nullptr->socket() 触发段错误。

根因分析

1. dde 哨兵会话使用 id=0

启动时(src/seat/helper.cpp):

// User dde does not has a real Logind session, so just pass 0 as id
m_sessionManager->updateActiveUserSession(QStringLiteral("dde"), 0);

dde 是一个不对应真实 logind 会话的哨兵会话,以 id=0 创建。

2. logind 无 seat 会话的 id 是非数字字符串

systemd-logind 的会话 id 命名规则:

id 格式 含义
1, 2 分配了 seat 的图形/登录会话
c1, c2 无 seat、无 VT 的后台会话(audit id 不可用时由计数器回退生成)

journal 中的证据:

Sending reply about created session: id=c1 object_path=/org/freedesktop/login1/session/c1 uid=1000 runtime_path=/run/user/1000 session_fd=41 seat= vtnr=0

seat= 为空、vtnr=0 是无 seat 会话的标志。

3. QString::toInt() 转换失败回退为 0

// src/greeter/greeterproxy.cpp(修复前)
auto session = Helper::instance()->sessionManager()->sessionForId(id.toInt());

logind 通过 org.freedesktop.login1.Manager.SessionRemoved 信号把会话 id 作为字符串传递。
对于无 seat 会话 "c1",QString("c1").toInt() 转换失败时返回 0,从而误匹配到 id=0 的 dde
哨兵会话。

为什么会出现 c1/c2 这种非数字 id

$XDG_SESSION_ID 是不透明字符串,其数字值通常取自 audit session id
(/proc/self/sessionid,见 pam_systemd(8))。当 audit 不可用或取不到 audit id 时,
systemd-logind 回退为独立的会话计数器。

systemd-logind 二进制中的格式串 c%lu 表明,这种回退生成的 id 会带上 c 前缀。因此
root 通过 su/runuser 切到普通用户时产生的无 seat 后台会话,id 形如 c1、c2,它们
永远无法被 toInt() 正确解析。

4. 完整触发链

deepin-appstore-checkappupgrade.timer
  → com.home.appstore.daemon / deepin-service-manager -g app
    → runuser / su 切到 deepin(root → deepin)
      → pam_systemd 向 logind 注册无 seat 会话 c1、c2(seat 为空)
        → su 退出 → logind 发 SessionRemoved("c1")、SessionRemoved("c2")
          → treeland 的 GreeterProxy::onSessionRemoved 收到
            → "c1".toInt() == 0 → 误删 id=0 的 dde 哨兵会话
              → 之后客户端绑定 treeland_wallpaper_shell_v1 全局对象
                → globalSession() 返回 nullptr
                  → nullptr->socket() → SIGSEGV

时间线:

09:50:13  treeland 启动,创建 dde 会话 (id=0)
09:50:25  deepin 登录,创建 deepin 会话 (id=1),成为 active
09:55:57  systemd-logind: New session c1 of user deepin.
09:55:57  systemd-logind: Removed session c1.
09:55:57  systemd-logind: New session c2 of user deepin.
09:55:57  systemd-logind: Removed session c2.
09:58:58  treeland SIGSEGV(用户触发应用启动器,绑定 wallpaper shell 协议)

崩溃发生在会话被误删约 3 分钟后,解释了"登录后过一段时间才崩溃"的现象。

修复方案

对 GreeterProxy::onSessionRemoved() 中的会话 id 字符串做严格数字校验,非法 id 直接忽略。

void GreeterProxy::onSessionRemoved(const QString &id, [[maybe_unused]] const QDBusObjectPath &path)
{
    bool ok = false;
    const int sessionId = id.toInt(&ok);
    if (!ok) {
        // logind sessions without an audit session id use non-numeric ids
        // like "c1"; they never map to a SessionManager session, so ignore
        // them instead of letting toInt()'s 0 fallback collide with the dde
        // sentinel session (id=0).
        qCDebug(lcTlGreeter) << "Ignoring SessionRemoved with non-numeric session id:" << id;
        return;
    }
    ...
    auto session = Helper::instance()->sessionManager()->sessionForId(sessionId);
    ...
}

修复后 "c1"、"c2" 等非数字 id 不再被转换成 0,因此不会误删 id=0 的 dde 哨兵会话。

修复验证

  • 构建通过:cmake --build --preset ci --target treeland(含 -Wall -Wextra -Werror,无告警)。
  • 日志使用集中化分类 lcTlGreeter(treeland.greeter),遵循 logging-guidelines。

建议的测试项

  1. 在 DDM 管理的 treeland(--lockscreen)下复现崩溃。
  2. 执行 root 的 su/runuser 切换到 deepin,确认无 seat 会话 "c1" 不再误删 dde 哨兵会话。
  3. 验证 dde 用户客户端的 wallpaper shell 协议仍正常。

关联信息

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No unresolved blocking issues were identified.

0 open findings

What changed in this PR

Fixes logind session removal by ignoring non-numeric IDs such as c1, preventing accidental removal of the DDE sentinel session.

Changes:

  • Validate session IDs before conversion.
  • Ignore invalid IDs with debug logging.
  • Use validated IDs for session lookup.
File Description
src/​greeter/​greeterproxy.cpp Safely validates and handles removed logind session IDs.

🧠 Review effort: Lite


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

@wineee

wineee commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

研究完成。基于 systemd 源码,我可以给你完整的、有源码依据的结论了。

一、c1/c2 的确切生成规则(源码证据)

src/login/logind-dbus.c → manager_choose_session_id()(约第 838-880 行):

  /* Try to keep our session IDs and the audit session IDs in sync */                                   
  uint32_t audit_id = AUDIT_SESSION_INVALID;                                                            
  r = audit_session_from_pid(leader, &audit_id);                                                        
  if (r < 0) {                                                                                          
      // 读 audit id 失败 → 进入下面 fallback                                                           
  } else {                                                                                              
      asprintf(&id, "%"PRIu32, audit_id);   // 数字 id                                                  
      ...                                                                                               
  }                                                                                                     
                                                                                                        
  if (!id)                                                                                              
      do {                                                                                              
          id = mfree(id);                                                                               
          if (asprintf(&id, "c%" PRIu64, ++m->session_counter) < 0)   // ← c 前缀来源                   
              return -ENOMEM;                                                                           
      } while (hashmap_contains(m->sessions, id));                                                      

结论:

  • 能读到 audit session id → id 是数字(%PRIu32)
  • 读不到 → 回退为 c + 自增计数器(c%lu),即 c1、c2

src/basic/audit-util.c → audit_session_from_pid():

  r = audit_read_field(pid, "sessionid", &s);   // 读 /proc/<pid>/sessionid                             
  ...                                                                                                   
  if (!audit_session_is_valid(u))               // id > 0 && id != UINT32_MAX                           
      return -ENODATA;                                                                                  

读的是 /proc/<leader_pid>/sessionid,这个文件由内核 audit 子系统(pam_loginuid.so 设置 loginuid 后)提
供。su/runuser 这种不经过完整 PAM login 流程的切用户操作,进程的 /proc/self/sessionid 是 4294967295(即
UINT32_MAX = invalid),所以 audit_session_from_pid 返回 -ENODATA,logind 就走了 c 前缀 fallback。

而 DDM 的图形登录走完整 PAM(pam_loginuid.so),leader 进程有有效 audit session id(如 1),所以 id 是
数字。

二、为什么 onSessionRemoved 拿不到 service(源码证据)

src/login/logind-session.c → session_finalize()(约第 1043 行起):

  int session_finalize(Session *s) {                                                                    
      ...                                                                                               
      if (s->started)                                                                                   
          log_struct(..., LOG_MESSAGE("Removed session %s.", s->id));   // ← 先打 "Removed session c1." 
                                                                                                        
      ...                                                                                               
      if (s->started) {                                                                                 
          session_send_signal(s, false);   // ← 然后发 SessionRemoved 信号                              
          s->started = false;                                                                           
      }                                                                                                 
      ...                                                                                               
      session_add_to_gc_queue(s);          // ← 加入 GC 队列                                            
      ...                                                                                               
  }                                                                                                     

session_send_signal()(logind-session-dbus.c:839):

  sd_bus_emit_signal(..., new_session ? "SessionNew" : "SessionRemoved", "so", s->id, p);               

随后 GC 队列处理(logind.c:1095)会调用 session_free(),其中:

  free(s->service);          // ← 释放 service 字符串                                                   
  hashmap_remove(s->manager->sessions, s->id);   // ← 从 hashmap 移除                                   
  free(s->id);                                                                                          

关键时序:

  1. session_finalize() 里先打印 "Removed session c1.",再发 SessionRemoved 信号;
  2. 信号通过 D-Bus 异步派发到 treeland 时,会话已被加入 GC 队列(可能已 session_free);
  3. 即使还没 free,session_get_service 依赖的 s->service 字段仍在,但对象路径
    /org/freedesktop/login1/session/c1 已不可用,sd_session_get_service("c1") 走的是 session 对象查询,
    返回 -ENXIO。

我用实际程序验证过(查一个不存在的会话 c999):sd_session_get_service 返回 -6(ENXIO)。

对比 SessionNew:session_start() 里是先创建好会话对象、填充 service、加入 hashmap,然后才发 SessionNew
信号。所以 onSessionNew 能查到 service。

三、结论(回答你最初的问题)

┌────────────────────────────┬──────────────────┬─────────────────────────────┐
│ │ onSessionNew │ onSessionRemoved │
├────────────────────────────┼──────────────────┼─────────────────────────────┤
│ 信号到达时会话对象 │ 已创建、存活 │ 已 finalize/进入 GC,查不到 │
├────────────────────────────┼──────────────────┼─────────────────────────────┤
│ sd_session_get_service │ ✅ 返回 "ddm" 等 │ ❌ 返回 ENXIO │
├────────────────────────────┼──────────────────┼─────────────────────────────┤
│ 能否用 service=="ddm" 过滤 │ ✅ 能 │ ❌ 不能(时序上做不到) │
└────────────────────────────┴──────────────────┴─────────────────────────────┘

所以 onSessionRemoved 不是"忘了判断 service",而是 systemd 的 SessionRemoved 信号在会话销毁后才发出,回
调里根本无法再用 sd_session_get_service 判断服务类型。

四、对 onSessionNew 是否加 toInt 检查的最终建议

基于源码,现在可以更准确地判断:

  • onSessionNew 里 id.toInt() 能执行到,前提是 sd_session_get_service 返回成功且 service=="ddm"。
  • DDM 会话走完整 PAM login 流程,leader 有有效 audit id,manager_choose_session_id 一定走数字分支
    (%PRIu32),所以 DDM 会话的 id 在理论上一定是纯数字。
  • 因此 onSessionNew 的 id.toInt() 在当前 systemd 语义下不会失败,加检查纯粹是防御性编程,收益很低。

我的建议:不需要改 onSessionNew。 它是安全的,因为:

  1. 有 service=="ddm" 守卫(DDM 会话才有有效 audit id → 数字 id);
  2. systemd 源码保证 DDM 图形会话 id 一定是数字。

当前修复(只改 onSessionRemoved)已经完整覆盖了根因。

@wineee
wineee force-pushed the dde-shell-v2 branch 2 times, most recently from f5cd407 to b3e88ab Compare October 9, 2026 10:32
@wineee
wineee marked this pull request as draft October 10, 2026 01:33
@wineee
wineee requested a lite review from Copilot October 10, 2026 01:36
@wineee wineee changed the title fix: ignore non-numeric logind session ids on removal fix(session): store logind session id as string to avoid collision Oct 10, 2026
@wineee

wineee commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

linuxdeepin/ddm#114

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Critical DDM socket boundary conversions are missing, causing legacy protocol incompatibility.

3 open findings

🧠 Review effort: Lite

Comment thread src/greeter/greeterproxy.cpp
Comment thread src/session/session.cpp
Comment thread src/session/session.h
1. Migrate Session::m_id and the SessionManager session-id APIs from int
   to QString
2. Remove the toInt() validation in onSessionRemoved; look up the raw id
   string so "c1" can no longer collide with the dde sentinel session ("0")
3. Keep the DDM socket protocol as int for now, converting at the boundary

Log: No user-facing changes

Influence:
1. Reproduce the crash under a DDM-managed treeland (--lockscreen)
2. Run a root su/runuser to deepin and confirm the logind session ("c1")
   no longer removes the dde sentinel session
3. Verify the wallpaper shell protocol still works for the dde user client

fix(session): 用字符串存储 logind 会话 id 避免碰撞

1. 将 Session::m_id 及 SessionManager 会话 id 接口从 int 迁移为 QString
2. 移除 onSessionRemoved 中的 toInt 校验,直接按原始字符串查找,
   使 "c1" 不再与 dde 哨兵会话("0")碰撞
3. DDM socket 协议暂保持 int,在边界处转换

Log: 无用户可见变化

Influence:
1. 在 DDM 管理的 treeland(--lockscreen)下复现崩溃
2. 执行 root 的 su/runuser 切换到 deepin,确认 "c1" 会话不再误删 dde 哨兵会话
3. 验证 dde 用户客户端的 wallpaper shell 协议仍正常

Fixes: linuxdeepin#1464
@wineee
wineee marked this pull request as ready for review October 10, 2026 02:53

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/greeter/greeterproxy.cpp" line_range="534" />
<code_context>
         case DaemonMessages::UserActivateMessage: {
             QString user;
-            int sessionId;
+            QString sessionId;
             input >> user >> sessionId;

</code_context>
<issue_to_address>
**Session IDs break DDM messages**

When DDM exchanges session-ID fields using its existing integer socket protocol, `readyRead()` reads activation and login IDs as `QString`, while the `Logout` and `Lock` writers serialize `Session::id()` as a string. The mismatched `QDataStream` types make DDM session messages fail to parse, so activation, recovery, locking, or logout fails.

Keep session IDs as integers at DDM socket boundaries, converting to or from `QString` only for internal session handling.

Also at `src/greeter/greeterproxy.cpp:422-423`, `src/greeter/greeterproxy.cpp:567-568`, `src/session/session.h:25`, `src/session/session.h:41`, `src/session/session.cpp:89`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/greeter/greeterproxy.cpp
@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

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

@zccrs
zccrs merged commit 8ad7231 into linuxdeepin:master Oct 10, 2026
7 checks passed
@wineee
wineee deleted the dde-shell-v2 branch October 10, 2026 05:10
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.

打开企业微信时偶现一次崩溃

4 participants