昨天,学长 @zzh1996 发现在 Docker for Mac 的容器中对文件 chattr +i 之后,容器就无法被正常删除。我在自己的电脑上复现了这个问题,并且,嗯,最后成功删掉了。在这里记录一下这个 workaround。

(PS:Linux 上的 Docker 未见此问题)

复现问题

版本:Docker Desktop 2.0.0.2 (30215),Engine: 18.09.1

跑一个 Ubuntu 的容器。设置 --privileged=true 以使 chattr +i 成功执行。

docker container run --privileged=true -it ubuntu bash

创建 immutable 的文件。

root@1865cedf0ef0:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@1865cedf0ef0:/# touch test
root@1865cedf0ef0:/# chattr +i test	# now `test` is immutable
root@1865cedf0ef0:/# rm test	# cannot remove when you're root
rm: cannot remove 'test': Operation not permitted
root@1865cedf0ef0:/# rm -f test	# even `-f` won't work
rm: cannot remove 'test': Operation not permitted
root@1865cedf0ef0:/# exit

在退出之后看一下现有的容器。

(base) ➜  ~ docker container ls -a
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS                      PORTS               NAMES
1865cedf0ef0        ubuntu               "bash"                   2 minutes ago       Exited (1) 2 minutes ago                        ecstatic_hawking
# 省略其他容器

然后……删不掉。

(base) ➜  ~ docker container rm 186
Error response from daemon: container 1865cedf0ef0e58303a0361b8e19cd3a882a1bc741990603fb446e153656589f: driver "overlay2" failed to remove root filesystem: remove /var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff/test: operation not permitted

同样,加上 -f 也没有任何作用。可以注意到,我们创建的文件阻止了容器的正常删除。

Workaround

那么,返回中指向的路径在哪里呢?很明显这不是我们主机上的路径。

由 Docker 的工作原理我们知道,Docker for Mac 需要虚拟一个 Linux 内核才能够在 macOS 上正常运行,而这个最外层的虚拟机就是我们的目标:我们需要在其上把 test 文件删掉。

Docker for Mac 选择的虚拟机环境是 Xhyve VM。根据这条链接的内容,我们可以进入这个虚拟机环境。

screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty

但是……

linuxkit-025000000001:/var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff# ls
test
linuxkit-025000000001:/var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff# chattr
-sh: chattr: not found
linuxkit-025000000001:/var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff#

这个环境没有 chattr……虽然有个包管理器 apk,但是……

linuxkit-025000000001:/var/lib# apk update
ERROR: Unable to lock database: Read-only file system
ERROR: Failed to open apk database: Read-only file system

外层虚拟机的系统部分是只读的,装不了什么东西。虽然缺 chattr,但是一些基础的东西还是比较齐的——甚至有 wget。如果去运行的话会发现这些工具都来自于 BusyBox。但是,BusyBox 里面应该是有 chattr 的啊1

这就尴尬了。

所幸的是,在 BusyBox 的网站上可以直接下载到编译好的二进制文件。

wget https://busybox.net/downloads/binaries/1.30.0-i686/busybox_CHATTR

然后,

linuxkit-025000000001:/var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff# chmod +x ./busybox_CHATTR
linuxkit-025000000001:/var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff# ./busybox_CHATTR -i test
linuxkit-025000000001:/var/lib/docker/overlay2/e000b8a86824f48cb824ab643ce7d035e7ec4f3114bad897a2235cf922bba972/diff# rm test

之后退出虚拟机环境,再删一次就行了。

附录 1: 能不能直接从其他 Linux 机器上复制一份 chattr 过来?

答案是:(大部分情况下)不行。

实话讲,这是我想到的第一个办法。但最终证明是行不通的。为什么?动态链接。

root@tao-kali:~# uname -a
Linux tao-kali 4.19.0-kali1-amd64 #1 SMP Debian 4.19.13-1kali1 (2019-01-03) x86_64 GNU/Linux
root@tao-kali:~# ldd /usr/bin/chattr
	linux-vdso.so.1 (0x00007ffe607c1000)
	libe2p.so.2 => /lib/x86_64-linux-gnu/libe2p.so.2 (0x00007ff779d4e000)
	libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 (0x00007ff779d48000)
	libblkid.so.1 => /lib/x86_64-linux-gnu/libblkid.so.1 (0x00007ff779cf3000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff779b32000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ff779b11000)
	/lib64/ld-linux-x86-64.so.2 (0x00007ff779d86000)
	libuuid.so.1 => /lib/x86_64-linux-gnu/libuuid.so.1 (0x00007ff779b08000)

可以注意到,这份 chattr 依赖于 7 个动态链接库。而这个精简的虚拟环境很难满足这些需求。

而下载得到的 chattr 是静态链接的。

(base) ➜  Downloads file busybox_CHATTR
busybox_CHATTR: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped

所以说……用于 rescue 的程序最好还是静态链接。

附录 2: chattr +i 到底做了什么?

简单地说,”immutable” 等实际是文件系统提供的一种特性。所以要对不支持此特性的文件系统(比如 FAT32)上的文件运行 chattr 是行不通的。

从 BusyBox 的源码中可以找到 chattr.c,该文件属于 e2fsprogs。分析代码可以发现在解码完参数之后,调用了宏 fsetflags(name, flags)(实际指向函数 fgetsetflags(name, NULL, flags)),通过 ioctl 对文件的 flags 进行了修改。

目前,Docker 使用 OverlayFS 文件系统。它是一种 union filesystem,可以让不同文件系统的文件挂载在同一个挂载点上。Docker 以这种方式实现分层镜像2。而在虚拟环境中 df -hT 可以看到,/dev/sda1(虚拟磁盘)是以 ext4 挂在 /var/lib 上的。