Docker容器内ls -l显示文件权限等字段为问号的问题排查
问题背景
此前同类ls -l输出问号的问题多由权限问题导致,本次场景存在本质差异。
环境信息
已下线Docker宿主机环境
- Kernel 3.10
- docker 18.06
- glibc 2.17
- libseccomp 2.3.1
- coreutils 8.22
SLES 15 Docker镜像环境
- glibc 2.31
- coreutils 8.32
故障现象与初步排查
使用命令docker run -it --rm -u root <docker-image> bash启动容器后,家目录下存在bin目录,执行ls可正常显示该目录,但执行ls -l时出现大量问号,输出如下:
$ ls bin $ ls -l ls: cannot access 'bin': Operation not permitted total 0 d????????? ? ? ? ? ? bin
前期初步排查曾判断是宿主机3.10内核未实现statx系统调用(该调用在Linux 4.11版本正式加入,glibc 2.28版本才新增对应库支持,coreutils 8.32及以上版本的ls会优先调用该接口)导致,但后续测试发现,使用docker run -it --rm --security-opt seccomp=unconfined -u root <docker-image> bash关闭seccomp配置启动容器时,ls -l可正常运行,输出如下:
$ ls bin $ ls -l total 0 drwxr-xr-x 2 abcuser abcuser 6 Jul 4 2022 bin
该现象说明问题与seccomp配置直接相关,但存在两个已知逻辑与现象矛盾:
- 公开提交记录显示
statx早在Docker 18.04版本就已加入默认seccomp白名单,本次使用的Docker 18.06理应放行该系统调用 - coreutils实现逻辑中,若
statx调用不可用,会自动回退使用stat系列系统调用,默认seccomp配置下本应正常运行
通过strace抓取两种场景的系统调用轨迹,得到关键差异:
- 默认seccomp配置下:
statx调用返回-1 ENOSYS (Function not implemented)后,后续write、getdents64、close、fstat、openat等所有系统调用均返回ENOSYS错误,程序执行流程完全中断 - 关闭seccomp配置下:
statx调用同样返回ENOSYS错误,但后续会自动调用newfstatat等stat系列接口完成文件信息获取,流程正常执行
问题解答
1、为何使用默认seccomp配置时ls -l无法正常运行?
根因是宿主机搭载的旧版本libseccomp存在已知逻辑缺陷:
- 当前宿主机的libseccomp版本为2.3.1,该版本未实现未知系统调用返回ENOSYS时的透传机制。虽然Docker 18.06的默认seccomp规则确实已经将
statx加入了放行白名单,但白名单的放行逻辑只对内核真实存在的系统调用生效:当容器内程序调用3.10内核未实现的statx时,内核返回ENOSYS,旧版libseccomp不会将这个错误正常透传给用户态程序,反而会误判为进程非法调用了白名单外的系统调用,直接触发拦截逻辑,导致后续所有系统调用都无法正常执行,coreutils内置的statx失败回退逻辑根本没有机会运行。 - 这个逻辑缺陷在libseccomp 2.4及以上版本才被修复,新版本libseccomp会识别内核返回的ENOSYS错误,直接透传给用户态程序,不会触发拦截。
2、底层内核未实现statx的场景下,为何关闭seccomp配置后ls -l可正常运行?
关闭seccomp配置后,系统调用路径上不存在seccomp过滤器的拦截篡改,所有返回值都会直接传递给用户态程序:
- 程序第一次调用
statx时,内核因为自身未实现该系统调用,正常返回ENOSYS错误,这个错误被ls进程正常捕获 - coreutils 8.32的
ls内置了兼容逻辑,捕获到statx调用失败的错误后,会自动回退调用newfstatat等在3.10内核中已经成熟支持的传统stat系列系统调用 - 这些传统系统调用没有被拦截,能正常返回文件的权限、属主、大小、时间戳等元数据,因此
ls -l可以输出正常结果。
3、使用Docker默认seccomp配置时,如何在宿主机或容器内通过命令查询默认seccomp白名单包含的系统调用列表?
分宿主机、容器两个场景操作:
- 宿主机侧查询(无需进入容器)
Docker默认seccomp配置在不同发行版的存放路径略有差异,优先通过以下方式查询:- 先查找默认seccomp配置文件路径,常见路径为
/etc/docker/seccomp.json、/var/lib/docker/seccomp/default.json,找到后直接执行命令提取允许的系统调用:# 替换为环境中实际的seccomp配置文件路径,需提前安装jq工具 jq -r '.syscalls[] | select(.action == "SCMP_ACT_ALLOW") | .names[]' /etc/docker/seccomp.json - 如果找不到本地配置文件,可以启动一个临时特权容器,从容器内的Docker默认配置路径导出规则,再过滤放行的系统调用即可。
- 先查找默认seccomp配置文件路径,常见路径为
- 容器内查询
普通权限容器默认没有读取seccomp规则的权限,需要在启动容器时添加--cap-add SYS_ADMIN --security-opt apparmor=unconfined参数,进入容器后操作:- 先安装seccomp相关工具,以SLES 15为例:
zypper refresh zypper install -y libseccomp-tools strace - 可以通过
seccomp_dump工具直接导出当前进程生效的seccomp规则,也可以通过strace跟踪系统调用过程,过滤出被允许的系统调用列表。
注意:如果容器启动时没有加对应权限,只能通过cat /proc/self/status | grep Seccomp看到seccomp的启用模式,无法获取完整的白名单内容。
- 先安装seccomp相关工具,以SLES 15为例:
内容的提问来源于stack exchange,提问作者Praveen Lobo
相关产品推荐
相关产品推荐

