lightdm 丢失了 Ambient capabilities

2025年8月调试的记录,之前保存在语雀。已经提交 lightdm PR

引言

我遇到了一个与XFCE4桌面环境相关的挑战,登录到 XFCE 桌面不支持ambient capabilities。为了更好地理解和解决这个问题,我进行了深入的调试和研究。本文将详细介绍我的调试过程,包括环境搭建、调试步骤、以及最终的解决方案。

问题描述

从Lightdm登录XFCE4桌面,用户启动的程序没有Ambient Capabilities。为什么需要Ambient Capabilities?因为希望某一个用户启动的所有进程都有某一个能力。通过tty的login登录到桌面,pam_cap设置的ambient capabilities是可以被保留的,gdm登录到gnome桌面环境ambient capabilities也是可以被保留的,而lightdm登录到xfce4 ambient capabilities却是空的。

环境搭建

为了进行调试,我首先搭建了调试所需的环境,并复现问题。具体步骤如下:

  1. 安装Fedora 42
    • 使用命令vagrant init bento/fedora-42 初始化 Vagrantfile。
    • 使用命令vagrant up 启动 Fedora-42 虚拟机。
  2. 安装XFCE4桌面环境
    • 使用命令dnf install @xfce-desktop安装XFCE4桌面环境。
  3. 安装调试信息 (可选,后面调试的时候再安装也不迟)
    • 使用命令dnf debuginfo-install lightdm安装lightdm的调试信息。
    • lightdm需要的调试不止是 lightdm 本身,还有它用到的其他包(例如 PAM)的调试信息。Fedora 的 gdb 会提示我们安装缺失的调试信息。

复现问题

使用 pam_cap 为 vagrant 用户设置CAP_NET_BIND_SERVICE能力,vagrant 不需要 sudo 就能使用 1024 以下的端口。

在 tty 下通过 login 登录,pam_cap 设置的 ambient set 可以保留,具体步骤如下:
  1. 在 login 的 pam 配置中设置 pam_cap,vagrant ssh登录到虚拟机,修改 /etc/pam.d/login:
--- /etc/pam.d/login before modification
+++ /etc/pam.d/login after modification
@@ -1,6 +1,7 @@
 #%PAM-1.0
 auth       substack     system-auth
 auth       include      postlogin
+auth      optional     pam_cap.so keepcaps defer
 account    required     pam_nologin.so
 account    include      system-auth
 password   include      system-auth
  1. 为 vagrant 用户的 ambient set 添加 cap_net_bind_service,修改 /etc/security/capability.conf:
--- /dev/null
+++ /etc/security/capability.conf
@@ -0,0 +1 @@
+^cap_net_bind_service vagrant
  1. 通过 tty 登录 vagrant,capsh --print 看到 Ambient set 有 cap_net_bind,我们可以不需要切换root用户就能使用 1024 以下的端口。

通过 lightdm 登录,pam_cap 设置的 ambient 能力丢失了,具体验证步骤如下:
  1. 在 lightdm 的 PAM 配置中设置 pam_cap。
--- /etc/pam.d/lightdm before modification
+++ /etc/pam.d/lightdm after modification
@@ -6,6 +6,7 @@
 -auth       optional    pam_kwallet5.so
 -auth       optional    pam_kwallet.so
 auth       include     postlogin
+auth       optional     pam_cap.so keepcaps defer
 account    required    pam_nologin.so
 account    include     system-auth
 password   include     system-auth
  1. 和上面的 login 验证步骤一样,修改 /etc/security/capability.conf。
  2. 运行下面的命令启动lightdm.service登录到 xfce 桌面:
systemctl restart lightdm

Ambient set 是空的,Inherited Set 有一个 cap_wake_alarm,这个能力不是我们设置的,是系统默认配置自带的。

后面通过调试我们能找到是 pam_systemd 设置了cap_wake_alarm能力,pam_systemd 的文档也提到了这一点。

去掉 pam_cap 的 defer 参数后仍旧如此:

为了找到为什么 lightdm 丢失了 pam_cap 设置的 ambient caps,先将 pam_systemd 模块的配置先去掉,修改/etc/pam.d/system-auth:

--- /etc/pam.d/system-auth before modification
+++ /etc/pam.d/system-auth after modification
@@ -16,6 +16,6 @@

 session     optional                                     pam_keyinit.so revoke
 session     required                                     pam_limits.so
