Docker 使用 seccomp 无法获取系统时间的 bug 一则

 

最近需要在运行 Raspbian buster 的树莓派上用 Docker 跑一些任务,但是却遇到了一个奇怪的问题,感觉调研的过程比较有意思。

表征

最初的问题是使用 ubuntu:20.04 镜像时,无法运行 apt update,报系统时间错误,因此签名无效。检查后发现树莓派本身系统时间是正确的,在 docker 中获取时间是错误的(接近 unix epoch)的一个时间。再进一步尝试发现使用 ubuntu:18.04 镜像是工作的,表现如下:

[email protected]:~ $ date
Fri  7 Aug 15:19:12 CST 2020
[email protected]:~ $ sudo docker run --rm ubuntu:18.04 /bin/date
Fri Aug  7 07:19:27 UTC 2020
[email protected]:~ $ sudo docker run --rm ubuntu:20.04 /bin/date
Thu Feb 19 15:54:35 UTC 1970

这就奇怪了。Ubuntu 20.04 用的是 glibc 2.32,而 18.04 是 2.27。我首先怀疑是否树莓派的内核太老,syscall 的语义或者 ABI 发生了变化。但是 apt dist-upgrade 之后并没有解决问题。我反复用各种关键词 Google(以及百度),都没有搜出什么有用的东西。

挖掘

事情到现在,只能使用大法器 strace 了。当然,我没有办法在容器里面直接安装它(当然还是因为时间不对,甚至 tar 都没法解压,报 utime 调用失败)。于是我下载到本地之后在两个版本的容器中分别运行,得到关键结果如下:

# 18.04
clock_gettime(CLOCK_REALTIME, {tv_sec=1596783947, tv_nsec=198668123}) = 0
# 20.04
clock_gettime64(CLOCK_REALTIME, 0xbea10be8) = -1 EPERM (Operation not permitted)
clock_gettime64(CLOCK_REALTIME, 0xbea10bd0) = -1 EPERM (Operation not permitted)

这下就明白了,glibc 切换到了 clock_gettime64 这个 syscall,得到了异常返回值,所以产生了错误的结果(当然,我也怀疑它是不是根本没检查返回值)。我尝试用 --privileged 选项来运行 docker,果然正常了。

探源

但是问题是,为什么这么一个人畜无害的 syscall 会需要特权呢?

通过群友的提示,我了解到了 docker 使用 seccomp 来限制容器的行为,官方也有一个介绍,其中就包括内部能够使用的 syscall。但是由于一个已知的问题(见 Alpine Linux 的一个 issue),任何未知(不在 seccomp profile 白名单中)的 syscall 都会被拦下来返回 EPERM

根据上述 issue 中的提示,6/18 发布的 Docker 19.03.12 在 seccomp 的配置文件白名单中加入了一系列 64 位 syscall,其中就包括 clock_gettime64,具体的提交可见这个 commit。我本地的 Docker 只有 18.09.1,看来需要升级。

波折

然而当我一通操作升级到了 19.03.13 之后重新尝试,却发现问题还在。一番搜索查到了 docker 有 --security-opt seccomp=/path/to/file/your-custom-profile.json 或者 --security-opt seccomp=unconfined 的选项,允许手动指定 seccomp 的配置文件,或者直接禁用这一功能。后者的确是有效的,但是前者依然无效。我继续测试,确定了 profile 文件的确是生效的,比如从白名单去掉 execve 的话,整个容器都起不来;然而虽然里面有 clock_gettime64,却依旧是返回 EPERM 的。

这时候群友们指出,或许是 libseccomp2 的版本不够新,不认识这个 syscall。由于 Raspbian 没有 backports,我就从 Debian backports 里面下回来了更新的包,装上,然而还是不管用。这时候坏人指出可以试一试 testing,我抱着最后的希望装上。再次运行 docker run,日期居然对了。

于是,一个玄学的 bug 就这样在消耗我两个多小时以后解决了。