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 請求之間也可能存在競爭。有兩種可能性
INTERRUPT 請求在原始請求被處理之前被處理
INTERRUPT 請求在原始請求已被應答之後被處理
如果檔案系統找不到原始請求,它應該等待一段時間超時和/或等待一些新的請求到達,之後它應該回復 INTERRUPT 請求並返回 EAGAIN 錯誤。在情況 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 中透過檢查掛載點的訪問許可權並僅在掛載所有者可以進行無限修改(對掛載點具有寫入訪問許可權,並且掛載點不是“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”將訪問許可權限制為同一 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”標誌指示何時正在進行復制,並且中止會延遲到取消設定此標誌為止。