--session    optional                                     pam_systemd.so
+# -session    optional                                     pam_systemd.so
 session     [success=1 default=ignore]                   pam_succeed_if.so service in crond quiet use_uid
 session     required                                     pam_unix.so

重启lightdm登录桌面,pam_cap 的配置生效了

现在没有设置defer参数,inherited set有 ambient set 丢失了。我们加上defer参数反而 pam_cap 应该设置的能力,全都没有

以上就是lightdm丢失lightdm的问题。

systemd 设置了 cap_wake_alarm

systemd 老版本的 pam_systemd 不会设置权能,所以在有的发行版上 pam_systemd 不会影响pam_cap

login 如果配置了 pam_cap 不会受到 pam_systemd 的影响,修改/etc/pam.d/login的 pam_cap 只有 keepcaps 参数:

--- /etc/pam.d/login before modification
+++ /etc/pam.d/login after modification
@@ -1,7 +1,7 @@
 #%PAM-1.0
 auth       substack     system-auth
 auth       include      postlogin
-auth       optional     pam_cap.so keepcaps defer
+auth       optional     pam_cap.so keepcaps
 account    required     pam_nologin.so
 account    include      system-auth
 password   include      system-auth

从 tty 登录,pam_cap 设置了能力,因为有 cap_net_bind_service 这个能力,但是这个能力只在 Inherited set 有,Ambient set 还是空的。

调试过程

一边补知识,一边尝试。

前置知识:

PAM 和 Linux capabilities 一样难搞懂,写这篇博客的时候我暂时还不知道一个 PAM 应用该怎么写,我只知道有几个 API,大概会是什么顺序调用。有这些知识基本上也够用了。

一篇名为 'pam_cap not working with su for ambient setting' 的帖子记录了 ambient cap 怎么在 setuid 之后保留。

Lightdm 是一个 Dispaly Manager,也被称为 Login Manager。Arch Wiki 的 Display Manager 引用了下面这篇文章,讨论了 Display Manager 的实现。

有了前边的知识,可以了解到关键在 setuid() 后,有没有设置 Ambient cap,正如前面的帖子(https://bugzilla.kernel.org/show_bug.cgi?id=212945#c3)中所讲,下面的调用顺序是关键。

prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0, 0);  /* [*] */
cap_iab_t iab = cap_iab_get_proc();

setuid(uid);

cap_iab_set_proc(iab);
cap_free(iab);

pgrep、ps、pstree查看有哪些进程,当我还没登录的时候 Lightdm (pgrep -x lightdm)有三个进程。面对这样的情况,我想我只需要看函数调用的顺序是不是像上面描述的那样,那我就 strace所有的 lightdm 进程,登录到桌面,再回头查看 strace 的输出。总之最后没有看出来 setuid 后有设置 capabilities。

从 strace 的输出可以发现,setuid 是由哪个进程调用的。我们想跟踪到 setuid 就要跟踪 pid 最大的 lightdm 进程。不过我一开始就是gdb -p <lightdm pid>随便跟踪一个……最后发现跟踪 pid 最大的 lightdm 可以跟踪进 PAM 模块的一些函数。

调试的过程不是一帆风顺的,没符合预期就重启 lightdm 服务,再重新登录。

pgrep -x lightdm,gdb -p <max lightdm pid>,systemctl restart lightdm 这几个命令反复运行。可能手动执行了有十几次或者更多,终于意识到这样下去不行。

为了简化调试过程,我编写了一个脚本debug.sh,这个脚本可以自动化启动gdb的过程,避免了每次手动查找PID和启动gdb的繁琐操作

#!/usr/bin/env bash

while true; do
    pids=$(pgrep -x lightdm)
    count=$(echo "$pids" | wc -w)
    if [[ "$count" -ne 3 ]]; then
        echo "Warning: Expected 3 lightdm processes"
        pgrep -x lightdm -l
        echo -n "Do you want to restart lightdm.service? [y/N] "
        read -e -n 1 ans
        if [[ "$ans" == "y" ]]; then
            sudo systemctl restart lightdm
            sleep 4
        else
            exit 1
        fi
    else
        break
    fi
done

sudo gdb -x init.gdb -p $(pgrep -x lightdm | sort -nr | head -n1)

