文章目录
一日三坑:从 AppArmor 拦截到 GPU 直通死锁的 libvirt 排障实录
环境:Ubuntu 24.04, libvirt 9.x, QEMU, 3× NVIDIA L40 GPU
目标:部署 GPU 直通虚拟机vm-gpu,存储池挂载于/data
排障时长:约 6 小时 | 关键教训:安全模块与驱动占用的双重陷阱
坑位地图:三个层层递进的 Permission Denied
本次踩坑并非单点故障,而是权限层、安全层、驱动层的三个 Permission denied 连环套:
| 阶段 | 表象 | 真实卡点 | 紧急程度 |
|---|---|---|---|
| 第一坑 | virsh list 拒绝连接 | Unix Socket 用户组权限 | 🔧 低(配置问题) |
| 第二坑 | /data/docker 卷无法访问 | AppArmor 路径白名单拦截 | ⚠️ 中(安全策略) |
| 第三坑 | virsh list 卡死 / 命令无响应 | GPU 解绑死锁导致 libvirtd 僵尸 | 🚨 高(内核死锁) |
第一坑:Socket 权限 —— 入门级的门槛
现象
非 root 用户执行管理命令:
$ virsh -c qemu:///system list
error: Failed to connect socket to '/var/run/libvirt/libvirt-sock': Permission denied
根因
libvirtd 默认创建 Unix Domain Socket 的权限为 srwxr-x--- root:root,仅 root 可读写。
解决(标准方案)
将用户加入 libvirt 组,并放开 socket 的组权限:
sudo usermod -aG libvirt $USER
sudo sed -i 's/#unix_sock_group = "libvirt"/unix_sock_group = "libvirt"/' /etc/libvirt/libvirtd.conf
sudo sed -i 's/#unix_sock_rw_perms = "0770"/unix_sock_rw_perms = "0770"/' /etc/libvirt/libvirtd.conf
sudo systemctl restart libvirtd
newgrp libvirt # 立即生效
验证:ls -la /var/run/libvirt/libvirt-sock 应显示 srwxrwx--- libvirt:libvirt。
第二坑:AppArmor 拦截 —— 最隐蔽的 Permission Denied
场景还原
通过正规流程创建存储池:
virsh pool-define-as data-pool dir --target /data
virsh pool-build data-pool
virsh pool-start data-pool
virsh vol-create-as data-pool docker.qcow2 100G --format qcow2
虚拟机 XML 指向 /data/docker,启动时报错:
$ virsh start vm-gpu
error: Cannot access storage file '/data/docker': Permission denied
关键认知误区
不是文件名问题:AppArmor 不识别 “docker” 关键字,也不会因为路径里有 “docker” 就拦截。它是纯路径白名单机制——只要不在 /etc/apparmor.d/abstractions/libvirt-qemu 的允许列表中,无论叫 /data/docker 还是 /data/vm-disk,都会被拒。
我的选择:直接关闭 AppArmor
为快速验证,执行了最激进的方案:
sudo systemctl disable --now apparmor
sudo aa-teardown
效果:立即解除拦截,虚拟机可启动。
⚠️ 风险警示(事后反思)
关闭 AppArmor 确实解障,但移除了强制访问控制:
- 若 QEMU 存在逃逸漏洞,可访问宿主机任意路径(
/etc/shadow,/root) - 多租户环境下,虚拟机可突破存储边界,读写其他租户数据
更优方案(生产环境推荐):
# 仅放行 /data 路径,保留其他安全限制
sudo tee /etc/apparmor.d/local/abstractions/libvirt-qemu <<'EOF'
/data/ r,
/data/** rwk,
EOF
sudo apparmor_parser -r /etc/apparmor.d/abstractions/libvirt-qemu
第三坑:virsh 卡死与僵尸进程 —— 从"无响应"到"GPU 死锁"
现象
AppArmor 关闭后,执行:
$ virsh list
# 光标闪烁,无任何输出,Ctrl+C 无法中断
$ virsh start vm-gpu
# 同样卡死
排查:strace 定位阻塞点
对挂起的 virsh 进程追踪系统调用:
$ sudo strace -p $(pgrep -f "virsh list")
connect(5, {sa_family=AF_UNIX, sun_path="/var/run/libvirt/libvirt-sock"}, 110
结论:客户端正常,阻塞在 socket 连接阶段,说明 libvirtd 服务端无响应。
发现:libvirtd 已成僵尸进程
$ ps aux | grep libvirtd | grep -v grep
root 1723 0.0 0.0 0 0 ? Zsl 13:28 0:00 [libvirtd] <defunct>
状态码 Zsl 表示 Zombie(僵尸进程),且父进程为 systemd。僵尸进程不处理任何新连接,因此所有 virsh 命令无限期挂起。
为什么 libvirtd 会崩溃?
根本原因在 GPU 直通解绑死锁:
- 虚拟机配置 3× L40 GPU 直通,
<hostdev mode='subsystem' type='pci' managed='yes'/> - libvirtd 启动 VM 时,尝试自动将 GPU 从宿主机解绑并绑定到
vfio-pci - 但宿主机内核中
nvidia驱动仍牢牢占用 GPU:$ lspci -nnk | grep -A3 -i nvidia Kernel driver in use: nvidia # 致命问题:不是 vfio-pci - 内核级解绑操作死锁,libvirtd 主线程阻塞在 IOCTL 调用上
- 超时或信号处理异常后,进程未被 systemd 正确回收,沦为僵尸
手动验证死锁
尝试手动解绑 GPU,同样挂起:
$ echo "0000:06:00.0" | sudo tee /sys/bus/pci/devices/0000:06:00.0/driver/unbind
# 命令无响应,证实内核驱动死锁
根治方案:vfio-pci 预绑定
核心原则
GPU 直通必须让宿主机在启动阶段就彻底放手,不能依赖 managed='yes' 在运行时解绑(极易死锁)。
配置步骤
-
黑名单宿主机驱动(若宿主机无需 GPU 计算):
# /etc/modprobe.d/blacklist-nvidia.conf blacklist nvidia blacklist nouveau -
预绑定 vfio-pci:
# /etc/modprobe.d/vfio.conf options vfio-pci ids=10de:26b5 # L40 的 Vendor:Device ID -
更新 initramfs 并重启:
sudo update-initramfs -u sudo reboot -
验证绑定状态:
$ lspci -nnk | grep -A3 -i nvidia Kernel driver in use: vfio-pci # 成功 Kernel modules: nvidia
重启后 libvirtd 恢复正常,virsh start vm-gpu 顺利启动,GPU 直通生效。
经验总结:四层权限穿透与防御性配置
1. 权限 denied 是分层的
遇到 Permission denied 时,按此顺序排查:
- Unix 权限层:
ls -la查看属主和权限位,确认libvirt-qemu用户有访问权 - AppArmor/SELinux 层:
dmesg | grep apparmor查看内核拒绝日志,这是最容易被忽视的"隐形墙" - 驱动占用层:
lspci -nnk查看Kernel driver in use,GPU 直通必须显示vfio-pci而非nvidia
2. 关闭安全模块是技术债
AppArmor 的拦截是路径白名单机制,与文件名无关。关闭服务虽快,但生产环境应使用 /etc/apparmor.d/local/ 做针对性放行,保留系统的强制访问控制。
3. virsh 卡死 ≠ 客户端问题
virsh 命令无响应时,第一时间检查服务端:
ps aux | grep libvirtd | grep -v grep # 查看是否有 Zsl 僵尸进程
若发现僵尸,不要尝试 systemctl stop(会一起卡死),应直接重启宿主机,并追查是什么操作(如 GPU 解绑)导致崩溃。
4. GPU 直通必须"先放手,后启动"
- ❌ 错误:依赖
managed='yes'在virsh start时动态解绑(易触发内核死锁) - ✅ 正确:通过
vfio-pci在系统启动阶段预绑定,确保lspci显示Kernel driver in use: vfio-pci后再启动虚拟机
最终检查清单
在下次部署 GPU 直通环境前,按此清单验证:
- 非 root 用户已加入
libvirt组,socket 权限为0770 - 存储池路径已添加到 AppArmor 本地规则(或确认已禁用 AppArmor)
- 宿主机已 blacklist
nvidia/nouveau,并配置vfio-pci预绑定 - 重启后
lspci -nnk确认三张 L40 均为Kernel driver in use: vfio-pci - 首次启动 VM 前,确认 libvirtd 进程状态为
Ssl(正常睡眠),而非Zsl(僵尸)
后记:这次排障从最简单的用户组权限,深入到 AppArmor 的路径白名单机制,最终触及内核驱动的死锁与进程状态管理。最深的坑往往藏在"权限已给足,但安全模块/内核状态不允许"的交界处。
362



被折叠的 条评论
为什么被折叠?



