FUSE¶
定义¶
- 用户空间文件系统
一种文件系统,其中的数据和元数据由普通用户空间进程提供。可以通过内核接口正常访问该文件系统。
- 文件系统守护进程
提供文件系统数据和元数据的进程。
- 非特权挂载(或用户挂载)
由非特权(非 root)用户挂载的用户空间文件系统。文件系统守护进程以挂载用户的权限运行。注意:这与 /etc/fstab 中使用“user”选项允许的挂载不同,此处不讨论。
- 文件系统连接
文件系统守护进程和内核之间的连接。该连接一直存在,直到守护进程死亡或文件系统被卸载。请注意,分离(或延迟卸载)文件系统不会断开连接,在这种情况下,它将一直存在,直到释放对文件系统的最后一个引用。
- 挂载所有者
执行挂载的用户。
- 用户
正在执行文件系统操作的用户。
什么是 FUSE?¶
FUSE 是一个用户空间文件系统框架。它由一个内核模块 (fuse.ko)、一个用户空间库 (libfuse.*) 和一个挂载实用程序 (fusermount) 组成。
FUSE 最重要的特性之一是允许安全的非特权挂载。这为文件系统的使用开辟了新的可能性。一个很好的例子是 sshfs:一个使用 sftp 协议的安全网络文件系统。
用户空间库和实用程序可从 FUSE 主页获取:
文件系统类型¶
提供给 mount(2) 的文件系统类型可以是以下之一
- fuse
这是挂载 FUSE 文件系统的常用方法。mount 系统调用的第一个参数可以包含任意字符串,内核不会解释该字符串。
- fuseblk
该文件系统基于块设备。mount 系统调用的第一个参数被解释为设备的名称。
挂载选项¶
- fd=N
用于用户空间文件系统和内核之间通信的文件描述符。文件描述符必须通过打开 FUSE 设备('/dev/fuse')获得。
- rootmode=M
文件系统根的八进制表示的文件模式。
- user_id=N
挂载所有者的数字用户 ID。
- group_id=N
挂载所有者的数字组 ID。
- default_permissions
默认情况下,FUSE 不检查文件访问权限,文件系统可以自由地实现其访问策略,或者将其留给底层文件访问机制(例如,在网络文件系统的情况下)。此选项启用权限检查,根据文件模式限制访问。它通常与“allow_other”挂载选项一起使用。
- allow_other
此选项会覆盖限制文件访问的安全性措施,仅限于挂载文件系统的用户。默认情况下,此选项仅允许 root 用户使用,但可以通过(用户空间)配置选项删除此限制。
- max_read=N
使用此选项可以设置读取操作的最大大小。默认值是无限的。请注意,读取请求的大小仍然限制为 32 页(在 i386 上为 128 KB)。
- blksize=N
设置文件系统的块大小。默认值为 512。此选项仅对“fuseblk”类型挂载有效。
控制文件系统¶
FUSE 有一个控制文件系统,可以通过以下方式挂载
mount -t fusectl none /sys/fs/fuse/connections
将其挂载到 '/sys/fs/fuse/connections' 目录下使其向后兼容早期版本。
在 FUSE 控制文件系统下,每个连接都有一个以唯一编号命名的目录。
对于每个连接,此目录中都存在以下文件
- waiting
等待传输到用户空间或正在被文件系统守护进程处理的请求数。如果没有任何文件系统活动并且 'waiting' 非零,则文件系统已挂起或死锁。
- abort
向此文件写入任何内容都将中止文件系统连接。这意味着所有等待的请求都将被中止,并且将为所有中止的和新的请求返回错误。
只有挂载的所有者才能读取或写入这些文件。
中断文件系统操作¶
如果发出 FUSE 文件系统请求的进程被中断,则会发生以下情况
如果请求尚未发送到用户空间,并且信号是致命的(SIGKILL 或未处理的致命信号),则请求将出队并立即返回。
如果请求尚未发送到用户空间,并且信号不是致命的,则会为请求设置一个中断标志。当请求已成功传输到用户空间并且设置了此标志时,将排队一个 INTERRUPT 请求。
如果请求已发送到用户空间,则会排队一个 INTERRUPT 请求。
INTERRUPT 请求的优先级高于其他请求,因此用户空间文件系统将先接收排队的 INTERRUPT,然后再接收任何其他请求。
用户空间文件系统可以完全忽略 INTERRUPT 请求,或者可以通过将错误设置为 EINTR 来回复原始请求来响应它们。
在处理原始请求及其 INTERRUPT 请求之间也可能存在竞争。有两种可能性
在处理原始请求之前处理 INTERRUPT 请求
在回答原始请求后处理 INTERRUPT 请求
如果文件系统找不到原始请求,则应等待一段时间或等待一些新请求到达,之后应以 EAGAIN 错误回复 INTERRUPT 请求。在情况 1) 中,INTERRUPT 请求将重新排队。在情况 2) 中,INTERRUPT 回复将被忽略。
中止文件系统连接¶
在某些情况下,文件系统可能没有响应。原因可能是
用户空间文件系统实现损坏
网络连接中断
意外死锁
恶意死锁
(有关 c) 和 d) 的更多信息,请参见后面的章节)
在任何这些情况下,中止与文件系统的连接可能很有用。有几种方法可以做到这一点
杀死文件系统守护进程。适用于 a) 和 b) 的情况
杀死文件系统守护进程和所有文件系统用户。适用于所有情况,除了某些恶意死锁
使用强制卸载 (umount -f)。适用于所有情况,但仅当文件系统仍附加时(尚未延迟卸载)
通过 FUSE 控制文件系统中止文件系统。最强大的方法,始终有效。
非特权挂载如何工作?¶
由于 mount() 系统调用是一项特权操作,因此需要一个辅助程序 (fusermount),该程序以 setuid root 权限安装。
提供非特权挂载的含义是,挂载所有者不得利用此功能来危害系统。由此产生的明显要求是
挂载所有者不应借助挂载的文件系统获得提升的特权
挂载所有者不应非法访问其他用户和超级用户的进程中的信息
挂载所有者不应在其他用户或超级用户的进程中引起不良行为
如何满足要求?¶
挂载所有者可以通过以下方式获得提升的特权
创建包含设备文件的文件系统,然后打开此设备
创建包含 suid 或 sgid 应用程序的文件系统,然后执行此应用程序
解决方案是不允许打开设备文件,并在执行程序时忽略 setuid 和 setgid 位。为了确保这一点,fusermount 始终为非特权挂载添加“nosuid”和“nodev”到挂载选项中。
如果另一个用户正在访问文件系统中的文件或目录,则服务请求的文件系统守护进程可以记录执行操作的确切顺序和时间。否则,挂载所有者无法访问此信息,因此这算作信息泄露。
此问题的解决方案将在 C) 的第 2 点中提出。
挂载所有者可以通过多种方式在其他用户的进程中引起不良行为,例如
将文件系统挂载到挂载所有者无法修改(或者只能进行有限修改)的文件或目录上。
这在 fusermount 中得到解决,方法是检查挂载点的访问权限,并且仅当挂载所有者可以进行无限制修改时才允许挂载(对挂载点具有写访问权限,并且挂载点不是“粘滞”目录)
即使解决了 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' 将访问限制为同一 userns 或其后代中的用户。
使用 'allow_sys_admin_access' 模块选项。如果设置此选项,则超级用户的进程可以无限制地访问挂载,而不管 'allow_other' 设置或挂载用户的用户命名空间如何。
请注意,这两种放宽都使系统暴露在上一节中 B 和 C/2/i-ii 中描述的潜在信息泄漏或 DoS 攻击的风险中。
内核 - 用户空间接口¶
下图显示了如何在 FUSE 中执行文件系统操作(在此示例中为取消链接)。
| "rm /mnt/fuse/file" | FUSE filesystem daemon
| |
| | >sys_read()
| | >fuse_dev_read()
| | >request_wait()
| | [sleep on fc->waitq]
| |
| >sys_unlink() |
| >fuse_unlink() |
| [get request from |
| fc->unused_list] |
| >request_send() |
| [queue req on fc->pending] |
| [wake up fc->waitq] | [woken up]
| >request_wait_answer() |
| [sleep on req->waitq] |
| | <request_wait()
| | [remove req from fc->pending]
| | [copy req to read buffer]
| | [add req to fc->processing]
| | <fuse_dev_read()
| | <sys_read()
| |
| | [perform unlink]
| |
| | >sys_write()
| | >fuse_dev_write()
| | [look up req in fc->processing]
| | [remove from fc->processing]
| | [copy write buffer to req]
| [woken up] | [wake up req->waitq]
| | <fuse_dev_write()
| | <sys_write()
| <request_wait_answer() |
| <request_send() |
| [add request to |
| fc->unused_list] |
| <fuse_unlink() |
| <sys_unlink() |
注意
以上描述中的所有内容都经过了极大的简化
有几种方法可以死锁 FUSE 文件系统。由于我们讨论的是非特权的用户空间程序,因此必须对此采取一些措施。
场景 1 - 简单死锁:
| "rm /mnt/fuse/file" | FUSE filesystem daemon
| |
| >sys_unlink("/mnt/fuse/file") |
| [acquire inode semaphore |
| for "file"] |
| >fuse_unlink() |
| [sleep on req->waitq] |
| | <sys_read()
| | >sys_unlink("/mnt/fuse/file")
| | [acquire inode semaphore
| | for "file"]
| | *DEADLOCK*
解决此问题的方法是允许中止文件系统。
场景 2 - 棘手的死锁
这需要一个精心制作的文件系统。这是上述情况的一个变体,只是对文件系统的回调不是显式的,而是由缺页错误引起的。
| Kamikaze filesystem thread 1 | Kamikaze filesystem thread 2
| |
| [fd = open("/mnt/fuse/file")] | [request served normally]
| [mmap fd to 'addr'] |
| [close fd] | [FLUSH triggers 'magic' flag]
| [read a byte from addr] |
| >do_page_fault() |
| [find or create page] |
| [lock page] |
| >fuse_readpage() |
| [queue READ request] |
| [sleep on req->waitq] |
| | [read request to buffer]
| | [create reply header before addr]
| | >sys_write(addr - headerlength)
| | >fuse_dev_write()
| | [look up req in fc->processing]
| | [remove from fc->processing]
| | [copy write buffer to req]
| | >do_page_fault()
| | [find or create page]
| | [lock page]
| | * DEADLOCK *
解决方案基本上与上述相同。
另一个问题是,在将写入缓冲区复制到请求时,请求不得中断/中止。这是因为复制的目标地址在请求返回后可能无效。
这通过原子地进行复制来解决,并允许在使用 get_user_pages() 缺页访问属于写入缓冲区的页面时中止。 'req->locked' 标志指示何时正在进行复制,并且中止会延迟到此标志被取消设置。