為什麼設備節點的綁定掛載會在 tmpfs 的根目錄中與 EACCES 中斷?
設置容器/沙箱的一個常見場景是希望在新的 tmpfs 中創建一組最小的設備節點(而不是暴露主機
/dev
),而我知道的唯一(非特權)方法是綁定掛載想要的人進入它。我正在使用的命令(內部unshare -mc --keep-caps
)是:mkdir /tmp/x mount -t tmpfs none /tmp/x touch /tmp/x/null mount -o bind /dev/null /tmp/x/null
打算將坐騎移到
/dev
. 但是,即使在移動之前,執行也會echo > /tmp/x/null
產生“權限被拒絕”錯誤 (EACCES
)。但是,如果我另外執行:
mkdir /tmp/x/y touch /tmp/x/y/null mount -o bind /dev/null /tmp/x/y/null echo > /tmp/x/y/null
寫入成功。我已經玩了很多次,但找不到應該發生這種情況的根本原因或原因。可以通過將綁定安裝的節點放在子目錄中並將它們的符號連結放在將成為 new 的文件系統的頂層中來解決它
/dev
,但似乎沒有必要這樣做。這是怎麼回事?對此有合理的解釋嗎?還是某些訪問控制邏輯出錯了?
嗯,這似乎是一個非常有趣的效果,這是三種機制結合在一起的結果。
第一個(微不足道的)點是,當您將某些內容重定向到文件時,shell 會打開目標文件,並
O_CREAT
選擇確保該文件尚不存在時將被創建。第二件要考慮的事情是,它
/tmp/x
是一個tmpfs
掛載點,而/tmp/x/y
它是一個普通目錄。鑑於您tmpfs
在沒有任何選項的情況下掛載,掛載點的權限會自動更改,使其變為全域可寫並具有粘性位(1777
,這是 的常用權限集/tmp
,因此這感覺像是一個正常的預設值),而 的權限/tmp/x/y
是可能0755
(取決於你的umask
)。最後,難題的第三部分是您設置使用者命名空間的方式:您指示
unshare(1)
將主機使用者的 UID/GID 映射到新命名空間中的相同 UID/GID。這是新命名空間中的唯一映射,因此嘗試在父/子命名空間之間轉換任何其他 UID 將導致所謂的溢出 UID,預設情況下是65534
-nobody
使用者(請參閱第 1user_namespaces(7)
節Unmapped user and group IDs
)。這使得/dev/null
(及其綁定掛載)由nobody
子使用者命名空間內擁有(因為在子使用者命名空間中沒有主機root
使用者的映射):$ ls -l /dev/null crw-rw-rw- 1 nobody nobody 1, 3 Nov 25 21:54 /dev/null
將所有事實結合在一起,我們得出以下結論:
echo > /tmp/x/null
嘗試使用選項打開現有文件O_CREAT
,而該文件位於全域可寫的粘性目錄中,並由 擁有nobody
,他不是包含它的目錄的所有者。現在,
openat(2)
仔細閱讀,逐字逐句:EACCES
指定 O_CREAT 的地方,啟用 protected_fifos 或 protected_regular sysctl,文件已經存在並且是 FIFO 或正常文件,文件的所有者既不是目前使用者也不是包含目錄的所有者,包含目錄是兩個世界- 或組可寫和粘性。詳見proc(5)中/proc/sys/fs/protected_fifos和/proc/sys/fs/protected_regular的描述。
這不是很精彩嗎?這似乎和我們的情況差不多……除了手冊頁只講述普通文件和 FIFO,而沒有講述設備節點這一事實。
好吧,讓我們看一下實際實現 this 的程式碼。我們可以看到,本質上,它首先檢查必須成功的異常情況(第一個
if
),然後如果粘性目錄是全域可寫的(第二個if
,第一個條件),它只是拒絕任何其他情況的訪問:static int may_create_in_sticky(umode_t dir_mode, kuid_t dir_uid, struct inode * const inode) { if ((!sysctl_protected_fifos && S_ISFIFO(inode->i_mode)) || (!sysctl_protected_regular && S_ISREG(inode->i_mode)) || likely(!(dir_mode & S_ISVTX)) || uid_eq(inode->i_uid, dir_uid) || uid_eq(current_fsuid(), inode->i_uid)) return 0; if (likely(dir_mode & 0002) || (dir_mode & 0020 && ((sysctl_protected_fifos >= 2 && S_ISFIFO(inode->i_mode)) || (sysctl_protected_regular >= 2 && S_ISREG(inode->i_mode))))) { const char *operation = S_ISFIFO(inode->i_mode) ? "sticky_create_fifo" : "sticky_create_regular"; audit_log_path_denied(AUDIT_ANOM_CREAT, operation); return -EACCES; } return 0; }
因此,如果目標文件是 char 設備(不是正常文件或 FIFO),
O_CREAT
當該文件位於全域可寫粘性目錄中時,核心仍然拒絕打開它。為了證明我找到了正確的原因,我們可以在以下任何一種情況下檢查問題是否消失:
- mount
tmpfs
with-o mode=777
— 這不會使掛載點有粘性;- 打開
/tmp/x/null
asO_WRONLY
,但沒有O_CREAT
選項(為了測試這個,編寫一個呼叫open("/tmp/x/null", O_WRONLY | O_CREAT)
and的程序open("/tmp/x/null", O_WRONLY)
,然後編譯並執行它strace -e trace=openat
以查看每個呼叫的返回值)。我不確定這種行為是否應該被視為核心錯誤,但文件
openat(2)
顯然並未涵蓋此系統呼叫實際上以EACCES
.