这个脚本也是经过几次迭代了的,一开始直接重启 lightdm 再 gdb 跟踪。服务重启需要时间,必须要 sleep 几秒钟等待,如果还没有登录进桌面我就不需要重启 lightdm。登录到桌面后 lightdm 的进程就只有 2 个了,所以如果有 3 个 lightdm 的进程我就当作是还没登录。

一开始没有 init.gdb,甚至我都不知道 gdb -x,所以我在这次调试的过程中学到了很多。

我调试到某一行代码想查看一下进程的能力,在 gdb 运行 info proc看 PID,到另外一个窗口运行 grep Cap /proc/<pid>/status,getpcaps --iab <pid>,来来回回这样,看一个进程的能力要切换窗口,要手动运行两道三条命令,而且要把 PID 记住,每次都要手动敲 grep 后面的路径。这样下去不行,非常打断思路,而且手动敲路径手都酸了。

写一个 gdb 的插件,添加一个 pcap的命令,在 gdb 内就能查看当前进程的能力。

# Author: DeepSeek V3
import gdb
import subprocess
import re

class PcapCommand(gdb.Command):
    """Display process capabilities (pcap)"""

    def __init__(self):
        super(PcapCommand, self).__init__("pcap", gdb.COMMAND_STATUS)

    def invoke(self, arg, from_tty):
        # Get the current process ID
        pid = gdb.selected_inferior().pid

        if pid == 0:
            print("No process is being debugged")
            return

        try:
            # Use getpcaps to show capabilities
            output = subprocess.check_output(['getpcaps', '--iab', str(pid)], stderr=subprocess.STDOUT)
            print(output.decode('utf-8').strip())

            # Alternative: read from /proc/<pid>/status
            try:
                with open(f'/proc/{pid}/status', 'r') as f:
                    for line in f:
                        if line.startswith('Cap'):
                            print(line.strip())
            except IOError:
                pass

        except subprocess.CalledProcessError as e:
            print(f"Error getting capabilities: {e.output.decode('utf-8').strip()}")
        except Exception as e:
            print(f"Error: {str(e)}")

PcapCommand()

这个命令使用起来效果如下:

我希望每次启动 gdb 都能自动加载 gdb-pcap.py,创建一个 init.gdb,使用 gdb -x加载init.gdb。

# Instruct GDB not to stop on SIG33
handle SIG33 nostop

source gdb-pcap.py

运行 debug.sh ,gdb 会提示我们缺失哪些调试信息的包,我们安装这些包就能在 gdb 里面看到源码。

我们停在了 pam_authenticate,我们可以在 lightdm 输入 vagrant 的密码回车。

打个断点到 337 行,pam_cap是auth模块,看看 pam_authenticate 后进程的能力。

找找代码,在633行会 fork,fork 后的子进程调用了 setuid。

打个断点跟踪这个子进程。下面这个图片的上面运行了

(gdb) b 633
(gdb) c

我们跟踪 fork 出来的子进程,现在进程的能力还是满的,ambient cap 还没设置。直到 setuid 之前,进程的能力都是满的。

执行 setuid 后进程的能力被清空了。

直到 execve,进程的能力都没有再设置。

我们没有跟踪到 pam_cap 的代码。setuid 后 CapPrm 是空的,说明prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0, 0);没有被调用。那就是可能 lightdm 没有配置 pam_cap。我在写博客的过程中尝试了 dnf reinstall lightdm覆盖了前面的 /etc/pam.d/lightdm。如果 lightdm 的pam配置和复现问题部分的配置一样,setuid 后 CapPrm 应该是会被保留。如下:

Permitted Set 中有的能力,才能被添加。所以,Permitted 没有被清空,我们才能在 setuid 后恢复/设置 Ambient caps。

可直到 execve 能力也没有被恢复。

略微调整init.gdb,在关键的几个地方设置断点。当运行到准备 fork 的时候自动设置跟踪子进程。

b pam_cap.c:pam_sm_setcred

# breakpoint before fork()
b session-child.c:633

# breakpoint before setuid()
b session-child.c:646

# Follow child after fork()
commands 2
        set follow-fork-mode child
end


source gdb-pcap.py

调试过程看起来很简短,我只介绍了最关键的几个地方,没有贴出 pam_dispatch 部分的跟踪。在对代码库不熟悉的情况下找到这最关键的几个路径,就是尝试,试试看跟踪新 fork 出来的进程能不能找到关键的代码。


解决方案

