FUSE
定义
- 用户空间文件系统:
普通用户空间进程提供数据和元数据的文件系统。文件系统可以通过内核接口正常访问。 - 文件系统守护进程:
提供文件系统数据和元数据的进程。 - 非特权挂载(或用户挂载):
由非特权(非根用户)用户挂载的用户空间文件系统。文件系统守护进程以挂载用户的权限运行。注意:这与在 /etc/fstab 中允许使用 "user" 选项挂载的情况不同,在此不讨论。 - 文件系统连接:
文件系统守护进程与内核之间的连接。连接存在直到守护进程终止或文件系统被卸载。请注意,分离(或延迟卸载)文件系统不会断开连接,在这种情况下,连接将一直存在直到对文件系统的最后引用被释放。 - 挂载所有者:
进行挂载的用户。 - 用户:
执行文件系统操作的用户。
什么是 FUSE?
FUSE 是一个用户空间文件系统框架。它包括一个内核模块(fuse.ko)、一个用户空间库(libfuse.*
)和一个挂载实用程序(fusermount)。
FUSE 最重要的特性之一是允许安全的非特权挂载。这为文件系统的使用开辟了新的可能性。一个很好的例子是 sshfs:使用 sftp 协议的安全网络文件系统。
用户空间库和实用程序可从 FUSE 主页获取:
文件系统类型
传递给 mount(2) 的文件系统类型可以是以下之一:
- fuse
这是挂载 FUSE 文件系统的通常方式。挂载系统调用的第一个参数可以包含一个任意字符串,内核不会对其进行解释。 - fuseblk
文件系统基于块设备。挂载系统调用的第一个参数被解释为设备的名称。
挂载选项
- fd=N
用于用户空间文件系统与内核之间通信的文件描述符。文件描述符必须通过打开 FUSE 设备('/dev/fuse')获得。 - rootmode=M
以八进制表示的文件系统根目录的文件模式。 - user_id=N
挂载所有者的数值用户 ID。 - group_id=N
挂载所有者的数值组 ID。 - default_permissions
默认情况下,FUSE 不检查文件访问权限,文件系统可以自由实现其访问策略或将其留给底层文件访问机制(例如网络文件系统)。此选项启用权限检查,根据文件模式限制访问。通常与 'allow_other' 挂载选项一起使用。 - allow_other
此选项覆盖限制文件访问权限仅限于挂载文件系统的用户的安全措施。默认情况下,此选项仅允许根用户使用,但可以通过(用户空间)配置选项移除此限制。 - max_read=N
通过此选项可以设置读操作的最大大小。默认为无限制。请注意,读请求的大小无论如何都受限制为 32 页(在 i386 上为 128KB)。 - blksize=N
设置文件系统的块大小。默认为 512。此选项仅对 'fuseblk' 类型的挂载有效。
控制文件系统
FUSE 有一个控制文件系统,可以通过以下方式挂载:
mount -t fusectl none /sys/fs/fuse/connections
将其挂载在 '/sys/fs/fuse/connections' 目录下,使其向后兼容早期版本。
在 fuse 控制文件系统下,每个连接都有一个以唯一数字命名的目录。
对于每个连接,在此目录中存在以下文件:
- waiting
等待传输到用户空间或由文件系统守护进程处理的请求数量。如果没有文件系统活动且 'waiting' 不为零,则文件系统已挂起或死锁。 - abort
向此文件写入任何内容将中止文件系统连接。这意味着所有等待的请求将被中止,并且对所有中止和新请求都返回错误。
只有挂载的所有者可以读取或写入这些文件。
中断文件系统操作
如果发出 FUSE 文件系统请求的进程被中断,将会发生以下情况:
- 如果请求尚未发送到用户空间且信号是致命的(SIGKILL 或未处理的致命信号),则请求将被出列并立即返回。
- 如果请求尚未发送到用户空间且信号不是致命的,则为请求设置中断标志。当请求成功传输到用户空间并设置了此标志时,将排队一个中断请求。
- 如果请求已经发送到用户空间,则将排队一个中断请求。
中断请求优先于其他请求,因此用户空间文件系统将在任何其他请求之前接收排队的中断请求。
用户空间文件系统可以完全忽略中断请求,也可以通过将错误设置为 EINTR 向原始请求发送回复以予以尊重。
还可能存在处理原始请求和其中断请求之间的竞争。有两种可能性:
- 中断请求在处理原始请求之前被处理
- 中断请求在原始请求已被回答后被处理
如果文件系统找不到原始请求,应等待一段时间和/或一定数量的新请求到达,然后应以 EAGAIN 错误回复中断请求。在情况 1)中,中断请求将被重新排队。在情况 2)中,中断回复将被忽略。
中止文件系统连接
可能会出现文件系统不响应的情况。造成这种情况的原因可能包括:
a. 损坏的用户空间文件系统实现
b. 网络连接中断
c. 意外死锁
d. 恶意死锁
(有关 c) 和 d) 详见后续章节)
在这些情况下,中止与文件系统的连接可能是有用的。有几种方法可以实现这一点:
- 终止文件系统守护进程。适用于 a) 和 b) 情况
- 终止文件系统守护进程和所有文件系统的用户。适用于所有情况,除了一些恶意死锁
- 使用强制卸载(umount -f)。适用于所有情况,但仅当文件系统仍然挂载时(尚未延迟卸载)
- 通过 FUSE 控制文件系统中止文件系统。最强大的方法,始终有效。
非特权挂载如何工作?
由于 mount() 系统调用是特权操作,需要一个辅助程序(fusermount),该程序安装为 setuid root。
提供非特权挂载的含义是,挂载所有者不能利用此功能来危害系统。由此产生的明显要求包括:
A. 挂载所有者不应能够利用挂载的文件系统获得提升的特权
B. 挂载所有者不应从其他用户和超级用户的进程中获取非法访问信息
C. 挂载所有者不应能够诱发其他用户或超级用户的进程产生不良行为
如何满足需求?
- A. 挂载所有者可以通过以下方式获得提升的特权:
- 创建包含设备文件的文件系统,然后打开该设备
- 创建包含 suid 或 sgid 应用程序的文件系统,然后执行该应用程序
- 解决方案是不允许打开设备文件,并在执行程序时忽略 setuid 和 setgid 位。为了确保 fusermount 始终为非特权挂载添加 "nosuid" 和 "nodev" 到挂载选项中。
- B. 如果另一个用户正在访问文件系统中的文件或目录,为请求提供服务的文件系统守护程序可以记录执行的确切操作序列和时间。这些信息对于挂载所有者是无法访问的,因此这被视为信息泄漏。
这个问题的解决方案将在 C) 的第 2 点中提出。 - C. 挂载所有者有几种方式可以引发其他用户进程中的不良行为,例如:
- 将文件系统挂载到挂载所有者本来无法修改的文件或目录上(或者只能进行有限的修改)。
这在 fusermount 中得到解决,通过检查挂载点的访问权限,并且只有在挂载所有者可以进行无限制修改(对挂载点具有写访问权限,并且挂载点不是 "sticky" 目录)时才允许挂载。 - 即使解决了 1),挂载所有者仍然可以改变其他用户进程的行为。
- 它可以减慢或无限期延迟执行文件系统操作,对用户或整个系统造成 DoS。例如,一个 suid 应用程序锁定了系统文件,然后访问挂载所有者的文件系统上的文件,这样会导致系统文件永远被锁定。
- 它可以呈现无限长度的文件或目录,或者无限深度的目录结构,可能导致系统进程占用磁盘空间、内存或其他资源,再次导致 DoS。
- 这以及 B) 的解决方案是不允许进程访问文件系统,否则挂载所有者无法监视或操纵。因为如果挂载所有者可以 ptrace 一个进程,它可以做到上述所有操作而不使用 FUSE 挂载,因此可以使用与 ptrace 中使用的相同标准来检查进程是否被允许访问文件系统。
请注意,ptrace 检查并不是严格必要的,以防止 C/2/i,只需检查挂载所有者是否具有足够的特权来向访问文件系统的进程发送信号即可,因为 SIGSTOP 可以用于获得类似的效果。
我认为这些限制是不可接受的?
如果系统管理员足够信任用户,或者可以通过其他措施确保系统进程永远不会进入非特权挂载,可以通过以下几种方式放宽最后一个限制:
- 使用 'user_allow_other' 配置选项。如果设置了此配置选项,挂载用户可以添加 'allow_other' 挂载选项,从而禁用对其他用户进程的检查。
用户命名空间与 'allow_other' 有一个不直观的交互:通常受限于使用 'allow_other' 进行挂载的非特权用户可以在其特权用户命名空间中执行挂载。如果任何进程可以访问这样的 'allow_other' 挂载,这将使挂载用户能够操纵其非特权用户命名空间中的进程。因此,出于这个原因,'allow_other' 限制对于相同用户命名空间或后代中的用户的访问。 - 使用 'allow_sys_admin_access' 模块选项。如果设置了此选项,超级用户的进程可以无限制地访问挂载,而不受 allow_other 设置或挂载用户的用户命名空间的限制。
请注意,这两种放宽方式都会使系统面临潜在的信息泄漏或 DoS,就像前面部分 B 和 C/2/i-ii 中描述的那样。
内核 - 用户空间接口
以下图表显示了在 FUSE 中执行文件系统操作(例如 unlink)的过程。
| "rm /mnt/fuse/file" | FUSE 文件系统守护程序 | | | | >sys_read() | | >fuse_dev_read() | | >request_wait() | | [在 fc->waitq 上休眠] | | | >sys_unlink() | | >fuse_unlink() | | [从 fc->unused_list 获取请求] | >request_send() | | [将请求排队到 fc->pending] | [唤醒 fc->waitq] | [被唤醒] | >request_wait_answer() | | [在 req->waitq 上休眠] | | | <request_wait() | | [从 fc->pending 中移除请求] | | [将请求复制到读缓冲区] | | [将请求添加到 fc->processing] | | <fuse_dev_read() | | <sys_read() | | | | [执行 unlink] | | | | >sys_write() | | >fuse_dev_write() | | [在 fc->processing 中查找请求] | | [从 fc->processing 中移除] | | [将写缓冲区复制到请求] | [被唤醒] | [唤醒 req->waitq] | | <fuse_dev_write() | | <sys_write() | <request_wait_answer() | | <request_send() | | [将请求添加到 fc->unused_list] | <fuse_unlink() | | <sys_unlink() |
注意
上述描述中的所有内容都经过了大幅简化
有几种方式可以使 FUSE 文件系统陷入死锁。由于我们谈论的是非特权用户空间程序,因此必须对此进行处理。
- 情景 1 - 简单死锁:
| "rm /mnt/fuse/file" | FUSE 文件系统守护程序 | | | >sys_unlink("/mnt/fuse/file") | | [获取 "file" 的 inode 信号量] | | >fuse_unlink() | | [在 req->waitq 上休眠] | | | <sys_read() | | >sys_unlink("/mnt/fuse/file") | | [获取 "file" 的 inode 信号量] | | *死锁*
对此的解决方案是允许中止文件系统。
- 情景 2 - 棘手的死锁
这需要一个精心设计的文件系统。这是上述情况的变体,只是文件系统的回调不是显式的,而是由页面错误引起的。
| 神风文件系统线程 1 | 神风文件系统线程 2 | | | [fd = open("/mnt/fuse/file")] | [正常服务请求] | [将 fd 映射到 'addr'] | | [关闭 fd] | [FLUSH 触发 'magic' 标志] | [从 addr 读取一个字节] | | >do_page_fault() | | [查找或创建页面] | | [锁定页面] | | >fuse_readpage() | | [排队读取请求] | | [在 req->waitq 上休眠] | | | [将请求读取到缓冲区] | | [在 addr 之前创建回复头] | | >sys_write(addr - headerlength) | | >fuse_dev_write() | | [在 fc->processing 中查找请求] | | [从 fc->processing 中移除] | | [将写缓冲区复制到请求] | | >do_page_fault() | | [查找或创建页面] | | [锁定页面] | | * 死锁 *
解决方案基本上与上述相同。
另一个问题是,在将写缓冲区复制到请求时,请求不能被中断/中止。这是因为在请求返回后,复制的目标地址可能无效。
这通过原子方式进行复制来解决,并且允许在使用 get_user_pages() 为写缓冲区引起页面错误时中止。'req->locked' 标志指示复制何时正在进行,并且中止将延迟到取消此标志为止。