「一切皆文件」是 UNIX 的基本设计哲学。文件按照层级关系组织为树形目录,构成了文件系统 的基本形态。用户使用文件系统来保存数据时,不必关心数据底层的存储方式,便可以按照约定的接口规范进行访问。
关于文件系统的接口规范,应用最为广泛的莫过于 POSIX,源于 IEEE 委员会编写的相关标准,其中有些章节是关于文件及目录操作的。标准本身比较冗长晦涩,在此不作深入探讨。我们可以参考 Quora 上的一个问答 “What does POSIX conformance/compliance mean in the distributed systems world?” ,对此概括的比较全面。
POSIX 兼容要求文件系统具备以下几项特征:
open(O_CREAT)
,目录通过 mkdir
创建等等opendir/readdir
遍历rename
、 link / unlink
、 symlink / readlink
等修改write
或 writev
写入, fsync
时要求持久化,通过 read
或 readv
读取stat
, chmod / chown
等一个文件系统是否真正满足 POSIX 兼容性,我们可以通过测试工具来检验。比较流行的一个测试用例集是 pjdfstest,来源于 FreeBSD,也适用于 Linux 等系统。pjdfstest 的测试用例需要以 root 身份来运行,并且要求系统里安装了 Perl 和 TAP::Harness(Perl 软件包),测试过程如下:
cd /path/to/filesystem/under/test
sudo prove --recurse --verbose /path/to/pjdfstest/tests
我们选取了几种云环境中的共享文件系统进行测试,统计测试结果中的失败用例如下:
image
因为 Amazon EFS 失败的测试用例相比其他产品大了几个数量级,为了方便比较,上图的横坐标使用了对数坐标。
我们还同时测试了 S3FS 和 Goofys,失败的用例数均为数百项乃至上千项,其根本原因是这两个项目并不是严格按照文件系统来设计的:
下面我们将测试的失败用例进行分类统计,挑选几类比较有代表性的来分析下会对应用造成何种限制。
[图片上传失败...(image-8410b3-1646300520186)]
总的来说,无论从数量还是类别来看,JuiceFS 的失败用例都更少,有更好的兼容性。Amazon EFS 的失败用例无论从总数及类别均大大超出其它几种文件系统,无法放入同一图表对比,后面将单独分析。
JuiceFS 在本次测试中通过了8811项用例中的绝大多数,仅在 utimensat 测试集上失败了 3 项。对应日志如下:
…
/root/pjdfstest/tests/utimensat/08.t ........
not ok 5 - tried 'lstat pjdfstest_bfaee1fc7f2c1f80768e30f203f41627 atime_ns', expected 100000000, got 0
not ok 6 - tried 'lstat pjdfstest_bfaee1fc7f2c1f80768e30f203f41627 mtime_ns', expected 200000000, got 0
Failed 2/9 subtests
/root/pjdfstest/tests/utimensat/09.t ........
not ok 5 - tried 'lstat pjdfstest_7911595d91adcf915009f551ac48e1f2 mtime', expected 4294967296, got 0
这几个测试用例出自utimensat/08.t和utimensat/09.t。其中 08.t 是测试亚秒级的文件访问时间和修改时间精度,09.t 则是要求支持64位时间戳。
JuiceFS 目前只支持秒,时间戳保存为32位整数,故无法通过这三个测试(实际上本次测试涉及的所有文件系统都无法100%通过这个测试集)。如果您的应用场景要求秒以下的时间精度或者更大范围,欢迎联系我们商讨解决方案。
除了和 JuiceFS 一样在 utimesat 测试集上存在若干失败结果之外,GCP Filestore 还在 unlink 测试集中失败了 1 项。这一项在其他所有文件系统中也都是失败的。
/root/pjdfstest/tests/unlink/14.t ...........
not ok 4 - tried 'open pjdfstest_b03f52249a0c653a3f382dfe1237caa1 O_RDONLY : unlink pjdfstest_b03f52249a0c653a3f382dfe1237caa1 : fstat 0 nlink', expected 0, got 1
该测试集(unlink/14.t)用于验证一个文件在打开状态下被删除时的行为:
desc="An open file will not be immediately freed by unlink"
删除文件的操作在系统层面实际对应于 unlink,即移除该文件名到对应 inode 的链接,对应 nlink 的值减 1,这个测试用例就是要验证这一点。
# A deleted file's link count should be 0
expect 0 open ${n0} O_RDONLY : unlink ${n0} : fstat 0 nlink
文件内容只有在链接数(nlink)减少至 0 并且没有打开的文件描述符(fd)指向该文件时才会被真正删除。如果 nlink 没有被正确更新,可能会导致本该删除的文件仍然残留在系统里。
CFS 相比 Google Filestore,还未能通过 open 和 symlink 的几项测试。
选取其中一部分失败日志如下:
/root/pjdfstest/tests/open/07.t .............
not ok 5 - tried '-u 65534 -g 65534 open pjdfstest_f24a42815d59c16a4bde54e6559d0390 O_RDONLY,O_TRUNC', expected EACCES, got 0
not ok 7 - tried '-u 65533 -g 65534 open pjdfstest_f24a42815d59c16a4bde54e6559d0390 O_RDONLY,O_TRUNC', expected EACCES, got 0
not ok 9 - tried '-u 65533 -g 65533 open pjdfstest_f24a42815d59c16a4bde54e6559d0390 O_RDONLY,O_TRUNC', expected EACCES, got 0
Failed 3/23 subtests
此测试集 open/07.t 用于验证不具备写权限时,应该对 O_TRUNC 模式返回 EACCES 错误这一行为。
desc="open returns EACCES when O_TRUNC is specified and write permission is denied"
上面这三个失败日志需要结合测试代码来分析,分别对应 owner,group 和 other 三种情况。不失一般性,我们仅就 owner 情况进行分析 :
expect 0 -u 65534 -g 65534 chmod ${n1} 0477
expect EACCES -u 65534 -g 65534 open ${n1} O_RDONLY,O_TRUNC
首先设置文件 owner 权限为 4,即 r--
只读,然后尝试以 O_RDONLY,O_TRUNC 模式打开文件,预期应该返回 EACCES,实际返回了 0。
根据 The Single UNIX ® Specification, Version 2 中对 O_TRUNC 的说明
O_TRUNC If the file exists and is a regular file, and the file is successfully opened O_RDWR or O_WRONLY, its length is truncated to 0 and the mode and owner are unchanged. It will have no effect on FIFO special files or terminal device files. Its effect on other file types is implementation-dependent. The result of using O_TRUNC with O_RDONLY is undefined.
O_TRUNC 与 O_RDONLY 组合使用的结果是未知的,而且此用例的被测文件本身就是空文件,O_TRUNC 不会产生任何效果。
对应测试日志如下:
/root/pjdfstest/tests/symlink/03.t ..........
not ok 1 - tried 'symlink 7ea12171c487d234bef89d9d77ac8dc2929ea8ce264150140f02a77fc6dcad7c3b2b36b5ed19666f8b57ad861861c69cb63a7b23bcc58ad68e132a94c0939d5/.../... pjdfstest_57517a47d0388e0c84fa1915bf11fe4a', expected 0, got EINVAL
not ok 2 - tried 'unlink pjdfstest_57517a47d0388e0c84fa1915bf11fe4a', expected 0, got ENOENT
Failed 2/6 subtests
该测试集(symlink/03.t)用于测试路径超出 PATH_MAX 长度时 symblink 的行为
desc="symlink returns ENAMETOOLONG if an entire length of either path name exceeded {PATH_MAX} characters"
失败的用例对应代码如下:
n0=`namegen`
nx=`dirgen_max`
nxx="${nx}x"
mkdir -p "${nx%/*}"
expect 0 symlink ${nx} ${n0}
expect 0 unlink ${n0}
该测试用例是要创建长度为 PATH_MAX (包括结尾的0在内)的符号链接,通不过表明无法在 腾讯云 NAS 上创建长度为 PATH_MAX 的符号链接。
相比腾讯云 NAS,阿里云 NAS 在 symlink 上表现正常,但未能通过 chmod 和 rename 上的几项测试用例。
在这个测试集中,阿里云 NAS 失败了以下几个项目
/root/pjdfstest/tests/chmod/12.t ............
not ok 3 - tried '-u 65534 -g 65534 open pjdfstest_db85e6a66130518db172a8b6ce6d53da O_WRONLY : write 0 x : fstat 0 mode', expected 0777, got 04777
not ok 4 - tried 'stat pjdfstest_db85e6a66130518db172a8b6ce6d53da mode', expected 0777, got 04777
not ok 7 - tried '-u 65534 -g 65534 open pjdfstest_db85e6a66130518db172a8b6ce6d53da O_RDWR : write 0 x : fstat 0 mode', expected 0777, got 02777
not ok 8 - tried 'stat pjdfstest_db85e6a66130518db172a8b6ce6d53da mode', expected 0777, got 02777
not ok 11 - tried '-u 65534 -g 65534 open pjdfstest_db85e6a66130518db172a8b6ce6d53da O_RDWR : write 0 x : fstat 0 mode', expected 0777, got 06777
not ok 12 - tried 'stat pjdfstest_db85e6a66130518db172a8b6ce6d53da mode', expected 0777, got 06777
Failed 6/14 subtests
该测试集(chmod/12.t)用于测试 SUID/SGID 位的行为
desc="verify SUID/SGID bit behaviour"
我们选取其中的第11和12个测试用例来详细解释一下,同时覆盖了这两个权限位
# Check whether writing to the file by non-owner clears the SUID+SGID.
expect 0 create ${n0} 06777
expect 0777 -u 65534 -g 65534 open ${n0} O_RDWR : write 0 x : fstat 0 mode
expect 0777 stat ${n0} mode
expect 0 unlink ${n0}
此处,我们先以 06777 的权限创建目标文件,然后修改文件内容,检查 SUID 和 SGID 是否被正确清除。文件权限里的 777 大家会比较熟悉,分别对应 owner,group和 other 的 rwx,即可读、可写、可执行。最前面的 0 表示八进制数。
第二位 6 需要着重解释下,这个八位元组(octet)代表特殊权限位,其中前两位分别对应 setuid/setgid(或称 SUID/SGID),可以应用于可执行文件及公共目录。该权限位被设置时,任何用户都会以 owner (或 group)身份来运行该文件。这个特殊的属性允许用户获取通常只对 owner 开放的文件和目录访问权限。例如 passwd 命令就设置了 setuid 权限,这允许普通用户修改密码,因为保存密码的文件是只允许 root 访问的,用户不可直接修改。
setuid/setgid 设计的出发点是提供一种方法,让用户以限定的方式(指定可执行文件)访问受限文件(非当前用户所有)。因此,当文件被非 owner 修改时应自动清除此权限位,以避免用户通过这个途径获取其他权限。
从测试结果中我们可以看到在阿里云 NAS 中,文件被非 owner 修改时,setuid/setgid 均未被清除,这样实际上用户可以通过修改文件内容以该 owner 身份进行任意操作,这将会是个安全隐患。
阿里云 NAS 在这个测试集上失败数量较多,达到了 24 项,全部出现在 rename/09.t 中:
desc="rename returns EACCES or EPERM if the directory containing 'from' is marked sticky, and neither the containing directory nor 'from' are owned by the effective user ID"
这个测试集用于检验 sticky 位被设置时 rename 的行为:当包含源对象的目录设置了 sticky 权限位的时候,并且源对象和包含目录的 owner 都与有效用户ID(effective user ID)不同时,rename 应该返回 EACCES 或 EPERM。(这样的复杂逻辑令人联想到三国杀的武将技能设定……)。
sticky 位的典型应用是 /tmp 目录,允许所有人创建内容,但是只有 owner 才能删除文件。FTP 里面的公共上传目录通常也是这种设置。
几个失败的测试用例表明阿里云 NAS 对 sticky 位的支持还不够完善,非 owner 的 rename 操作没有被拒绝,并且产生了实际的效果——源文件被重命名。这种行为越过了文件系统的访问控制,对用户文件的安全性造成了威胁。
Amazon Elastic File System (EFS) 在 pjdfstest 测试中的不仅失败比例极高(8811个测试用例失败了1533个),而且几乎覆盖了所有类别,这比较令人意外。
image
EFS 支持以 NFS 方式挂载,但对 NFS 特性的支持并不完整。比如EFS 不支持块设备和字符设备,这直接导致了 pjdfstest 中大量测试用例的失败。排除这两类文件之后,仍然有上百项不同类别的失败,所以在复杂场景中应用 EFS 必须慎之又慎。
通过上面的对比分析,JuiceFS 在兼容性方面表现最好,像大多数网络文件系统一样,为了性能牺牲了秒以下的时间精度和范围(1970 - 2106 年)。Google Filestore 和腾讯云 CFS 次之,有几类未能通过。而阿里云 NAS 和 Amazon EFS 的兼容性最差,有大量的兼容性测试通不过,其中包括有严重安全隐患的若干个测试用例,使用前建议做安全性评估。
JuiceFS 一直非常重视对 POSIX 标准的高度兼容,我们把 pjdfstest 等兼容性测试工具同其他随机和并发测试工具(比如 fsracer、fstool 等)一起作为集成测试工具,在持续完善功能、提高性能的同时,尽力保持最大程度的 POSIX 兼容性,避免用户在使用过程中落入各种陷阱,从而更加专注于自身业务的发展。