login程序支持 Ambient capabilities 的 PR 就添加了一行代码

这个 PR 的解决办法具体的函数调用顺序和 su 不支持 Ambient Capabilities 的解决办法一样。

但是没有手动在 setuid() 前后写 libcap API 的调用。

   cap_iab_t iab = cap_iab_get_proc();

   setuid(uid);

   cap_iab_set_proc(iab);

pam_cap 的 keepcaps 会在 pam_sm_setcred()被调用的时候调用(void) cap_prctlw(PR_SET_KEEPCAPS, 1, 0, 0, 0, 0);。

pam_cap 的 defer会将设置能力的函数作为 cleanup()函数,等到 pam_end()的时候才被调用。

See also:pam_cap(8),pam_end(3)

login就加了一行 pam_end(), pam_end()就是调用之前设置的cleanup()回调函数。我理解需要调用 pam_end()才会执行PAM模块设置的 cleanup回调,但是没有地方说了 pam_end 应该在子进程中调用。

This argument can be logically OR'd with PAM_DATA_SILENT

to indicate that the module should not treat the call too

seriously

我不理解什么是 not treat the call too seriously。

前面提到 pam_systemd 设置了 cap_wake_alarm。也有其他人对 PAM_DATA_SILENT 感到疑惑。

有很多使用 PAM 的程序没有遵守所谓的 PAM 应用规范,但是对于 pam_end的手册,我确实也理解不到 PAM 应用规范是什么。

总之,结论就是一个符合 PAM 规范的应用,应该在 fork 的子进程调用 pam_end() 带着PAM_DATA_SILENT这个 flag。

个人吐槽

我直觉上认为这样的用法十分依赖对内部实现的了解,PAM 的配置也很复杂,我希望有一个更简洁,更易用的替代方案。就我个人的理解,pam_end() 就应该只是释放 pam_handle_t。

pam_end() 既要承担释放,还要承担应用和 PAM 模块的沟通。这确实很令人费解。PAM 本身没有所谓的defer 机制,defer 实际上是 cleanup,clean 操作才是实际的业务逻辑这有点不太恰当。

如果 PAM 专门设置了 before end 这样的 hook,在语义上要更明确,但在实际使用的时候 PAM 应用,还是要注意在合适的地方触发 before end。这样 PAM 就更复杂了。所以现在就这样倒也无妨,已经有大量的程序都使用了 PAM,而且 PAM_DATA_SILENT 在手册中也有一个解释,只是大部分的应用的作者们在写代码的时候,可能意识不到我需要在子进程里面调用 pam_end(...., flags | PAM_DATA_SILENT)。

这也怪不得作者们,谁怪这个参数的含义如此隐晦呢。2021 年的时候 pam_cap 支持了 Ambient caps,login 和 su 得到了修复。libcap 的维护者,和知道这个问题存在的人们似乎也没有想尽量把可能没有正确调用 pam_end的应用都修复。一个原因可能是 Ambient Caps 虽然已经得到支持很长时间了,但是大部分的应用还没有用起来。

Microsoft Copilot 解释 pam_end 中的 PAM_DATA_SILENT

在计算机领域,“tear-down process” 的本质(nature of the tear-down process)通常指的是在某个操作或事务结束时,系统或程序所采取的清理、释放资源、终止连接等一系列步骤。这个过程的“本质”描述的是它的状态、目的和影响范围。

以下是更具体的解释:


🧩 什么是 Tear-down Process?

  • 定义:tear-down 是指在一个操作完成后,系统将其相关资源(如内存、文件句柄、网络连接等)清理和释放的过程。
  • 常见场景:
    • 程序退出时释放内存
    • 网络连接关闭时释放 socket
    • 用户认证结束后清除会话数据(如 PAM 模块中的 pam_end())

🔍 “Nature” 的含义

当我们说 “nature of the tear-down process”,我们是在探讨这个过程的性质或状态,比如:

  • ✅ 成功或失败:tear-down 是正常完成,还是因为错误而中断?
  • 🔕 是否静默执行:是否需要通知模块或用户?是否在子进程中执行而无需完全清理?
  • 🧼 清理程度:是彻底清理所有资源,还是只做部分清理(例如 fork 后子进程只清理自己)?

例如在 Linux 的 PAM(Pluggable Authentication Modules)中,pam_end() 函数会根据传入的状态参数通知模块 tear-down 的性质,从而决定是否执行额外的清理操作。


