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却是空的。
环境搭建
为了进行调试,我首先搭建了调试所需的环境,并复现问题。具体步骤如下:
- 安装Fedora 42
- 使用命令
vagrant init bento/fedora-42初始化 Vagrantfile。 - 使用命令
vagrant up启动 Fedora-42 虚拟机。
- 使用命令
- 安装XFCE4桌面环境
- 使用命令
dnf install @xfce-desktop安装XFCE4桌面环境。
- 使用命令
- 安装调试信息 (可选,后面调试的时候再安装也不迟)
- 使用命令
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 可以保留,具体步骤如下:
- 在 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
- 为 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
- 通过 tty 登录 vagrant,
capsh --print看到 Ambient set 有 cap_net_bind,我们可以不需要切换root用户就能使用 1024 以下的端口。

通过 lightdm 登录,pam_cap 设置的 ambient 能力丢失了,具体验证步骤如下:
- 在 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
- 和上面的 login 验证步骤一样,修改
/etc/security/capability.conf。 - 运行下面的命令启动
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 还是空的。

调试过程
一边补知识,一边尝试。
前置知识:
- capabilities(7) 各个能力集合的作用和 execve 后的变化
- tlpi capability,稍微了解 libcap 的 API
- 略知 PAM: https://docs.rockylinux.org/guides/security/pam/
- 略知 PAM again: https://wpollock.com/AUnix2/PAM-Help.htm
PAM 和 Linux capabilities 一样难搞懂,写这篇博客的时候我暂时还不知道一个 PAM 应用该怎么写,我只知道有几个 API,大概会是什么顺序调用。有这些知识基本上也够用了。
一篇名为 'pam_cap not working with su for ambient setting' 的帖子记录了 ambient cap 怎么在 setuid 之后保留。
- https://bugzilla.kernel.org/show_bug.cgi?id=212945
- https://github.com/shadow-maint/shadow/pull/408#issuecomment-919673098 (关于 ambient caps更详细的讨论)
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 怎么用还是不了解。

浙公网安备 33010602011771号