FUSE

定义

用户空间文件系统

一种文件系统,其数据和元数据由普通用户空间进程提供。可以通过内核接口正常访问该文件系统。

文件系统守护进程

提供文件系统数据和元数据的进程。

非特权挂载(或用户挂载)

由非特权(非 root)用户挂载的用户空间文件系统。文件系统守护进程以挂载用户的权限运行。注意:这与 /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

此选项覆盖了将文件访问限制为挂载文件系统的用户的安全措施。默认情况下,此选项仅允许 root 用户使用,但可以使用(用户空间)配置选项删除此限制。

max_read=N

使用此选项可以设置读取操作的最大大小。默认值为无限。请注意,读取请求的大小无论如何都限制为 32 页(在 i386 上为 128k 字节)。

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 请求之间也可能存在竞争。有两种可能性

  1. INTERRUPT 请求在原始请求被处理之前被处理

  2. INTERRUPT 请求在原始请求已被应答之后被处理

如果文件系统找不到原始请求,它应该等待一段时间超时和/或等待一些新的请求到达,之后它应该回复 INTERRUPT 请求并返回 EAGAIN 错误。在情况 1)中,INTERRUPT 请求将被重新排队。在情况 2)中,INTERRUPT 回复将被忽略。

中止文件系统连接

可能存在文件系统无响应的某些情况。原因可能是

  1. 损坏的用户空间文件系统实现

  2. 网络连接断开

  3. 意外死锁

  4. 恶意死锁

(有关 c) 和 d) 的更多信息,请参阅后面的章节)

在这些情况下,中止与文件系统的连接可能很有用。有几种方法可以做到这一点

  • 杀死文件系统守护进程。在 a) 和 b) 的情况下有效

  • 杀死文件系统守护进程和文件系统的所有用户。在除某些恶意死锁之外的所有情况下都有效

  • 使用强制卸载(umount -f)。在所有情况下都有效,但前提是文件系统仍处于连接状态(尚未延迟卸载)

  • 通过 FUSE 控制文件系统中止文件系统。最强大的方法,始终有效。

非特权挂载如何工作?

由于 mount() 系统调用是一项特权操作,因此需要一个辅助程序 (fusermount),它已安装 setuid root。

提供非特权挂载意味着挂载所有者不得能够使用此功能来危害系统。由此产生的明显要求是

  1. 挂载所有者不应能够借助挂载的文件系统获得提升的权限

  2. 挂载所有者不应非法访问其他用户和超级用户的进程中的信息

  3. 挂载所有者不应能够在其他用户或超级用户的进程中引发不需要的行为

如何满足要求?

  1. 挂载所有者可以通过以下方式获得提升的权限

    1. 创建一个包含设备文件的文件系统,然后打开此设备

    2. 创建一个包含 suid 或 sgid 应用程序的文件系统,然后执行此应用程序

    解决方案是不允许打开设备文件并忽略执行程序时的 setuid 和 setgid 位。为了确保这一点,fusermount 始终将“nosuid”和“nodev”添加到非特权挂载的挂载选项中。

  2. 如果另一个用户正在访问文件系统中的文件或目录,则为请求提供服务的文件系统守护进程可以记录执行的操作的确切顺序和时间。此信息是挂载所有者无法访问的,因此这算作信息泄漏。

    此问题的解决方案将在 C) 的第 2 点中介绍。

  3. 挂载所有者可以通过多种方式在其他用户的进程中引发不需要的行为,例如

    1. 将文件系统挂载到挂载所有者原本无法修改(或只能进行有限修改)的文件或目录之上。

      这在 fusermount 中通过检查挂载点的访问权限并仅在挂载所有者可以进行无限修改(对挂载点具有写入访问权限,并且挂载点不是“sticky”目录)时才允许挂载来解决。

    2. 即使解决了 1),挂载所有者也可以更改其他用户进程的行为。

      1. 它可以减慢或无限期地延迟文件系统操作的执行,从而针对用户或整个系统创建 DoS。例如,锁定系统文件的 suid 应用程序,然后访问挂载所有者的文件系统上的文件可能会停止,从而导致系统文件永远被锁定。

      2. 它可以呈现无限长度的文件或目录,或无限深度的目录结构,可能会导致系统进程耗尽磁盘空间、内存或其他资源,从而再次导致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 设置或挂载用户的用户命名空间如何。

请注意,这两种放宽都会使系统面临潜在的信息泄漏或DoS,如上一节中的 B 和 C/2/i-ii 点中所述。

内核 - 用户空间接口

下图显示了在 FUSE 中如何执行文件系统操作(在本例中为 unlink)。

|  "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”标志指示何时正在进行复制,并且中止会延迟到取消设置此标志为止。