🛠 举个例子:PAM 模块中的 tear-down

int pam_end(pam_handle_t *pamh, int pam_status);

  • pam_status 表示 tear-down 的性质:
    • 如果是 PAM_SUCCESS,说明事务成功结束
    • 如果是 PAM_SYSTEM_ERR,说明有系统错误
    • 可以加上 PAM_DATA_SILENT,表示不需要模块做太多处理(比如在 fork 的子进程中)

模块可以根据这个状态决定是否执行 cleanup() 回调函数。


如果你是在做操作系统课程的教学或文档整理,这个概念可以帮助学生理解资源管理的生命周期,特别是在系统调用、进程管理或认证模块中。需要我帮你把这个概念整理成教学材料或图示吗?

最后的解决补丁如下:

From e74d1b8b4dab00ad743d57bf1ddd61e199050843 Mon Sep 17 00:00:00 2001
From: wngtk <wngtk@outlook.com>
Date: Thu, 21 Aug 2025 15:07:09 +0000
Subject: [PATCH] Add support for ambient capabilities

---
 src/session-child.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/session-child.c b/src/session-child.c
index 17cb759..e488bed 100644
--- a/src/session-child.c
+++ b/src/session-child.c
@@ -667,8 +667,12 @@ session_child_run (int argc, char **argv)
         /* Reset SIGPIPE handler so the child has default behaviour (we disabled it at LightDM start) */
         signal (SIGPIPE, SIG_DFL);

+        /* Apply IAB capabilities (pam_cap.so keepcaps defer) */
+        char * const *command_envp = pam_getenvlist (pam_handle);
+        (void) pam_end (pam_handle, PAM_SUCCESS | PAM_DATA_SILENT);
+
         /* Run the command */
-        execve (command_argv[0], command_argv, pam_getenvlist (pam_handle));
+        execve (command_argv[0], command_argv, command_envp);
         _exit (EXIT_FAILURE);
     }

--
2.50.1

制作补丁并打包测试的过程不再赘述,大致过程如下:

使用fedpkg下载和构建lightdm的源码包

- 使用rpm -qi lightdm或rpm -qif <file path>找到源码包的名字。
- 使用fedpkg co -a lightdm下载lightdm的源码包。
- 修改session-child.c文件后,使用fedpkg local -- --noprep构建,并安装修改后的包

总结

在问题解决后,如果对 pam_cap 模块熟悉、PAM 应用开发熟悉似乎不需要用上 gdb 都能找到解决方案,因为最关键的就是在 setuid 后再设置 ambient caps,出问题的地方大概率是 setuid 后没有设置 ambient caps 或者有什么地方把 ambient caps 给覆盖了。

如果一个问题原本 Google 一下就能解决,却用 gdb 或者其他调试工具揪了很多细节,后来发现别人已经解决过这个问题了。这会给我一种杀鸡用牛刀的感觉。

如果有足够的背景知识,对 Linux capabilities(7) 和 pam_cap 设置 ambient capability 的过程和原理了解,只需稍稍借助 gdb 看看 setuid 后是否设置了 ambient capability,甚至只需要搜索到代码看一下代码是怎么写的就能知道该怎么解决。

我没有足够的知识,我遇到这个问题的时候,对 Linux 能力的了解仅限于通过capsh --print可以查看我现在启动一个程序能有什么能力[1],不知道 Effective Set、Permitted Set 等几个到底分别有什么作用。借助 gdb 我可以知道程序运行过程中所发生的事情。

我对 PAM、lightdm、pam_cap 还有很多我不关心的代码都不了解,gdb 可以让程序停在我感兴趣的地方,让我看看程序运行到某一行代码的时候到底发生了什么。也就是说我不熟悉代码库,但是了解原理,这个时候调试器能帮大忙。

问题的解决方案很简单,收获都来自调试的过程中,和学习的过程中。

[1] 这个视频是我的 Linux 能力的入门视频,我只知道能让 root 用户也受限 https://youtu.be/lBIvPys7L8w?feature=shared

另外一教程是https://linux-audit.com/kernel/capabilities/linux-capabilities-101/ 我只知道 capsh --print,但是看完之后对 capsh 怎么用还是不了解。

posted @ 2026-09-19 22:19  wngtk  阅读(12)  评论(0)    收藏  举报